go基础
基础#
函数#
命名返回值可以在函数体内作为一个普通的局部变量使用,而不需要在函数体内通过 :=
或 var
声明。
闭包#
闭包就是一个函数“捕获”了和它在同一作用域的其他常量和变量。这就意味着当闭包被调用的时候,不管在程序什么地方调用,闭包都能够使用这些常量或者变量。它不关心这些捕获了的变量和常量是否已经超出了作用域,所以只要闭包还在使用它,这些变量就还会存在。
闭包是由函数及其相关引用环境组合而成的实体,一般通过在匿名函数中引用外部函数的局部变量或包全局变量构成。
如果函数返回的闭包引用了该函数的局部变量(参数或函数内部变量):
- 多次调用该函数,返回的多个闭包所引用的外部变量是多个副本,原因是每次调用函数都会为局部变量分配内存。
- 用一个闭包函数调用多次,如果该闭包修改了其引用的外部变量,则每一次调用该闭包对该外部变量都有影响,因为闭包函数共享外部引用。(如果一个函数调用返回的闭包引用修改了全局变量,则每次调用都会影响全局变量)
- 同一个函数返回的多个闭包共享该函数的局部变量。
空结构体#
在 Go 语言中,我们可以定义空结构体(empty struct),即没有任何成员变量的结构体,使用关键字 struct{} 来表示。这种结构体似乎没有任何用处,但实际上它在 Go 语言中的应用非常广泛。
1、空结构体的定义和初始化
空结构体是指不包含任何字段的结构体。在 Golang 中,可以使用 struct{} 来定义一个空结构体。下面是一个简单的示例:
func main() {
var s struct{}
fmt.Printf("%#v", s) // 输出: struct {}{}
}
注意,在打印时使用了 %#v 占位符,这个占位符可以将变量以 Go 语法格式输出。
需要注意的是,空结构体变量实际上不占用任何内存空间,也就是说,它的大小是 0 字节。
2、空结构体的大小和内存占用
正如上面提到的,空结构体的大小是 0 字节。这意味着它不占用任何内存空间。这一点可以通过使用 unsafe.Sizeof 函数来验证:
package main
import (
"fmt"
"unsafe"
)
func main() {
var s struct{}
fmt.Printf("Size of struct{}: %v", unsafe.Sizeof(s)) // 输出: Size of struct{}: 0
}
需要注意的是,尽管空结构体的大小为 0,但它并不意味着它不能被作为函数参数或返回值传递。因为在 Go 中,每个类型都有自己的类型信息,可以用于类型检查和转换。因此,即使是空结构体,在类型系统中也有它自己的位置和作用。
3、空结构体作为占位符
空结构体最常见的用途是作为占位符。在函数或方法签名中,如果没有任何参数或返回值,那么可以使用空结构体来标识这个函数或方法。下面是一个简单的示例:
func doSomething() struct{} {
fmt.Println("Doing something")
return struct{}{}
}
func main() {
doSomething()
}
在 main 函数中,我们调用 doSomething 函数。由于它没有返回任何值,所以我们不需要将其结果存储在变量中。
需要注意的是,在这个示例中,我们将返回值的类型显式指定为 struct{}。这是因为如果不指定返回值的类型,那么 Go 编译器会将它默认解析为 interface{} 类型。在这种情况下,每次调用 doSomething 函数都会分配一个新的空接口对象,这可能会带来性能问题。
4、空结构体作为通道元素
空结构体还可以用作通道的元素类型。在 Go 中,通道是一种用于在协程之间进行通信和同步的机制。使用通道时,我们需要指定通道中元素的类型。
如果我们不需要在通道中传输任何值,那么可以使用空结构体作为元素类型。下面是一个简单的示例:
func main() {
c := make(chan struct{})
go func() {
fmt.Println("Goroutine is running")
c <- struct{}{}
}()
<-c
fmt.Println("Goroutine is done")
}
在这个示例中,我们创建了一个名为 c 的通道,并将其元素类型指定为 struct{}。然后,我们在一个新的协程中运行一些代码,并在协程中向通道中发送一个空结构体。在 main 函数中,我们从通道中接收一个元素,这里实际上是在等待协程的结束
。一旦我们接收到了一个元素,我们就会打印出 "Goroutine is done"。
需要注意的是,在这个示例中,我们并没有向通道中发送任何有用的数据。相反,我们只是使用通道来同步协程之间的执行。这种方法对于实现复杂的并发模型非常有用,因为它可以避免使用显式的互斥量或信号量来实现同步和通信。
5、空结构体作为 map 的占位符
在 Go 中,map 是一种用于存储键值对的数据结构。如果我们只需要一个键集合,而不需要存储任何值,那么可以使用空结构体作为 map 的值类型。下面是一个简单的示例:
func main() {
m := make(map[string]struct{})
m["key1"] = struct{}{}
m["key2"] = struct{}{}
m["key3"] = struct{}{}
fmt.Println(len(m)) // 输出: 3
}
在这个示例中,我们创建了一个名为 m 的 map,并将其值类型指定为 struct{}。然后,我们向 map 中添加了三个键,它们的值都是空结构体。最后,我们打印了 map 的长度,结果为 3。
需要注意的是,在这个示例中,我们并没有使用空结构体的任何其他特性。我们只是使用它作为 map 的值类型,因为我们不需要在 map 中存储任何值。
6、空结构体作为方法接收器
在 Go 中,方法是一种将函数与特定类型相关联的机制。如果我们不需要访问方法中的任何接收器字段,那么可以使用空结构体作为接收器类型。下面是一个简单的示例:
type MyStruct struct{}
func (m MyStruct) DoSomething() {
fmt.Println("Method is called")
}
func main() {
s := MyStruct{}
s.DoSomething()
}
在 main 函数中,我们创建了一个 MyStruct 实例 s,然后调用了它的 DoSomething 方法。由于我们不需要在方法中访问接收器的任何字段,所以我们可以使用空结构体作为接收器类型。
需要注意的是,即使我们在方法中使用空结构体作为接收器类型,我们仍然可以将其他参数传递给该方法。例如,我们可以像下面这样修改 DoSomething 方法:
func (m MyStruct) DoSomething(x int, y string) {
fmt.Println("Method is called with", x, y)
}
在这个示例中,我们向 DoSomething 方法添加了两个参数。然而,我们仍然可以使用空结构体作为接收器类型。
7、空结构体作为接口实现
在 Go 中,接口是一种定义对象行为的机制。如果我们不需要实现接口的任何方法,那么可以使用空结构体作为实现。下面是一个简单的示例:
type MyInterface interface {
DoSomething()
}
type MyStruct struct{}
func (m MyStruct) DoSomething() {
fmt.Println("Method is called")
}
func main() {
s := MyStruct{}
var i MyInterface = s
i.DoSomething()
}
我们还定义了一个名为 MyStruct 的结构体,并为其实现了 DoSomething 方法。
在 main 函数中,我们创建了一个 MyStruct 实例 s,然后将其分配给 MyInterface 类型的变量i。由于 MyStruct 实现了 DoSomething 方法,所以我们可以调用 i.DoSomething 方法,并打印出一条消息。
需要注意的是,在这个示例中,我们并没有为接口实现添加任何特殊。我们只是使用空结构体作为实现,因为我们不需要实现接口的任何方法。
8、 空结构体作为信号量
在 Go 中,我们可以使用空结构体作为信号量,以控制并发访问。下面是一个简单的示例
package ch1
import (
"fmt"
"io/ioutil"
"net/http"
"sync"
"testing"
)
func worker(urls []string, wg *sync.WaitGroup, limitCh chan struct{}) {
defer wg.Done()
wg1 := sync.WaitGroup{}
for _, url := range urls {
limitCh <- struct{}{} // 占用一个通道位置,限制并发数量
wg1.Add(1)
go func(url string) {
defer wg1.Done()
defer func() { <-limitCh }() // 完成任务后释放通道位置
res, err := http.Get(url)
if err != nil {
fmt.Printf("error fetching %s:%s", url, err)
return
}
//处理响应数据
defer res.Body.Close()
bytes, err := ioutil.ReadAll(res.Body)
if err != nil {
fmt.Println(err)
return
} else {
fmt.Println(string(bytes))
}
}(url)
}
wg1.Wait()
}
func TestChan(t *testing.T) {
urls := []string{
"http://127.0.0.1:8080/page1",
"http://127.0.0.1:8080/page2",
"http://127.0.0.1:8080/page3",
"http://127.0.0.1:8080/page4",
"http://127.0.0.1:8080/page5",
}
maxWorks := 2 // 最大并发数
limitCh := make(chan struct{}, maxWorks)
var wg sync.WaitGroup
wg.Add(maxWorks)
fmt.Println(urls[:3])
fmt.Println(urls[3:])
go worker(urls[:3], &wg, limitCh)
go worker(urls[3:], &wg, limitCh)
// time.Sleep(2 * time.Second)
wg.Wait()
}
在上述代码中,我们创建了两个工作协程(worker),每个协程负责处理一部分URL列表。limitCh 通道的容量被设置为 maxWorkers,这样可以确保同时进行的请求不超过指定的并发数。
在每个协程的任务循环中,我们首先通过 limitCh <- struct{}{} 获取一个通道位置,限制了并发请求数量。然后,通过匿名函数启动一个新的goroutine来执行HTTP请求。在请求完成后,通过 defer func() { <-limitCh }() 释放占用的通道位置。
使用这种方式,我们可以控制同时进行的请求数量,以避免对服务器施加过大的负载。
通常使用空结构体 {} 是为了节省内存,因为空结构体不占用任何空间。它只是作为一个信号来表示某个事件的发生或者触发一些操作。
需要注意的是,在这个示例中,我们使用空结构体作为信号量,以控制并发访问。由于空结构体不占用任何内存空间,所以它非常适合作为信号量。
限流--空结构体和管道#
package main
import (
"fmt"
"sync/atomic"
"time"
)
var (
concurrent int32
concurrent_limit = make(chan struct{}, 10)
)
func readDB() string {
// 计数器进来加一 完成后减一 计数器支持并发
atomic.AddInt32(&concurrent, 1)
fmt.Printf("readDB()调用并发 %d\n", atomic.LoadInt32(&concurrent))
time.Sleep(200 * time.Millisecond)
atomic.AddInt32(&concurrent, -1)
return "ok"
}
func handler3() {
// 10的时候会阻塞,这样readDB()并发度不会超过10
concurrent_limit <- struct{}{}
readDB()
<-concurrent_limit
return
}
func main() {
for i := 0; i < 100; i++ {
go handler3()
}
time.Sleep(3 * time.Second)
}
atomic原子操作:
Golang的atomic包的原子操作是通过CPU指令实现的。在大多数CPU架构中,原子操作的实现都是基于32位或64位的寄存器。Golang的atomic包的原子操作函数会将变量的地址转换为指针型的变量,并使用CPU指令对这个指针型的变量进行操作。
例如,当我们调用AddInt32函数时,Golang会将变量的地址转换为int32类型的指针,并使用CPU提供的原子指令对这个指针型的变量进行加法操作。这样,就可以保证对共享变量的操作是原子性的。
需要注意的是,不同的CPU架构可能会提供不同的原子指令。因此,在使用atomic包的原子操作时,需要根据具体的CPU架构来选择合适的原子操作函数。
Golang的atomic包提供了一组原子操作函数,包括Add、CompareAndSwap、Load、Store、Swap等函数。这些函数的具体作用如下:
-
Add函数:用于对一个整数型的变量进行加法操作,并返回新的值。
-
CompareAndSwap函数:用于比较并交换一个指针型的变量的值。如果变量的值等于旧值,就将变量的值设置为新值,并返回true;否则,不修改变量的值,并返回false。
-
Load函数:用于获取一个指针型的变量的值。
-
Store函数:用于设置一个指针型的变量的值。
-
Swap函数:用于交换一个指针型的变量的值,并返回旧值。
接口超时控制#
package main
import (
"net/http"
"time"
)
func readDB() string {
time.Sleep(200 * time.Millisecond)
return "ok"
}
func home(w http.ResponseWriter, req *http.Request) {
var resp string
done := make(chan struct{}, 1)
go func() {
resp = readDB()
done <- struct{}{}
}()
// 谁先解除阻塞
select {
case <-done:
case <-time.After(100 * time.Millisecond):
resp = "timeout"
}
w.Write([]byte(resp))
}
func main() {
http.HandleFunc("/", home)
http.ListenAndServe("127.0.0.1:5678", nil)
}
context#
https://www.bilibili.com/read/cv24661572
切片#
make([]T, size, cap)
切片的本质就是对底层数组的封装,它包含了三个信息:底层数组的指针、切片的长度(len)和切片的容量(cap)。
要检查切片是否为空,请始终使用len(s) == 0
来判断。
切片唯一合法的比较操作是和nil
比较。 一个nil
值的切片并没有底层数组,一个nil
值的切片的长度和容量都是0
切片的赋值拷贝:
func main() {
arr := make([]int,3,5) //[0 0 0]
arr[0],arr[1],arr[2] = 2,7,9 // 上面已声明 可以直接赋值
brr := arr //将arr直接赋值给brr,arr和brr共用一个底层数组
brr[0] = 100
fmt.Println(arr) //[100 7 9]
fmt.Println(brr) //[100 7 9]
}
func main() {
arr := make([]int,3,5)
arr[0],arr[1],arr[2] = 2,7,9 // 注意: arr[3] 是不能赋值的
brr := append(arr,8) // 构造一个新切片 brr
fmt.Printf("%d %d\n",len(arr),len(brr)) // 3 4
fmt.Printf("%d %d\n",cap(arr),cap(brr)) // 5 5
fmt.Printf("%v %v\n",arr,brr) // [2 7 9] [2 7 9 8]
brr = append(brr,8)
arr[0] = 4
fmt.Println(brr[0]) // 4
brr = append(brr, 8) // 注意:此时 arr 和 brr 指向的内存地址已经不是同一个了(扩容引起的)
arr[1] = 5
fmt.Println(brr[1]) // 7
fmt.Println(len(brr),cap(brr)) // 6 10
fmt.Println(len(arr),cap(arr)) // 5 5
}
由于切片是引用类型,所以a和b其实都指向了同一块内存地址。修改b的同时a的值也会发生变化。
append()方法为切片添加元素。
每个切片会指向一个底层数组,这个数组的容量够用就添加新增元素。当底层数组不能容纳新增的元素时,切片就会自动按照一定的策略进行“扩容”,此时该切片指向的底层数组就会更换。“扩容”操作往往发生在append()
函数调用时,所以我们通常都需要用原变量接收append函数的返回值。
copy()
函数可以迅速地将一个切片的数据复制到另外一个切片空间中copy(destSlice, srcSlice []T)
srcSlice: 数据来源切片 destSlice: 目标切片
要从切片a中删除索引为index
的元素,操作方法是a = append(a[:index], a[index+1:]...)
字符串#
字符串的本质是 【不可修改的byte数组】 可使用for range遍历
defer#
func main() {
c := 8
// 注意:defer后直接跟的是go语句,go语句中涉及的变量,注册的时候就已经
// 把这个变量值给计算好了
// 如果跟的是匿名函数,这个函数体里涉及的变量,它是在defer执行的时候才去计算的
defer fmt.Println("111",c) // 8
fmt.Println(c)
defer fmt.Println("222",c) //8
defer func() {
fmt.Println("333",c) // 100
}()
c = 100
}
接口#
接口是一组行为规范的集合。
时间相关#
package main
import (
"fmt"
"time"
)
const (
DATE = "2006-01-02"
TIME = "2006-01-02 15:04:05"
)
func main() {
t := time.Now() // time.Time
fmt.Println(t.Unix()) // 时间戳
time.Sleep(500*time.Millisecond)
// 时间相减
t1 := time.Now()
// Time - Time = Duration
diff := t1.Sub(t)
fmt.Println(diff.Milliseconds())
// 更常用的 相减
fmt.Println(time.Since(t).Milliseconds())
// time.Duration 表示时间段的持续时间
// 使用time.Duration 来进行时间间隔的计算和处理
// Time + Duration = Time
d := time.Duration(2*time.Second)
t2 := t.Add(d)
fmt.Println(t2.Unix())
// 时间格式化
fmt.Println(t.Format(DATE))
s := t.Format(TIME)
fmt.Println(s)
// 日期 转换为 Time 格式
t3,_ := time.Parse(TIME,s)
fmt.Println(t3.Unix())
// 指定时区
loc,_ := time.LoadLocation("Asia/Shanghai")
t4,_ := time.ParseInLocation(TIME,s,loc)
fmt.Println(t4.Unix())
}
文件相关#
package main
import (
"bufio"
"fmt"
"io"
"os"
)
func main() {
// 只能读
file,err := os.Open("a.txt")
// 根据第二个参数,可以读写和创建
// file, err := os.Openfile("a.txt",os.O_RDWR|os.O_CREATE,755)
// file.Write() file.Read()
if err != nil {
fmt.Println("打开文件失败",err)
return
}
defer file.Close()
reader := bufio.NewReader(file)
for {
line,err := reader.ReadString('\n')
if err != nil{
if err == io.EOF{ // End Of Line
fmt.Println(line)
break;
}else{
fmt.Println("读文件发生错误",err)
}
}else{
// ReadString 已经将换行加入到line中
fmt.Print(line)
}
}
}
单元测试和基准测试#
// 单元测试 文件 json_test.go
package main
import (
"encoding/json"
"fmt"
"testing"
)
type Student struct {
Name string
Age int
Gender bool
}
type Class struct {
Id string
Students []Student
}
var (
s = Student{"张三",18,true}
c = Class{
Id:"1(2)班",
Students: []Student{s,s},
}
)
// 单元测试文件必须以 下换线 _test.go 结尾
// 运行 go test json_test.go -v // -v 使 print函数正常输出
// 多个测试函数时 只需要运行一个 加 -run=Json 参数 -count=1 去除缓存
// 单元测试 函数的参数必须是 *testing.T
// 单元测试目的是证明一段函数它的逻辑是正确的
func TestJson(t *testing.T){
bytes,err := json.Marshal(c)
if err != nil {
// 整个函数会直接退出 后面的代码不再执行
t.Fail()
}
var c2 Class
err = json.Unmarshal(bytes,&c2)
if err != nil {
t.Fail()
}
fmt.Println("json没毛病")
}
基准测试实际上是性能测试,要把一个函数反复的调很多次,来看看他的速度怎么样以及在CPU在内存的开销,
*文件名需要以_test.go结尾,函数需要以Benchmark开头,参数是 b testing.B
package ch1
import (
"encoding/json"
"testing"
)
type Student struct {
Name string
Age int
Gender bool
}
type Class struct {
Id string
Students []Student
}
var (
s = Student{"张三",18,true}
c = Class{
Id:"1(2)班",
Students: []Student{s,s},
}
)
// 运行 go test json_test.go -bench=Json
// 加上与内存相关的 -benchmem
func BenchmarkJson(b *testing.B){
// b.N不是一个常量,它会根据你实际的运行耗时来动态的选择b.N
// N是一个很大的数
for i := 0; i < b.N; i++ {
bytes,_ := json.Marshal(c)
var c2 Class
json.Unmarshal(bytes,&c2)
}
}
依赖管理#
// 开始一个项目 新建mod module名称可以随便取
go mod init moduleName
// 所有的依赖都会依赖于模块的名称 moduleName
// 导入包 首先是 go.mod里面这个module 名称 moduleName,下一级就是目录名称
import (
"fmt"
"moduleName/util" // 目录名称
)
func main(){
fmt.Println(util.Name) // 注意:这个util是 文件中 package util 的util
}
// 注意:加载第三方库的两种方法:
// 1、go get github.com/bytedance/sonic
// 2、 go mod tidy
MPG调度模型#
CSP并发模型 即:通信顺序进程, 它的核心观点是:不要以共享内存的方式来通信,而是要通过通信来共享内存。
go并发的MPG模型:
- M 代表着一个内核线程,也可以称为一个工作线程。goroutine就是跑在M之上的
- P 代表着(Processor)处理器 它的主要用途就是用来执行goroutine的,一个P代表执行一个Go代码片段的基础(可以理解为上下文环境),
所以它也维护了一个可运行的goroutine队列,和自由的goroutine队列,里面存储了所有需要它来执行的goroutine
。 - G 代表着goroutine 实际的数据结构(就是
你封装的那个方法
),并维护者goroutine 需要的栈、程序计数器以及它所在的M等信息。 - Seched 代表着一个调度器 它维护有存储空闲的M队列和空闲的P队列,可运行的G队列,自由的G队列以及调度器的一些状态信息等。
对于一个运行的GO程序,在调度器内部存在着:
-
全局M,P,G列表,分别用于记录当前所有的M,P,G信息;
-
空闲M,P,G列表,分别用于记录当前空闲可调度的M,P,G信息,以减少M,P,G的创建从而提升性能;
-
可运行G列表,存放等待调度器分配给P的G。
panic
package main
import (
"fmt"
"time"
)
func f1() {
defer func() {
err := recover()
if err != nil {
fmt.Printf("发生panic %s\n", err)
}
}()
a, b := 3, 0
fmt.Println(a,b)
_ = a/b // 发生painc
fmt.Println("f1 finish")
}
func main() {
go f1()
time.Sleep(1*time.Second)
fmt.Println("main fiinsh")
}
并发安全:
package main
import (
"fmt"
"sync"
// "sync/atomic"
)
var (
n int32
lock = sync.Mutex{}
)
// 并发安全性 多个协程修改一个全局变量 最终的结果是不可预知的
func f1() {
for i := 0; i < 100000; i++ {
lock.Lock()
// 确保只有一个协程进入,通过锁机制
n++
//atomic.AddInt32(&n,1) //也可以使用这个方法 原子性 原子操作
lock.Unlock()
}
fmt.Printf("n=%d\n",n)
}
func main() {
wg := sync.WaitGroup{}
wg.Add(2)
go func(){
defer wg.Done()
f1()
}()
go func(){
defer wg.Done()
f1()
}()
wg.Wait()
fmt.Printf("main n=%d\n",n) // 注意:n正常应该等于200000 实际结果不是 并发不安全应
}
管道#
先进先出
func main() {
ch := make(chan int,0)
fmt.Println(time.Now().Unix())
go func(){
// 管道为0的时候发生阻塞,先执行下面协程的读取管道内容后,再执行下面的打印
ch <- 4
fmt.Println(time.Now().Unix(),"写入4成功")
}()
time.Sleep(2*time.Second)
go func(){
a := <-ch
fmt.Println("%d 读出 %d\n",time.Now().Unix(),a)
}()
time.Sleep(1*time.Second)
}
package main
import (
"fmt"
"sync"
)
func main() {
ch := make(chan int, 100)
wg := sync.WaitGroup{}
wg.Add(2)
// 2个生产者,网channel终写入元素
go func() {
defer wg.Done()
for i := 0; i < 10; i++ {
ch <- i
}
}()
go func() {
defer wg.Done()
for i := 0; i < 10; i++ {
ch <- i
}
}()
// 1个消费者
// wg2 := sync.WaitGroup{}
// wg2.Add(1)
mc := make(chan struct{},0)
go func() {
sum := 0
for {
a, ok := <-ch
if !ok {
// channel被关闭,且channel为空,此时ok才为false
break
} else {
sum += a
}
}
fmt.Printf("sum=%d\n",sum)
// wg2.Done()
mc <- struct{}{} // 执行完成后 帮main 解除下面的阻塞
}()
wg.Wait()
close(ch) //关闭管道后 只能读取管道 不能往里写
// wg2.Wait()
// 这里的管到 只是充当一个 协程之间同步的功能
<-mc
}
死锁#
import (
"fmt"
"sync"
)
func main() {
ch := make(chan int, 0)
wg := sync.WaitGroup{}
wg.Add(1)
go func(){
defer wg.Done()
// 阻塞
ch <- 1
fmt.Println("over")
}()
wg.Wait()
}
gorm#
CREATE TABLE `user` (
`id` int(10) unsigned NOT NULL AUTO_INCREMENT,
`uid` int(10) unsigned NOT NULL DEFAULT '0' COMMENT '用户id',
`keywords` text NOT NULL COMMENT '索引词',
`degree` char(2) DEFAULT NULL COMMENT '学历',
`gender` char(1) DEFAULT NULL COMMENT '性别',
`city` char(2) NOT NULL COMMENT '城市',
PRIMARY KEY (`id`),
UNIQUE KEY `uid` (`uid`)
) ENGINE=MyISAM AUTO_INCREMENT=3 DEFAULT CHARSET=utf8mb4;
package main
import (
"fmt"
"os"
"gorm.io/driver/mysql"
"gorm.io/gorm"
)
// 表中有多个字段,程序中不需要其他的字段 可以不写
type User struct{ // User 默认表users
// Id int `gorm:"column:aid,primaryKey"` // 表中主键为aid的时候
Id int // 主键
Keyword string `gorm:"column:keywords"` // 指定和数据库的一致
City string
}
func (User) TableName() string {
return "user"
}
func read(db *gorm.DB,city string) *User{
var users []User
// 错误处理
err := db.Select("id,city").Where("city=?",city).Find(&users).Error
if err != nil{
fmt.Println(err)
}
if len(users) > 0 {
return &users[0]
}else{
return nil
}
}
func main() {
dsn := "root:root@tcp(127.0.0.1:3306)/test?charset=utf8mb4&parseTime=True&loc=Local"
// db,err := gorm.Open(mysql.Open(dsn), &gorm.Config{})
db,err := gorm.Open(mysql.Open(dsn), nil)
if err != nil {
fmt.Println(err)
os.Exit(1)
}
user := read(db,"北京")
if user != nil{
fmt.Printf("%+v\n",user)
}
user1 := User{
Id:2,
Keyword: "golang",
City: "天津",
}
db.Create(user1)
}
go-redis#
package main
import (
"fmt"
"os"
"time"
"github.com/go-redis/redis"
)
// string
func stringkey(client *redis.Client) {
key := "name"
value := "caibaotimes"
// 0 永不过期
err := client.Set(key, value, 1*time.Second).Err()
// 设置过期时间
client.Expire(key, 3*time.Second)
checkError(err)
v2, err := client.Get(key).Result()
checkError(err)
fmt.Println(v2)
client.Del(key)
}
// list
func list(client *redis.Client) {
key := "list"
values := []interface{}{1, 2, "中国"}
err := client.RPush(key, values...).Err()
checkError(err)
v2, err := client.LRange(key, 0, -1).Result()
checkError(err)
fmt.Println(v2)
}
func hashTable(client *redis.Client) {
m1 := map[string]interface{}{"Name": "张三", "Age": 18, "Height": 173.5}
err := client.HMSet("学生1", m1).Err()
checkError(err)
name, err := client.HMGet("学生1", "Name", "Age").Result()
checkError(err)
fmt.Println(name)
maps := client.HGetAll("学生1").Val()
for k, v := range maps {
fmt.Println(k, v)
}
err = client.HSet("学生2", "Name", "李四").Err()
checkError(err)
name1, err := client.HGet("学生2", "Name").Result()
checkError(err)
fmt.Println(name1)
}
func main() {
client := redis.NewClient(&redis.Options{
Addr: "127.0.0.1:6379",
Password: "",
DB: 0,
})
stringkey(client)
list(client)
hashTable(client)
}
func checkError(err error) {
if err != nil {
if err == redis.Nil {
fmt.Println("key 不存在")
} else {
fmt.Println(err)
os.Exit(1)
}
}
}
发起http请求:#
package ch1
import (
"fmt"
"io/ioutil"
"net/http"
"net/url"
"strings"
"testing"
)
func TestGetPost(t *testing.T){
// url.Values map[string][]string
resp,err := http.PostForm("http://127.0.0.1/get_post",url.Values{"student_id":[]string{"学生1"}})
if err != nil{
fmt.Println(err)
t.Fail()
}else{
defer resp.Body.Close()
bytes,err := ioutil.ReadAll(resp.Body)
if err != nil{
fmt.Println(err)
t.Fail()
}else{
fmt.Println(string(bytes))
}
}
}
func TesGetJson(t *testing.T){
client := http.Client{}
reader := strings.NewReader(`{"student_id":"学生1"}`) // 注意:加反引号是为了json里面的引号不转义
request,err := http.NewRequest("POST","http://127.0.0.1:12345/get_json",reader)
if err != nil{
fmt.Println(err)
return
}
request.Header.Add("Content-Type","application/json")
resp,err := client.Do(request)
if err != nil{
fmt.Println(err)
t.Fail()
}else{
defer resp.Body.Close()
bytes,err := ioutil.ReadAll(resp.Body)
if err != nil{
fmt.Println(err)
t.Fail()
}else{
fmt.Println(bytes)
}
}
}
pflag应用构建工具#
https://github.com/spf13/pflag
flag的作用是用来解析命令行的参数。
Pflag 包 FlagSet 定义:
FlagSet 是一些预先定义好的 Flag 的集合,几乎所有的 Pflag 操作,都需要借助 FlagSet 提供的方法来完成。在实际开发中,我们可以使用两种方法来获取并使用 FlagSet:
-
方法一:调用NewFlagSet创建一个FlagSet
-
方法二:使用pflag包定义的全局FlagSet:CommandLine。实际上CommandLine也是由NewFlagSet函数创建的。
// 自定义FlagSet var version bool flagSet := pflag.NewFlagSet("test",pflag.ContinueOnError) flagSet.BoolVar(&version,"version",true,"Print version information and quit.") flagSet.Parse(os.Args[1:]) fmt.Println(version) //使用全局FlagSet var version bool pflag.BoolVarP(&version,"version","v",true,"Print version information and quit.")
在一些不需要定义子命令的命令行工具中,我们可以直接使用全局的 FlagSet,更加简单方便。
flag.Parse():会把用户传递的命令行参数 解析为对应变量的值(一个是解析参数,一个是解析参数的值)
按flag值存储位置分为两种:保存在指针或绑定到变量。
flag值保存在指针:
// 1、支持长选项,默认值和使用说明文本
var name = pflag.String("name","test","Input your name")
func main(){
pflag.Parse()
fmt.Println("name=",*name)
}
// 2、支持长选项,短选项,默认值和使用说明文本
var name = pflag.StringP("name","n","test","Input your name")
func main(){
pflag.Parse()
fmt.Println("name=",*name)
}
flag值绑定到变量:
// 1、支持长选项,默认值和使用文本
var name string
func main(){
pflag.StringVar(&name,"name","test","Input your name")
pflag.Parse()
fmt.Println("name=",name)
}
//2、支持长选项,短选项,默认值和使用说明文本
var name string
func main(){
pflag.StringVarP(&name,"name","n","test","Input your name")
pflag.Parse()
fmt.Println("name=",name)
}
总结:
- 函数名带P的说明支持短选项
- 函数名带Var的说明是将 flag 绑定到变量,否则是存储在指针中
获取参数的值:
可以使用Get
注意:要获取的flag必须存在且类型必须和
注意:Get
获取非选项的参数:
pflag.NArg():返回非flag的参数个数
pflag.Args():返回所有非flag的参数
pflag.Arg(i int):返回第i个非flag参数
package main
import ( "fmt" "github.com/spf13/pflag")
var (
flagvar = pflag.Int("flagname", 1234, "help message for flagname")
)
func main() {
pflag.Parse()
fmt.Printf("argument number is: %v\n", pflag.NArg())
fmt.Printf("argument list is: %v\n", pflag.Args())
fmt.Printf("the first argument is: %v\n", pflag.Arg(0))
}
执行结果:
go run main.go --flagname 12345 345 567
argument number is: 2
argument list is: [345 567]
the first argument is: 345
当创建一个flag后,可以给这个flag设置 pflag.NoOptDefVal
如果一个flag具有NoOptDefVal,并且改flag在命令行上没有传递这个flag的值,那么这个flag的值被设置为
NoOptDefVal指定的值。
var port = pflag.IntP("port","p",6379,"redis port")
func main(){
pflag.Lookup("port").NoOptDefVal = "8080"
pflag.Parse()
fmt.Printf("redis port=%v\n",*port)
}
go run main.go //redis port=6379
go run main.go --port 8888 //redis port=8888
go run main.go --port //redis port=8080
注意:指定了NoOptDefVal后,在命令行传递flag时,不能再用如
--flag xxx
,要用--flag=xxx
,因为
--flag=xxx
会被认为是没有默认值,从而使用了NoOptDefVal的值,xxx则会被认为是 arg
命令行格式:
pflag包的 -
与 --
是不同的, -
表示短选项, --
表示长选项(标准包flag包-
和 --
作用是一样的)。
--flag //支持布尔类型flag,或者设置了NoOptDefVal的 flag
--flag x // 只支持没有设置NoOptDefVal的flag
--flag=x
nil#
1、nil不是golang的关键字,可以进行赋值操作。但是不建议
2、nil标识符是没有类型的,所以==
对于nil来说是一种未定义的操作,不可以进行比较。
3、同类型的nil比较重,指针类型nil、channel类型的nil、interface类型可以相互比较,而func类型、map类型、slice类型只能与nil标识符比较,两个类型相互比较是不合法的。
// 指针类型的nil比较
fmt.Println((*int64)(nil) == (*int64)(nil)) // true
// chan类型的nil比较
fmt.Println((chan int)(nil) == (chan int)(nil)) // true
// 函数类型的nil比较
fmt.Println((func())(nil) == nil) // true
// fmt.Println((func())(nil) == (func())(nil)) // func() 只能与nil进行比较
// 空接口类型的nil
fmt.Println((interface{})(nil) == (interface{})(nil)) // true
// map类型的nil比较
// fmt.Println((map[string]int)(nil) == (map[string]int)(nil)) // map 只能与nil进行比较
// slice类型的nil比较
// fmt.Println(([]*int64)(nil) == ([]*int64)(nil)) // slice 只能与nil进行比较
4、不同类型的nil比较:指针类型和channel类型与接口类型可以比较,其他类型的之间是不可以相互比较的。
5、interface和nil比较时,只有interface底层的数据结构中,必须类型和值同时都为nil的情况下,interface的nil判断才会为true
6、一个nil的map可以读数据,不可以写数据,所以使用的时候要make初始化。
7、一个nil的chan,读写都会阻塞,关闭会panic
8、一个nil的slice,不可以进行索引,否则会引发panic,其他操作是可以的。
9、普通的struct(非指针类型)的对象不能赋值为nil,也不能和nil进行判等(==),不能判断*s == nil
(编译错误),也不能写:var s Student = nil
type Student struct{}
s := new(Student) //使用new创建一个 *Student 类型对象
fmt.Println("s == nil", s == nil) // false
// fmt.Println(*s == nil) //编译错误 cannot convert nil to type Student
fmt.Printf("%T\n",s) // *test.Student
fmt.Printf("%T\n",*s) // test.Student<pre name="code" class="plain">
但是struct的指针对象可以赋值为nil或与nil进行判等。不过即使 *Student
类型的 s3 == nil ,依然输出s3的类型:*student
// var s3 Student = nil // 编译错误:cannot use nil as type Student in assignment
var s3 *Student = nil
fmt.Println("s3 == nil",s3 == nil) //true
fmt.Printf("%T\n",s3) // *test.Student
实战#
goland快捷键:
向上向下移动一行:Alt+up 或alt+down
alt+鼠标左键 选中多行同时编辑
ctrl+shift+L 选中编辑代码中相同的内容
在项目中查找某个关键字:ctrl+shift+f,快捷键无效的原因:被其他应用占用快捷键。比如,最常见的是搜狗输入法:ctrl+shift+f 是简体繁体切换快捷键,输入法右键 -> 更多设置 ->按键 ->系统功能快捷键, 把它关闭掉。
GO语言 json格式map go json unmarshal
https://blog.51cto.com/u_16099356/7094654
切片:
tmp := [][2]string{{"Content-Type","application/json"}}
使用make内置方法进行初始化,然后使用append在原切片的末尾添加元素
make([][]int,0)
解决go gin框架 binding:"required"`无法接收零值的问题
https://www.cnblogs.com/cheyunhua/p/17347692.html
Go json 字符串 转 结构体 Marshal与Unmarshal#
Json(Javascript Object Nanotation)是一种数据交换格式,常用于前后端数据传输。任意一端将数据(如:数组、map、结构体)转换成json 字符串,另一端再将该字符串解析成相应的数据结构,如string类型,strcut对象等。
下面是四种json转为结构体
1、普通JSON
package main
import (
"encoding/json"
"fmt"
)
// Actress 女演员
type Actress struct {
Name string
Birthday string
BirthPlace string
Opus []string
}
func main() {
// 普通JSON
// 因为json.UnMarshal() 函数接收的参数是字节切片
// 所以需要把JSON字符串转换成字节切片。
jsonData := []byte(`{
"name":"迪丽热巴",
"birthday":"1992-06-03",
"birthPlace":"新疆乌鲁木齐市",
"opus":[
"《阿娜尔罕》",
"《逆光之恋》",
"《克拉恋人》"
]
}`)
var actress Actress
err := json.Unmarshal(jsonData, &actress)
if err != nil {
fmt.Println("error:", err)
return
}
fmt.Printf("姓名:%s\n", actress.Name)
fmt.Printf("生日:%s\n", actress.Birthday)
fmt.Printf("出生地:%s\n", actress.BirthPlace)
fmt.Println("作品:")
for _, val := range actress.Opus {
fmt.Println("\t", val)
}
}
结果:
姓名:迪丽热巴
生日:1992-06-03
出生地:新疆乌鲁木齐市
作品:
《阿娜尔罕》
《逆光之恋》
《克拉恋人》
2、JSON内嵌普通JSON
package main
import (
"encoding/json"
"fmt"
)
// Opus 作品
type Opus struct {
Date string
Title string
}
// Actress 女演员
type Actress struct {
Name string
Birthday string
BirthPlace string
Opus Opus
}
func main () {
// JSON嵌套普通JSON
jsonData := []byte(`{
"name":"迪丽热巴",
"birthday":"1992-06-03",
"birthPlace":"新疆乌鲁木齐市",
"opus": {
"Date":"2013",
"Title":"《阿娜尔罕》"
}
}`)
var actress Actress
err := json.Unmarshal(jsonData, &actress)
if err != nil {
fmt.Println("error:", err)
return
}
fmt.Printf("姓名:%s\n", actress.Name)
fmt.Printf("生日:%s\n", actress.Birthday)
fmt.Printf("出生地:%s\n", actress.BirthPlace)
fmt.Println("作品:")
fmt.Printf("\t%s:%s", actress.Opus.Date, actress.Opus.Title)}
结果:
姓名:迪丽热巴
生日:1992-06-03
出生地:新疆乌鲁木齐市
作品:
2013:《阿娜尔罕》
3、JSON内嵌数组JSON
package main
import (
"encoding/json"
"fmt"
)
type Opus struct {
Date string
Title string
}
type Actress struct {
Name string
Birthday string
BirthPlace string
Opus []Opus
}
func main () {
// JSON嵌套数组JSON
jsonData := []byte(`{
"name":"迪丽热巴",
"birthday":"1992-06-03",
"birthPlace":"新疆乌鲁木齐市",
"opus":[
{
"date":"2013",
"title":"《阿娜尔罕》"
},
{
"date":"2014",
"title":"《逆光之恋》"
},
{
"date":"2015",
"title":"《克拉恋人》"
}
]
}`)
var actress Actress
err := json.Unmarshal(jsonData, &actress)
if err != nil {
fmt.Println("error:", err)
return
}
fmt.Printf("姓名:%s\n", actress.Name)
fmt.Printf("生日:%s\n", actress.Birthday)
fmt.Printf("出生地:%s\n", actress.BirthPlace)
fmt.Println("作品:")
for _, val := range actress.Opus {
fmt.Printf("\t%s - %s\n", val.Date, val.Title)
}
}
结果:
姓名:迪丽热巴
生日:1992-06-03
出生地:新疆乌鲁木齐市
作品:
2013 - 《阿娜尔罕》
2014 - 《逆光之恋》
2015 - 《克拉恋人》
4、JSON内嵌具有动态Key的JSON
package main
import (
"encoding/json"
"fmt"
)
// Opus 作品
type Opus struct {
Type string
Title string
}
// Actress 女演员
type Actress struct {
Name string
Birthday string
BirthPlace string
Opus map[string]Opus
}
func main () {
jsonData := []byte(`{
"name":"迪丽热巴",
"birthday":"1992-06-03",
"birthPlace":"新疆乌鲁木齐市",
"opus":{
"2013":{
"Type":"近代革命剧",
"Title":"《阿娜尔罕》"
},
"2014":{
"Type":"奇幻剧",
"Title":"《逆光之恋》"
},
"2015":{
"Type":"爱情剧",
"Title":"《克拉恋人》"
}
}
}`)
var actress Actress
err := json.Unmarshal(jsonData, &actress)
if err != nil {
fmt.Println("error:", err)
return
}
fmt.Printf("姓名:%s\n", actress.Name)
fmt.Printf("生日:%s\n", actress.Birthday)
fmt.Printf("出生地:%s\n", actress.BirthPlace)
fmt.Println("作品:")
for index, value := range actress.Opus {
fmt.Printf("\t日期:%s\n", index)
fmt.Printf("\t\t分类:%s\n", value.Type)
fmt.Printf("\t\t标题:%s\n", value.Title)
}
}
结果:
姓名:迪丽热巴
生日:1992-06-03
出生地:新疆乌鲁木齐市
作品:
日期:2013
分类:近代革命剧
标题:《阿娜尔罕》
日期:2014
分类:奇幻剧
标题:《逆光之恋》
日期:2015
分类:爱情剧
标题:《克拉恋人》
Go 每日一库之 mapstructure#
mapstructure
用于将通用的map[string]interface{}
解码到对应的 Go 结构体中,或者执行相反的操作。很多时候,解析来自多种源头的数据流时,我们一般事先并不知道他们对应的具体类型。只有读取到一些字段之后才能做出判断。这时,我们可以先使用标准的encoding/json
库将数据解码为map[string]interface{}
类型,然后根据标识字段利用mapstructure
库转为相应的 Go 结构体以便使用。
快速使用
本文代码采用 Go Modules。
首先创建目录并初始化:
$ mkdir mapstructure && cd mapstructure
$ go mod init github.com/darjun/go-daily-lib/mapstructure
下载mapstructure
库:
$ go get github.com/mitchellh/mapstructure
使用:
package main
import (
"encoding/json"
"fmt"
"log"
"github.com/mitchellh/mapstructure"
)
type Person struct {
Name string
Age int
Job string
}
type Cat struct {
Name string
Age int
Breed string
}
func main() {
datas := []string{`
{
"type": "person",
"name":"dj",
"age":18,
"job": "programmer"
}
`,
`
{
"type": "cat",
"name": "kitty",
"age": 1,
"breed": "Ragdoll"
}
`,
}
for _, data := range datas {
var m map[string]interface{}
err := json.Unmarshal([]byte(data), &m)
if err != nil {
log.Fatal(err)
}
switch m["type"].(string) {
case "person":
var p Person
mapstructure.Decode(m, &p)
fmt.Println("person", p)
case "cat":
var cat Cat
mapstructure.Decode(m, &cat)
fmt.Println("cat", cat)
}
}
}
运行结果:
$ go run main.go
person {dj 18 programmer}
cat {kitty 1 Ragdoll}
我们定义了两个结构体Person
和Cat
,他们的字段有些许不同。现在,我们约定通信的 JSON 串中有一个type
字段。当type
的值为person
时,该 JSON 串表示的是Person
类型的数据。当type
的值为cat
时,该 JSON 串表示的是Cat
类型的数据。
上面代码中,我们先用json.Unmarshal
将字节流解码为map[string]interface{}
类型。然后读取里面的type
字段。根据type
字段的值,再使用mapstructure.Decode
将该 JSON 串分别解码为Person
和Cat
类型的值,并输出。
实际上,Google Protobuf 通常也使用这种方式。在协议中添加消息 ID 或全限定消息名。接收方收到数据后,先读取协议 ID 或全限定消息名。然后调用 Protobuf 的解码方法将其解码为对应的Message
结构。从这个角度来看,mapstructure
也可以用于网络消息解码,如果你不考虑性能的话 。
字段标签
默认情况下,mapstructure
使用结构体中字段的名称做这个映射,例如我们的结构体有一个Name
字段,mapstructure
解码时会在map[string]interface{}
中查找键名name
。注意,这里的name
是大小写不敏感的!
type Person struct {
Name string
}
当然,我们也可以指定映射的字段名。为了做到这一点,我们需要为字段设置mapstructure
标签。例如下面使用username
代替上例中的name
:
type Person struct {
Name string `mapstructure:"username"`
}
看示例:
type Person struct {
Name string `mapstructure:"username"`
Age int
Job string
}
type Cat struct {
Name string
Age int
Breed string
}
func main() {
datas := []string{`
{
"type": "person",
"username":"dj",
"age":18,
"job": "programmer"
}
`,
`
{
"type": "cat",
"name": "kitty",
"Age": 1,
"breed": "Ragdoll"
}
`,
`
{
"type": "cat",
"Name": "rooooose",
"age": 2,
"breed": "shorthair"
}
`,
}
for _, data := range datas {
var m map[string]interface{}
err := json.Unmarshal([]byte(data), &m)
if err != nil {
log.Fatal(err)
}
switch m["type"].(string) {
case "person":
var p Person
mapstructure.Decode(m, &p)
fmt.Println("person", p)
case "cat":
var cat Cat
mapstructure.Decode(m, &cat)
fmt.Println("cat", cat)
}
}
}
上面代码中,我们使用标签mapstructure:"username"
将Person
的Name
字段映射为username
,在 JSON 串中我们需要设置username
才能正确解析。另外,注意到,我们将第二个 JSON 串中的Age
和第三个 JSON 串中的Name
首字母大写了,但是并没有影响解码结果。mapstructure
处理字段映射是大小写不敏感的。
内嵌结构
结构体可以任意嵌套,嵌套的结构被认为是拥有该结构体名字的另一个字段。例如,下面两种Friend
的定义方式对于mapstructure
是一样的:
type Person struct {
Name string
}
// 方式一
type Friend struct {
Person
}
// 方式二
type Friend struct {
Person Person
}
为了正确解码,Person
结构的数据要在person
键下:
map[string]interface{} {
"person": map[string]interface{}{"name": "dj"},
}
我们也可以设置mapstructure:",squash"
将该结构体的字段提到父结构中:
type Friend struct {
Person `mapstructure:",squash"`
}
这样只需要这样的 JSON 串,无效嵌套person
键:
map[string]interface{}{
"name": "dj",
}
看示例:
type Person struct {
Name string
}
type Friend1 struct {
Person
}
type Friend2 struct {
Person `mapstructure:",squash"`
}
func main() {
datas := []string{`
{
"type": "friend1",
"person": {
"name":"dj"
}
}
`,
`
{
"type": "friend2",
"name": "dj2"
}
`,
}
for _, data := range datas {
var m map[string]interface{}
err := json.Unmarshal([]byte(data), &m)
if err != nil {
log.Fatal(err)
}
switch m["type"].(string) {
case "friend1":
var f1 Friend1
mapstructure.Decode(m, &f1)
fmt.Println("friend1", f1)
case "friend2":
var f2 Friend2
mapstructure.Decode(m, &f2)
fmt.Println("friend2", f2)
}
}
}
注意对比Friend1
和Friend2
使用的 JSON 串的不同。
另外需要注意一点,如果父结构体中有同名的字段,那么mapstructure
会将JSON 中对应的值同时设置到这两个字段中,即这两个字段有相同的值。
未映射的值
如果源数据中有未映射的值(即结构体中无对应的字段),mapstructure
默认会忽略它。
我们可以在结构体中定义一个字段,为其设置mapstructure:",remain"
标签。这样未映射的值就会添加到这个字段中。注意,这个字段的类型只能为map[string]interface{}
或map[interface{}]interface{}
。
type Person struct {
Name string
Age int
Job string
Other map[string]interface{} `mapstructure:",remain"`
}
func main() {
data := `
{
"name": "dj",
"age":18,
"job":"programmer",
"height":"1.8m",
"handsome": true
}
`
var m map[string]interface{}
err := json.Unmarshal([]byte(data), &m)
if err != nil {
log.Fatal(err)
}
var p Person
mapstructure.Decode(m, &p)
fmt.Println("other", p.Other)
}
上面代码中,我们为结构体定义了一个Other
字段,用于保存未映射的键值。输出结果:
other map[handsome:true height:1.8m]
逆向转换
前面我们都是将map[string]interface{}
解码到 Go 结构体中。mapstructure
当然也可以将 Go 结构体反向解码为map[string]interface{}
。在反向解码时,我们可以为某些字段设置mapstructure:",omitempty"
。这样当这些字段为默认值时,就不会出现在结构的map[string]interface{}
中:
type Person struct {
Name string
Age int
Job string `mapstructure:",omitempty"`
}
func main() {
p := &Person{
Name: "dj",
Age: 18,
}
var m map[string]interface{}
mapstructure.Decode(p, &m)
data, _ := json.Marshal(m)
fmt.Println(string(data))
}
上面代码中,我们为Job
字段设置了mapstructure:",omitempty"
,且对象p
的Job
字段未设置。运行结果:
$ go run main.go
{"Age":18,"Name":"dj"}
泛型#
本文中的泛型特指计算机编程语言中的泛型, 即编程语言中的函数,方法,类定义等与特定的类型参数无关,相关的函数,方法和类的实例化是根据具体的调用参数来进行。泛型是和编译紧密相关的技术,尤其是针对 golang 这种静态类型的语言来说,更是如此。
下面我们来介绍几种基本的泛型语法。
通过内置的 any 接口类型化参数来实现泛型
比如下面的函数:
func Print[T any](s []T) {
for _, v := range s {
fmt.Println(v)
}
}
这里的[T any]即为类型参数,意思是该函数支持任何类型的 slice 。但是在调用该函数的时候,需要显式指定类型参数类型。如果想用该函数打印字符串 slice,则需要显式指定类型参数为 string, 以帮助编译器实行类型推导。
Print([]string{"Hello, ", "World\n"})
如果不显式指定,则编译报错。
Print([]{"Hello, ", "World\n"})
typeparam_basic.go2:18:14: expected type, found '{'
通过扩展的接口类型实现泛型(对参数实现约束和限制)
例如下面的接口定义:
type Addable interface {
type int, int8, int16, int32, uint, uint8, uint16, uint32
}
相关的泛型函数的类型参数指定为 Addable 类型:
func Add[T Addable](a, b T) T {
return a + b
}
则调用方式为:
c := Add(2, 3)
顺利编译通过。如果将调用参数改为不在 Addable 接口中指定的 float 类型,如:
c := Add(2.1, 3.2)
则编译报错:
extended_inf.go2:14:10: float64 does not satisfy Addable (float64 or float64 not found in int, int8, int16, int32, uint, uint8, uint16, uint32)
通过内置的扩展接口类型实现泛型(对泛型参数实现约束和限制)
例如 golang 语言泛型 sample 代码中,有个 Set 的泛型类型, 该类型定义如下:
// A Set is a set of elements of some type.
type Set[Elem comparable] struct {
m map[Elem]struct{}
}
该类型定义中,comparable 也是一个编译器内置的特定的扩展接口类型,该类型必须支持“==“ 方法,也就是任何支持等于比较操作的类型,都是该 Set 所支持的类型。
在这里我们先简单介绍上面几种基本的语法,当然泛型的使用方式远不止这些,具体的语法规则,可以看示例代码和后续 golang 更新的官方 spec,这里不再赘述。
七牛云上传图片和视频#
// 七牛云上传图片和视频
func (m *materialController) UploadFile(c *gin.Context) {
localFile := c.PostForm("filepath")
if localFile == "" {
core.WriteResponse(c, errno.UploadedFileParam, nil)
return
}
file, err := c.FormFile("file")
if err != nil {
core.WriteResponse(c, errno.UploadImgFileNil, nil)
return
}
fileExt := strings.ToLower(path.Ext(file.Filename))
fileName := util.StringMd5(fmt.Sprintf("%s%s", file.Filename, time.Now().String()))
key := fmt.Sprintf("%s%s%s", "aivideo/", fileName, fileExt)
bucket := viper.GetString("bucket")
ak := viper.GetString("accessKey")
sk := viper.GetString("secretKey")
putPolicy := storage.PutPolicy{
Scope: bucket,
}
mac := qbox.NewMac(ak, sk)
upToken := putPolicy.UploadToken(mac)
cfg := storage.Config{}
// 空间对应的机房
cfg.Region = &storage.ZoneHuadong
// 是否使用https域名
cfg.UseHTTPS = true
// 上传是否使用CDN上传加速
cfg.UseCdnDomains = false
// 构建表单上传的对象
formUploader := storage.NewFormUploader(&cfg)
ret := storage.PutRet{}
err = formUploader.PutFile(c, &ret, upToken, key, localFile, nil)
if err != nil {
log.Error(fmt.Sprintf("aivideo上传文件失败,err:%+v", err))
core.WriteResponse(c, errno.UploadedFileErr, nil)
return
}
result := map[string]interface{}{"code": 200, "message": "OK", "result": viper.GetString("uploadAiFileDomain") + ret.Key}
core.WriteResponse(c, nil, result)
}
格式化接口的数据(数据库数据)#
// 返回数据的字段
type ChartMaterialData struct {
ID int `json:"id"`
UserId int `json:"user_id"`
Title string `json:"title"`
Type int `json:"type"`
ImageUrl I `json:"image_url"`
Ancillary string `json:"ancillary"`
Status int `json:"status"`
CreateTime F `json:"create_time"`
}
type F int64
func (f F) MarshalJSON() ([]byte, error) {
return []byte(fmt.Sprintf(`"%s"`, format.FormatTime(int64(f), "2006-01-02 15:04:05", false))), nil
}
type I string
func (i I) MarshalJSON() ([]byte, error) {
return []byte(fmt.Sprintf(`"%s%s"`, viper.GetString("Domain"), string(i))), nil
}
type II string
func (i II) MarshalJSON() ([]byte, error) {
// 判断为空的情况
if i == "" {
return []byte(fmt.Sprintf(`"%s"`, string(i))), nil
}
return []byte(fmt.Sprintf(`"%s%s"`, viper.GetString("Domain"), string(i))), nil
}
// 格式化输出时间字符串
func FormatTime(t int64, layout string, isCurYear bool) string {
if !isCurYear {
return time.Unix(t, 0).Format(layout)
}
now := time.Now()
timeSign := time.Date(now.Year(), 1, 1, 0, 0, 0, 0, now.Location()).Unix()
i := strings.Index(layout, "/")
if i < 0 {
i = strings.Index(layout, "-")
}
if t >= timeSign {
return time.Unix(t, 0).Format(layout[i+1:])
}
return time.Unix(t, 0).Format(layout)
}
Go 里面不能把任意切片转换为 []interface{} ?#
任意类型都可以转换为空接口 interface{}
,为啥[]string
(或者任意别的类型的切片)不能转为空接口切片[]interface
?
是不可以的,其他强类型语言也不可以。
reflect.TypeOf()和reflect.Type(通过反射获取类型信息)#
反射第一定律:反可以将 接口类型变量 转换为 反射类型对象。
reflect 反射包提供了两种方法,reflect.TypeOf() 和 reflect.Value()
reflect.TypeOf()获取变量的反射类型对象(reflect.Type)。
通过反射类型对象获取类型信息
通过反射类型对象可以获取自身的类型信息,使用Name()方法获取类型名称,使用Kind()方法获取类型归属的种类
package main
import (
"fmt"
"reflect"
)
func GetReflectInfo(a interface{}) {
// 获取变量 a 的类型对象,类型对象的类型是 reflect.Type
var typeofA reflect.Type = reflect.TypeOf(a)
fmt.Printf("%T\n", typeofA)
// 打印类型名 和种类
fmt.Println(typeOfA.Name(), typeofA.Kind())
}
func mian() {
GetReflectInfo("666")
}
运行结果:
*reflect.rType
string string // 类型和种类都是string
理解反射的类型(Type)与种类(Kind)
编程中,使用最多的是类型,但在反射中,当需要区分一个大品类的类型时,就会用到种类(Kind)
1、类型(Type)
类型指的是系统原生数据类型,如int、string、bool、float32等类型,以及使用type关键字自定义的类型,
自定义类型需要指定类型名称,例如,type A struct{} 类型名就是A
2、种类(Kind)
Kind()方法会返回一个常量,表示底层数据的类型。
种类指的是对象归属的大品种,在reflect包中有如下定义:
type Kind uint
const (
Invalid Kind = iota // 非法类型
Bool // 布尔型
Int // 有符号整型
Int8 // 有符号8位整型
Int16 // 有符号16位整型
Int32 // 有符号32位整型
Int64 // 有符号64位整型
Uint // 无符号整型
Uint8 // 无符号8位整型
Uint16 // 无符号16位整型
Uint32 // 无符号32位整型
Uint64 // 无符号64位整型
Uintptr // 指针
Float32 // 单精度浮点数
Float64 // 双精度浮点数
Complex64 // 64位复数类型
Complex128 // 128位复数类型
Array // 数组
Chan // 通道
Func // 函数
Interface // 接口
Map // 映射
Ptr // 指针
Slice // 切片
String // 字符串
Struct // 结构体
UnsafePointer // 底层指针
)
Map、Slice、Chan属于引用类型,使用起来类似于指针,但是在种类常量定义中仍然属于独立的种类,不属于Ptr
*type A struct{} 结构体属于 Struct 种类, A 属于 Ptr 种类
反射第二定律:反射可以将 反射类型对象 转换为 接口类型变量
根据一个 reflect.Value 类型的变量,我们可以使用 Interface 方法恢复其接口类型的值。
函数声明如下:
// Interface return v's value as an interface{}.
// 相当于: var i interface{} = (v的底层值)
func (v value) Interface() interface{}
我们可以通过断言,恢复底层的具体值:
func main() {
var x float64 = 12.1
v := reflect.ValueOf(x)
y, ok := v.Interface().(float64)
fmt.Println(ok, y) // true 12.1
}
// 上面代码会打印一个float64类型的值,也就是反射类型变量v所代表的值。
反射第三定律:如果要修改 反射类型对象 其值必须是 可写的
我们通过 CanSet()
方法检查一个 reflect.Value
类型变量的可写性。
func main() {
var x float64 = 12.1
v := reflect.ValueOf(x)
fmt.Println("settability of v:", v.CanSet())
}
// 结果: settability of v: false
可写性 有些类似于 寻址能力,它是反射类型变量的一种属性,赋予该变量修改底层存储数据的能力。
我们可以调用 Value类型的 Elem 方法, Elem方法能够对指针进行 "解引用",然后将结果存储到反射 Value类型对象 v 中:
func main() {
var x float64 = 3.4
p := reflect.ValueOf(&x)
v := p.Elem()
fmt.Println("settability of v:", v.CanSet())
}
// 结果: settability of v: true
// 由于变量 v 代表 x,因此 我们可以使用 v.SetFloat 修改 x 的值:
func main() {
var x float64 = 3.4
p := reflect.ValueOf(&x)
v := p.Elem() // 获取指针 &x指向的变量x 等同于 *(&x)
v.SetFloat(7.1)
fmt.Println(v.Interface())
fmt.Println(x)
}
// 结果
// 7.1
// 7.1
Elem():专门用来获取指针指定的变量,返回仍然还是一个 reflect.Value类型。
注意:只要反射对象要修改它们表示的对象,就必须获取它们表示的对象的地址。
具体类型的切片 转 interface 切片解决办法#
// 具体切片类型 转为空接口切片
package main
import (
"fmt"
"reflect"
)
type a struct {
CreateTime int
Name string
Content string
}
func main () {
arr1 := []a{
a{Name:"no1",CreateTime:1,Content:"hello1"},
a{Name:"no2",CreateTime:2,Content:"hello2"},
a{Name:"no3",CreateTime:3,Content:"hello3"},
}
fmt.Println(arr1)
arr2 := ToInterfaceArr(arr1)
fmt.Printf("%T\n", arr2)
fmt.Println(arr2)
}
func ToInterfaceArr(arr interface{}) []interface{} {
if reflect.TypeOf(arr).Kind() != reflect.Slice {
return nil
}
arrValue := reflect.ValueOf(arr)
retArr := make([]interface{}, arrValue.Len())
for k := 0; k < arrValue.Len(); k++ {
retArr[k] = arrValue.Index(k).Interface()
}
return retArr
}
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· TypeScript + Deepseek 打造卜卦网站:技术与玄学的结合
· Manus的开源复刻OpenManus初探
· AI 智能体引爆开源社区「GitHub 热点速览」
· 三行代码完成国际化适配,妙~啊~
· .NET Core 中如何实现缓存的预热?
2021-01-22 跨域资源共享 CORS 详解
2021-01-22 MySQL字符串的拼接、截取、替换、查找位置。