本章知识点:
数组与切片
map(字典)
struct(结构体)
指针
泛型
数组与切片 在 Go 中,数组 是固定长度的,而切片 是动态的、是对数组的抽象。
数组 不管在Java还是Go似乎都不太常用,比较常用List或者Go中的切片。其语法如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 package mainimport "testing" func Test_Array (t *testing.T) { var arr1 [5 ]int t.Log(arr1) var arr2 [3 ]string t.Log(arr2) arr3 := [3 ]string {"a" , "b" , "c" } t.Log(arr3) arr4 := []string {"a" , "b" , "c" } t.Log(len (arr4)) arr5 := [5 ]int {1 : 100 , 2 : 200 } t.Log(arr5) }
访问数组
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 func Test_ArrayOpera (t *testing.T) { var arr1 [5 ]int arr1[0 ] = 10 arr1[3 ] = 88 t.Log(arr1) t.Log(arr1[3 ]) t.Log(len (arr1)) var arrab [2 ][3 ]string arrab[0 ][0 ] = "a" arrab[0 ][1 ] = "b" arrab[0 ][2 ] = "c" arrab[1 ][0 ] = "d" t.Log(arrab) arrab1 := [2 ][3 ]string { {"a" , "b" , "c" }, {"a1" , "b1" , "c1" }, } t.Log(arrab1) t.Log(arrab1[0 ][1 ]) }
对于数组的便利,下一章会有介绍。
关于数组,还有一个最主要的特性:
Go 中数组是值类型,赋值或传参会复制整个数组,处理大数据时非常低效
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 func Test_ArrayCopy (t *testing.T) { arr := [3 ]int {1 , 2 , 3 } b := arr b[0 ] = 6 t.Log(arr, b) modifyArray(arr) t.Log(arr) } func modifyArray (arr [3]int ) { arr[0 ] = 6 }
数组的长度必须是常量,且是类型的一部分。[3]int 和 [5]int 在编译器看来是完全不同的两种类型,不能互相赋值或比较。
1 2 3 4 5 6 7 8 9 10 11 func Test_ArrayCompare (t *testing.T) { arr1 := [3 ]int {1 , 2 , 3 } arr2 := [3 ]int {1 , 2 , 3 } arr3 := [3 ]int {1 , 2 , 4 } t.Log(arr1 == arr2) t.Log(arr1 == arr3) }
切片(重点) 切片是 Go 最核心的设计之一。它不是动态数组,而是对底层数组的描述符 。
切片三要素:
指针(指向底层数组的起始位置)
长度(len,当前可见的元素个数)
容量(cap,从起始位置到底层数组末尾的元素个数)
它的底层结构体runtime/slice.go:
1 2 3 4 5 type slice struct { array unsafe.Pointer len int cap int }
当你传递切片时,实际复制的是这个 header,底层数组是共享的。
下面是切片的初始化方式
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 func Test_Slice (t *testing.T) { var s1 []int s2 := []int {} s3 := []int {1 , 2 , 3 } t.Log(s1, s2, s3) s4 := make ([]int , 3 , 10 ) s5 := make ([]int , 3 ) t.Log(s4, s5) arr := [4 ]int {1 , 2 , 3 , 4 } s6 := arr[1 :3 ] t.Log(s6) s7 := new ([]int ) t.Log(s7) s8 := s3[1 :3 ] t.Log(s8) }
看样子,切片很像Java中的List,顺序存储、动态扩容。它和数组的区别是:
特性
数组
切片
长度
固定,是类型的一部分
可变
类型
值类型
引用类型(底层是结构体)
传参开销
复制整个数组
复制头信息(24字节)
动态扩展
不支持
支持 append
比较
支持 ==
不支持 ==(只能和 nil 比)
切片表达式
规则:
0 <= low <= high <= max <= cap(底层数组)
省略 low 默认为 0
省略 high 默认为 len
完整形式 [low:high:max] 限制新切片的容量
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 func Test_SliceReg (t *testing.T) { arr := [10 ]int {0 , 1 , 2 , 3 , 4 , 5 , 6 , 7 , 8 , 9 } s1 := arr[2 :5 ] t.Log(s1) s2 := arr[2 :5 :7 ] t.Log(s2) s3 := arr[:5 ] t.Log(s3) s4 := arr[5 :] t.Log(s4) s5 := arr[:] t.Log(s5) }
切片Append
append函数会返回一个新的切片,新的切片可能指向同一个底层数组,也可能指向一个新的底层数组,这取决于原切片的容量和追加元素的数量。
1 2 3 4 5 6 7 8 9 10 func Test_SliceAppend (t *testing.T) { s1 := []int {1 , 2 , 3 } s2 := append (s1, 4 ) s3 := append (s1, 5 , 6 ) s4 := append (s1, []int {7 , 8 , 9 }...) t.Log(s1, s2, s3, s4) }
如果容量足够,就会出现共享底层数组的情况:
1 2 3 4 5 6 func Test_ShareArray (t *testing.T) { a := []int {1 , 2 , 3 , 4 , 5 } b := a[:3 ] b = append (b, 99 ) t.Log(a, b) }
输出如下:
1 2 === RUN Test_ShareArray array_test.go:206: [1 2 3 99 5] [1 2 3 99]
1 2 3 4 5 6 7 8 func Test_ShareArray (t *testing.T) { a := []int {1 , 2 , 3 , 4 , 5 } b := a[:3 :3 ] b = append (b, 99 ) t.Log(a, b) }
此时,输出如下:
1 2 === RUN Test_ShareArray array_test.go:208: [1 2 3 4 5] [1 2 3 99]
还可能出现append以后,原切片可能被覆盖的情况
1 2 3 4 5 6 func Test_OverwriteSlice (t *testing.T) { s := make ([]int , 3 , 5 ) s1 := append (s, 1 ) s2 := append (s, 2 ) t.Log(s, s1, s2) }
输出如下:
1 2 === RUN Test_OverwriteSlice array_test.go:215: [0 0 0] [0 0 0 2] [0 0 0 2]
此时可以看到,s2覆盖了s1的最后一个元素。原因就是s1和s2指向了同一个底层数组。
解决方案:在 append 前对原切片做一次完整拷贝,让每次 append 独占底层数组
1 2 3 4 5 6 7 8 func Test_OverwriteSlice (t *testing.T) { s := make ([]int , 3 , 5 ) s1 := append (slices.Clone(s), 1 ) s2 := append (slices.Clone(s), 2 ) t.Log(s, s1, s2) }
切片扩容
既然涉及到自动扩容,那就要了解下扩容的原理:
当执行 s = append(s, val) 时,Go 的扩容策略在不同版本有所微调(以 Go 1.25 为准):
如果新长度大于容量的 2 倍,直接使用新长度作为新容量。
否则,如果旧容量 < 256,翻倍。
如果旧容量 >= 256,则增加 (old_cap + 3*256) / 4,直到满足要求。
注意 :扩容会触发内存重新分配和数据拷贝,旧底层数组将被 GC 回收。
扩容可能导致底层数组重新分配!此时原切片和新切片不再共享数据。
例如:
1 2 3 4 5 a := []int {1 , 2 , 3 } b := append (a, 4 )
切片的拷贝
copy函数会将源切片中的元素复制到目标切片中,复制的元素数量取决于源切片和目标切片的长度。copy函数返回实际复制的元素数量。
1 2 3 4 5 6 7 func Test_SliceCopy (t *testing.T) { ss := []int {1 , 2 , 3 , 4 , 5 } sd := make ([]int , 3 ) n := copy (sd, ss) t.Log(sd, n) }
特点:
复制个数 = min(len(src), len(dst))
两个切片可以重叠
可用于切片截断:copy(s, s[i:])
常用技巧:
1 2 3 4 5 6 7 8 9 10 11 12 copy (s[i:], s[i+1 :])s = s[:len (s)-1 ] s = append ([]int {x}, s...) small := make ([]int , 10 ) copy (small, data)
切片是引用类型,函数内修改会影响原切片(注意区分修改元素和改变切片本身):
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 func Test_SliceArgs (t *testing.T) { s := []int {1 , 2 , 3 , 4 , 5 } modifySlice(s) t.Log(s) modifySlice1(s) t.Log(s) appendValue(&s, 8 ) t.Log(s) } func modifySlice (s []int ) { s[0 ] = 99 } func modifySlice1 (s []int ) { s = append (s, 6 ) } func appendValue (s *[]int , v int ) { if s == nil { return } *s = append (*s, v) }
多维切片
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 func Test_MultiSlice (t *testing.T) { var s [][]int rows, cols := 3 , 4 s = make ([][]int , rows, cols) for i := range s { s[i] = make ([]int , cols) } s[1 ][2 ] = 99 t.Log(s) s2 := [][]int { {1 }, {1 , 2 }, {1 , 2 , 3 }, } t.Log(s2) }
map(字典) map 是 Go 内置的哈希表类型,用于存储 键值对 。其语法结构为
申明与初始化 nil map
1 2 3 var m1 map [string ]int t.Logf("m1 == nil 的结果是 : %v" , m1 == nil )
nil不能写入,但可以有以下操作:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 func Test_NilMap (t *testing.T) { var m map [string ]int v := m["x" ] t.Logf("nil map get key : %v" , v) _, ok := m["x" ] t.Logf("nil map get status : %v" , ok) delete (m, "x" ) clear(m) for i := range m { t.Log(i) } }
通过make创建map
1 2 3 m := make (map [string ]int ) m1:= make (map [string ]int ,1000 )
注意:这个 1000 是 容量提示 ,不是最大容量限制。map会自动扩容。
通过字面量创建map
1 2 3 4 m := map [string ]int { "a" :1 , "b" :2 }
当然,也可以创建一个空的map(注意:空map和nil map是两码事),空map可以写入数据。
此处附上nil map 与 空map 对比:
nil map
空map map[k]v{}
读取
返回零值
返回零值
写入
panic
正常
删除
panic
正常
len
0
0
map的CRUD 增加/修改
1 2 3 4 5 m := map [string ]int {} m["age" ] = 18 m["age" ] = 28
总结下来就是:
如果key不存在,就是新增。
如果key存在,就是修改。
读取
1 2 3 4 5 6 7 v := m["age" ] t.Log(v) v1 := m["age1" ] t.Log(v)
判断key是否存在(核心写法)
1 2 3 4 5 6 v,ok := m["age" ] if ok { t.Log("存在" ,v) } else { t.Log("不存在" ) }
删除元素
1 2 3 delete (m,"age" )delete (m, "not-exist" )
清空map
此特性是Go 1.21之后内置的函数clear()
删除map中的所有元素,对于nil map调用也安全的。
获取长度
遍历map
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 m := map [string ]int { "key1" :100 , "key2" :150 , "key3" :200 , } for k,v := range m { t.Logf("key = %v, value = %v" ,k,v) } for k:= range m { t.Logf("key = %v" ,k) } for _,v := range m { t.Logf("value = %v" ,v) }
那么如何确保输出顺序是稳定的呢?那就是先取key,在排序:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 func Test_LoopMapSort (t *testing.T) { m := map [string ]int { "key1" : 100 , "key2" : 150 , "key3" : 200 , } for k, v := range m { t.Log(k, v) } keys := make ([]string , 0 , len (m)) for k := range m { keys = append (keys, k) } sort.Strings(keys) for _, k := range keys { t.Logf("key = %v, value = %v" , k, m[k]) } }
多次执行,观察输出:
1 2 3 4 5 6 7 === RUN Test_LoopMapSort map_test.go:71: key3 200 map_test.go:71: key1 100 map_test.go:71: key2 150 map_test.go:80: key = key1, value = 100 map_test.go:80: key = key2, value = 150 map_test.go:80: key = key3, value = 200
对于Go语言的map,它对key还是略微有一些要求的,那就是map的key必须是可比较的类型,也就是说可以使用==或者!=比较的类型,以下类型都可以作为map的key:
string
int
bool
float64
complex64
pointer
channel
interface
array
struct
其中array和struct的元素/字段也必须为可比较。
下面三个是不能作为key的类型:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 func Test_MapKeyType (t *testing.T) { m := map [[2 ]int ]string { {1 , 2 }: "1" , {3 , 4 }: "2" , } m1 := map [struct { Name string Age int }]string { {"zhangsan" , 18 }: "1" , {"lisi" , 18 }: "2" , } t.Log(m, m1) }
至于slice不能作为map的key,主要原因是slice不能使用==比较
1 2 3 4 a := []int {1 , 2 , 3 } b := []int {1 , 2 , 3 } t.Log(a == b)
因为slice底层有三个属性指针、长度、容量,Go语言没有定义两个slice内容相等的比较语义。所以slice不能作为map的key。如果想用 slice 内容作为 key,可以转成 string 或 array。
1 2 3 4 5 6 7 8 m3 := map [string ]int {} key := fmt.Sprint([]int {1 , 2 , 3 }) m3[key] = 11 t.Log(m3) m4 := map [[3 ]int ]string {} m4[[3 ]int {1 , 2 , 3 }] = "ok" t.Log(m4)
还需要注意的是,两个map不能直接比较,但任意一个map可以和nil map比较,如果需要比较两个map,可以使用maps.Equal(m1,m2)来实现(注意,此时需要valuie也是可比较的才行),或者根据业务需求,自己定义比较逻辑。
map 是引用类型吗? Go 语言规范并没有把 map 正式定义为“引用类型”这一类别,但规范明确说明:非 nil 的 map 值包含对底层数据的引用;具体来说,map 值是对实现相关数据结构的引用。因此从使用效果看,map 赋值或传参会复制这个引用,而不会复制整张 map。
1 2 3 4 5 6 7 8 9 func Test_Map1 (t *testing.T) { m1 := map [string ]int { "a" : 1 , } m2 := m1 m2["a" ] = 100 t.Log(m1) }
或者传参也是类似:
1 2 3 4 5 6 7 8 9 func TestMapArg (t *testing.T) { m := make (map [string ]int ) changeMap(m) t.Log(m) } func changeMap (m map [string ]int ) { m["x" ] = 1 }
但是如果在函数里让参数指向一个新 map,不会影响外部变量本身:
1 2 3 4 5 6 7 8 9 10 11 func TestMapArg (t *testing.T) { m := make (map [string ]int ) changeMap1(m) t.Log(m) } func changeMap1 (m map [string ]int ) { m = make (map [string ]int ) m["y" ] = 1 }
如果想替换外部 map,需要返回新 map,或传 *map[...]...。
1 2 3 4 5 6 7 8 func reset () map [string ]int { return make (map [string ]int ) } func reset1 (m *map [string ]int ) { *m = make (map [string ]int ) }
其他关于map线程安全、其他map常用api或编码技巧等在后续学习中逐渐补充。
struct(结构体) struct 是 Go 中用于组合多个字段的数据类型,类似于Java中的classs。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 type User struct { Id, Age int Name string } func TestStruct (t *testing.T) { u := User{} u.Age = 18 u.Name = "John" u.Id = 1 t.Log(u) u1 := User{ Id: 2 , Age: 25 , Name: "Tom" , } t.Log(u1) }
struct 的零值是:所有字段都是各自类型的零值。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 type User struct { Id, Age int Name string Active bool Tags []string } func TestStruct (t *testing.T) { u2 := User{} t.Log(u2.Id) t.Log(u2.Age) t.Log(u2.Name) t.Log(u2.Tags) t.Log(u2.Active) t.Log(u2.Tags == nil ) }
struct 本身通常不为 nil,除非你使用的是 *Struct 指针。
初始化 对于struct,初始化方式有两种方式:
按照字段名初始化(推荐)
1 2 3 4 5 u1 := User{ Id: 2 , Age: 25 , Name: "Tom" , }
日常编码应当使用这种方式进行初始化。
按照字段顺序初始化
这种方式似乎看起来简单,但可读性差,一旦后期修改了struct的字段顺序,这样的代码就会出现错位或者报错。所以日常开发尽量不要使用这种方式。
个人觉得似乎还有一种,不过我是以Java程序员的视角似乎也是一种初始化,回头查查这样的初始化是不是有啥坑。
1 2 3 4 5 u := User{} u.Age = 18 u.Name = "John" u.Id = 1 t.Log(u)
访问和修改字段 1 2 3 4 5 6 7 func TestStructRU (t *testing.T) { u := User{Id: 1 , Name: "zhangsan" , Active: true , Tags: []string {"Java" , "Go" }, Age: 28 } t.Log(u.Tags) u.Age = 32 t.Log(u.Age) }
struct是值类型 struct 赋值时会拷贝整个值,不像 map、slice 那样共享整体结构。
1 2 3 4 5 6 7 func TestStructType (t *testing.T) { u := User{Id: 1 , Name: "zhangsan" , Active: true , Tags: []string {"Java" , "Go" }, Age: 28 } u1 := u u1.Name = "Jack" t.Log(u.Name) t.Log(u1.Name) }
当然,struct中的字段如果是引用类型,那么字段内部引用的数据仍然可能是共享的。
拷贝 如果struct中包含slice/map/pointer时,拷贝是浅拷贝
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 type Person struct { Name string Tags []string } func TestStructCopy (t *testing.T) { p1 := Person{ Name: "Jack" , Tags: []string {"Java" , "Go" }, } p2 := p1 p2.Tags[0 ] = "Python" t.Log(p1.Tags) t.Log(p2.Tags) }
原因:
struct 本身被拷贝了
但 Tags 这个 slice 头部被拷贝
slice底层数组仍然共享
函数传struct默认也是值拷贝
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 type Person struct { Name string Tags []string Age int } func grow (p Person) { p.Age++ } func TestStructCopyArg (t *testing.T) { p1 := Person{ Name: "Jack" , Tags: []string {"Java" , "Go" }, Age: 28 , } grow(p1) t.Log(p1.Age) }
如果想要修改原来的对象,那么就需要传指针
1 2 3 4 5 6 7 8 9 10 11 12 13 14 func grow1 (p *Person) { p.Age++ } func TestStructCopyArg (t *testing.T) { p1 := Person{ Name: "Jack" , Tags: []string {"Java" , "Go" }, Age: 28 , } grow1(&p1) t.Log(p1.Age) }
struct 指针访问字段可以省略解引用
1 2 3 4 5 6 func TestStruct2 (t *testing.T) { p := &Person{Name: "Jack" , Tags: []string {"Java" , "Go" }, Age: 28 } t.Log(p.Name) p.Age = 20 t.Log(p.Age) }
如何判断使用struct还是*struct 用 struct 值的情况
适合:
数据很小
不需要修改原对象
想避免共享状态
临时值、配置值、小对象
1 2 3 4 5 6 7 8 9 10 type Point struct { X, Y int } func Move (p Point, dx, dy int ) Point { p.X += dx p.Y += dy return p }
用 *struct 指针的情况
适合:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 type Counter struct { N int } func (c *Counter) Inc() { c.N++ } func main () { c := &Counter{} c.Inc() c.Inc() fmt.Println(c.N) }
参考上面的例子就可以说明问题。Inc()方法可以被Counter调用。
receiver 选择原则:如果一个类型的方法中,有任意一个方法需要指针 receiver,通常所有方法都用指针 receiver,保持一致。
1 2 3 4 5 6 7 8 9 10 11 12 type User struct { Name string Age int } func (u *User) Rename(name string ){ u.Name = name } func (u *User) IsAdult() bool { return u.Age >= 18 }
指针 指针保存的是一个变量的内存地址。其语法如下:
p 是一个指针变量
它指向一个 int 类型的变量
*int 表示“指向 int 的指针类型”
取地址 在Go语言中,使用&获取变量地址。
1 2 3 4 5 6 7 func TestGetAddress (t *testing.T) { x := 10 p := &x t.Logf("x = %d, p = %d" , x, p) }
解引用 以上一个例子为例,使用*p访问指针指向的值。
1 2 3 4 5 6 7 8 9 10 func TestDeref (t *testing.T) { x := 10 p := &x t.Logf("*p = %v" , *p) *p = 20 t.Logf("x = %d, *p = %d" , x, *p) }
指针的零值
1 2 3 4 5 6 7 8 9 func TestZeroValue (t *testing.T) { var p *int t.Logf("p == nil 的值是 %v" , p == nil ) }
通常为了避免这个问题,使用如下写法:
1 2 3 4 5 6 7 8 9 10 11 func TestZeroValue1 (t *testing.T) { x := 10 p := &x if p != nil { t.Log(*p) } else { t.Log("p is nil" ) } }
函数传值和传指针 其实前面的学习中,已经使用过几次。这里再介绍一下。
Go 默认是值传递。
如果传普通变量,函数内部修改的是副本。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 func change (x int ) { x = 20 } func TestArgsValue (t *testing.T) { n := 10 change(n) t.Logf("n = %d" , n) } func changePoint (p *int ) { *p = 20 } func TestArgsPoint (t *testing.T) { n := 10 changePoint(&n) t.Logf("n = %d" , n) }
struct指针(重点) 如果要修改struct的值,需要传递指针。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 type UserInfo struct { Name string Age int } func birthday (u *UserInfo) { u.Age++ } func TestStructPointer (t *testing.T) { u := UserInfo{ Name: "Jerry" , Age: 18 , } birthday(&u) t.Logf("Name = %v Age = %d" , u.Name, u.Age) }
new函数 new(T)会分配一个T类型的值,并返回*T
1 2 3 4 5 6 7 8 9 10 11 func TestNew (t *testing.T) { p := new (int ) t.Logf("p = %v, *p = %v" , p, *p) *p = 10 t.Logf(" *p = %v" , *p) u := new (UserInfo) u.Name = "Tom" u.Age = 18 t.Logf("Name = %v Age = %d" , u.Name, u.Age) }
指针不能做算数运算
1 2 3 4 5 6 7 8 9 10 func TestPointCompute (t *testing.T) { x := 10 p := &x }
指针和(数组、slice、map)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 func changeArray (a [3]int ) { a[0 ] = 99 } func chageArrayPoint (a *[3]int ) { a[0 ] = 99 } func TestArray (t *testing.T) { arr := [3 ]int {1 , 2 , 3 } changeArray(arr) t.Logf("arr = %v" , arr) chageArrayPoint(&arr) t.Logf("arr = %v" , arr) }
slice 本身包含指向底层数组的指针,所以传 slice 通常就能修改底层元素。
对于Go语言,slice的使用频率要远高于数组。下面通过一个例子来说明指针与切片:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 func changeSlice (nums []int ) { nums[0 ] = 99 } func changeSliceAppend (nums []int ) { nums = append (nums, 4 ) } func changeSliceReturnNew (nums []int ) []int { nums = append (nums, 5 ) return nums } func TestSlice (t *testing.T) { nums := []int {1 , 2 , 3 } changeSlice(nums) t.Logf("修改第一个值:nums = %v" , nums) changeSliceAppend(nums) t.Logf("追加一个新值:nums = %v" , nums) nums = changeSliceReturnNew(nums) t.Logf("追加一个新值,并返回新的slice: nums = %v" , nums) }
一般不推荐用 *[]T,除非你真的需要修改 slice 变量本身。
对于map来说,map 本身也是引用语义的数据结构,所以通常不需要传 *map。其他方式和slice基本一致。
泛型 与Java的泛型类似,Go语言泛型允许在定义函数、类型或方法时,暂时不指定具体类型,而是在使用时再传入具体类型。
它的核心作用是:
减少重复代码
提高类型安全
编写更通用的函数和数据结构
下面是一个最直观的例子:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 func maxInt (x int , y int ) int { if x > y { return x } else { return y } } func maxFloat (x float64 , y float64 ) float64 { if x > y { return x } else { return y } } func Max [T int | float64 ](a, b T) T { if a > b { return a } else { return b } } func TestGetMax (t *testing.T) { t.Logf("maxInt(1, 2) = %d" , maxInt(1 , 2 )) t.Logf("maxFloat(1.1, 2.2) = %f" , maxFloat(1.1 , 2.2 )) t.Logf("Max[int](1, 2) = %d" , Max[int ](1 , 2 )) t.Logf("Max[float64](1.1, 2.2) = %f" , Max[float64 ](1.1 , 2.2 )) }
使用泛型可以减少重复代码。
泛型的语法格式
1 2 3 4 5 6 7 8 9 10 func Identity [T any ](value T) T { return value } func TestGenerics (t *testing.T) { t.Logf("Identity[int](1) = %d" , Identity[int ](1 )) t.Logf("Identity[string]('hello') = %v" , Identity[string ]("Hello" )) t.Logf("Identity(2) = %v" , Identity(2 )) }
any约束 any 是 Go 1.18 引入的内置类型别名,本质上等价于 interface{}。它表示可以接受任何类型。
1 2 3 4 5 6 7 8 9 10 func PrintValue [T any ](value T) { fmt.Println(value) } func TestAny (t *testing.T) { PrintValue(123 ) PrintValue("Hello World!" ) PrintValue(true ) PrintValue(3.1415 ) }
comparable约束 comparable 表示该类型可以使用 == 和 != 比较。
常见可比较类型:
int
string
bool
指针
channel
由可比较字段组成的 struct
不可比较类型:
这一块和map的key比较像。
1 2 3 4 5 6 7 8 9 10 11 func Equal [T comparable ](a, b T) bool { return a == b } func TestComparable (t *testing.T) { t.Logf("Equal(1, 1) = %t" , Equal(1 , 1 )) t.Logf("Equal('hello', 'hello') = %t" , Equal("hello" , "hello" )) t.Logf("Equal(1.1, 1.1) = %t" , Equal(1.1 , 1.1 )) }
自定义类型约束 可以通过接口定义自己的类型约束(TypeScript?)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 type Number interface { int | int64 | float64 } func Add [T Number ](a, b T) T { return a + b } func TestCustomType (t *testing.T) { num1 := Add(2 , 3 ) num2 := Add(1.9 , 2.1 ) t.Logf("Add(2, 3) = %v" , num1) t.Logf("Add(1.9, 2.1) = %v" , num2) }
定义Number类型的|称之为类型联合,意思是可以是这些类型中的任意一种。例如如下:
1 2 3 type Integer interface { int | int8 | int16 | int32 | int64 }
表示满足该约束的类型是:
1 2 3 4 5 int int8 int16 int32 int64
这样的方式,也可以叫做类型集合。
底层类型约束 ~ 表示允许使用某个类型以及以该类型为底层类型的自定义类型。
下面通过两个例子作为对比:
不适用~
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 type MyInt int type IntOnly interface { int } func Double [T IntOnly ](v T) T { return v * 2 } type IntLike interface { ~int } func DoubleIntLike [T IntLike ](v T) T { return v * 2 } func Test1 (t *testing.T) { var y MyInt = 20 t.Logf("Double(y) = %v" , DoubleIntLike(y)) }
~int 的意思是: 只要底层类型是 int,都可以满足这个约束。
泛型函数 泛型函数是最常见的泛型用法。
1 2 3 4 5 6 7 8 9 10 11 12 13 func Contains [T comparable ](items []T, target T) bool { for _, item := range items { if item == target { return true } } return false } func TestGenericsFunc (t *testing.T) { t.Logf("Contains([]int{1, 2, 3}, 2) = %t" , Contains([]int {1 , 2 , 3 }, 2 )) t.Logf("Contains([]string{'a', 'b', 'c'}, 'd') = %t" , Contains([]string {"a" , "b" , "c" }, "d" )) }
可以实现一个类似lambda中的map操作。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 func Map [T any , R any ](items []T, fn func (T) R) []R { result := make ([]R, len (items)) for i, item := range items { result[i] = fn(item) } return result } func TestMapFunc (t *testing.T) { nums := []int {1 , 2 , 3 } strs := Map(nums, func (n int ) string { return fmt.Sprintf("Number: %d" , n) }) t.Log(strs) }
同理,实现一个Filter和Reduce
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 func Filter [T any ](items []T, fn func (T) bool ) []T { result := make ([]T, 0 , len (items)) for _, item := range items { if fn(item) { result = append (result, item) } } return result } func Reduce [T any , R any ](items []T, initial R, fn func (R, T) R) R { result := initial for _, item := range items { result = fn(result, item) } return result } func TestLambdaFunc (t *testing.T) { nums := []int {1 , 2 , 3 , 4 , 5 } evens := Filter(nums, func (n int ) bool { return n%2 == 0 }) t.Log(evens) sum := Reduce(nums, 0 , func (acc int , n int ) int { return acc + n }) t.Logf("Sum of nums = %d" , sum) }
结构体泛型 不仅函数可以使用泛型,结构体也可以使用泛型。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 type Box[T any] struct { value T } type Pair[K comparable, V any] struct { Key K Value V } func TestGenericsStruct (t *testing.T) { intBox := Box[int ]{value: 28 } stringBox := Box[string ]{value: "Hello Go" } t.Logf("intBox = %v, stringBox = %v" , intBox.value, stringBox.value) p1 := Pair[string , int ]{ Key: "age" , Value: 28 , } p2 := Pair[string , string ]{ Key: "Name" , Value: "Jerry" , } t.Logf("p1 = {Key: %v, Value: %v}, p2 = {Key: %v, Value: %v}" , p1.Key, p1.Value, p2.Key, p2.Value) }
泛型与接口的区别 虽然自定义泛型使用的是interface,看起来像是接口,但和接口还有些区别。普通接口关注的是行为,泛型约束关注的是类型集合。
1 2 3 type Writer interface { Write([]byte ) (int ,error ) }
只要实现Write方法,就满足接口。
1 2 3 type Number interface { int | int32 | float64 }
表示类型必须属于这个类型集合,也可以同时约束类型和方法。
1 2 3 4 type StringNumber interface { ~int | ~int64 String() string }
但这种约束要求类型:
底层类型是 int 或 int64
实现了 String() string