Go 语言中的复合类型包括:数组(array)、切片(slice)、哈希表(map)、结构体(struct)。 函数是 Go 语言里面的核心设计。 这里结合网上的一些资料和自己的学习理解,记录一下,加深理解。
说复合类型之前先说一下指针,这样复合类型里边的一些概念就好理解了。
指针 Go 保留了指针,*T
表示 T 对应的指针类型,如果包含包名, 则应该是*.T
。 代表指针类型的符号*
总是和类型放在一起,而不是紧挨着变量名。 同样支持指针的指针**T
。
声明
说明 操作符&
取变量地址,用*
透过指针变量间接访问目标对象 默认值是nil
,没有 NULL 常量 不支持指针运算,直接’.’选择符操作指针目标对象成员 可以在 unsafe.Pointer 和任意类型指针间进行转换 可以将 unsafe.Pointer 转换为 uintptr,然后变相做指针运算,uintptr 可以转换为整数
示例 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 package mainimport "fmt" type User struct { Id int Name string }func main () { i := 100 var p *int = &i println (*p) up := &User{1 , "Jack" } up.Id = 100 fmt.Println(up) u2 := *up u2.Name = "Tom" fmt.Println(up, u2) }
结果:
1 2 3 100 &{100 Jack} &{100 Jack} {100 Tom}
Array 在 Go 语言中,数组是一个值类型(value type)。 所有的值类型变量在赋值和作为参数传递时都将产生一个复制动作。 如果作为函数的参数类型,则在函数调用时参数发生数据复制,在函数体中无法修改传入数组的内容。
声明 & 赋值 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 var VarName [n]type e.g.var a [5 ]int var c [2 ][3 ]int var b int = [5 ]int {1 ,2 ,3 ,4 ,5 } a := [3 ]int {1 ,2 ,3 } b := [10 ]int {1 ,2 ,3 } c := [20 ]int {19 :1 } d := [...]int {4 ,5 ,6 } e := [...]int {0 :1 , 1 :2 , 19 :3 } doubleArray := [2 ][4 ]int {[4 ]int {1 ,2 ,3 ,4 }, [4 ]int {5 ,6 ,7 ,8 }} easyArray := [2 ][4 ]int {{1 ,2 ,3 ,4 }, {1 ,2 ,3 ,4 }}
数组的长度是该数组类型的一个内置常量
注意,数组长度也是类型的一部分,因此不同长度数组为不同类型(内置常量)
即[3]int
和[4]int
是不同类型,并且数组不能改变长度
数组之间的赋值是值的赋值,即当把一个数组作为参数传入函数的时候,传入的其实是该数组的副本(一次复制操作),而不是它的指针,如果要传入指针,使用slice
元素访问 1 2 3 4 5 6 7 for i:=0 ; i < len (array); i++ { fmt.Println(i, array[i]) }for i, v := range array { fmt.Println(i, v) }
可以用new
创建数组, 返回一个指向数组的指针
注意区分
1 2 3 4 5 6 7 a := [100 ]int {}var p *[100 ]int = &a x, y = 1 , 2 a := [...]*int {&x, &y}
Slice 切片(类似 Java 中的 ArrayList)就像一个指向数组的指针,但更复杂,实际上它拥有自己的数据结构,而不仅仅是指针(指向原生数组的指针 + 数组切片中元素个数 + 数组切片已分配的存储空间)。 切片是一个引用类型,总是指向一个底层 array,声明可以向 array 一样,只是不需要长度。 slice 就像一个结构体,包含三个元素:
一个指针,指向数组中 slice 指定的开始位置
长度,即 slice 的长度
最大长度,也就是 slice 开始位置到数组的最后位置的长度
声明 & 赋值 通过 array 创建
1 2 3 4 5 6 7 var myArray [10 ]int = [10 ]int {1 ,2 ,3 ,4 ,5 ,6 ,7 ,8 ,9 ,10 }var mySlice []int = myArray[:5 ] a := [5 ]int {1 ,2 ,3 ,4 ,5 } b := a[2 :4 ] b := a[:4 ] b := a[2 :]
从数组或已存在的 slice 再次声明
1 2 3 4 5 var ar [10 ]byte {'a' , 'b' , 'c' , 'd' , 'e' , 'f' , 'g' , 'h' , 'i' , 'j' }var a, b []byte a = ar[2 :5 ] b = ar[3 :5 ]
直接创建
1 2 3 myslice1 := make ([]int , 5 ) myslice2 := make ([]int , 5 , 10 ) myslice3 := []int {1 ,2 ,3 ,4 ,5 }
元素访问 1 2 3 4 5 6 7 for i:=0 ; i<len (mySlice); i++ { fmt.Println(i, mySlice[i]) }for i, v := range mySlice { fmt.Println(i, v) }
其他操作 大小和容量 len
获取 slice 的长度cap
获取 slice 的最大容量 容量需要注意一下,数组值的容量总是等于其长度。而切片值的容量则往往与其长度不同。请看下图。
动态增减元素 append
向 slice 里面追加一个或者多个元素,然后返回一个和 slice 一样类型的 slice
1 2 3 mySlice = append (mySlice, 1 , 2 , 3 ) mySlice = append (mySlice, mySlice2)
特别注意(这是一个大坑): append
会改变 slice 所引用的数组的内容,从而影响到引用统一数组的其他 slice,但当 slice 中没有剩余空间,此时动态分配新的数组空间返回的 slice 数组指针将指向这个空间,而原数组的内容将保持不变,其他引用此数组的 slice 不受影响
内容复制 可以用copy
从源 slice 的 src 中复制到目标 dst,并且返回复制元素的个数。
1 2 3 4 5 6 7 copy (dst, source) slice1 := []int {1 ,2 ,3 ,4 ,5 } slice2 := []int {5 ,4 ,3 }copy (slice2, slice1) copy (slice1, slice2)
切片 默认开始位置 0,ar[:n]
等价于ar[0:n]
第二个序列默认是数组长度ar[n:]
等价于ar[n:len(ar)]
从一个数组直接获取 slice,可以是ar[:]
slice 是引用类型,所以当改变其中元素的时候,其他的所有引用都会改变。
1 2 aSlice = array[3 :7 ] bslice = aSlice[:3 ]
Map Map 类型其实是哈希表的一个实现,类似 Java 中的 HashMap、Python 中字典的概念。 map 是无序的,长度不固定,内置的len
可以用于 map,可以方便的修改。
声明 & 赋值 初始化一个 Map
1 2 3 4 5 6 7 8 9 map [keyType]valueTypevar m map [string ] PersonInfo m = make (map [string ] personInfo[, 100 ])var numbers map [string ]int or numbers := make (map [string ]int ) numbers["one" ] = 1
元素访问 1 2 3 4 5 6 7 8 rating := map [string ]float32 {"c" :5 , "Go" :4.5 } csharpRating, ok := rating["C#" ]if ok { fmt.Println("get the value" ) } else { fmt.Println("error" ) }
基本操作 赋值 1 m["1234" ] = PersonInfo{}
删除
struct struct,一组字段的集合,类似其他语言的 class。 Go 放弃了大量包括继承在内的面向对象特性,只保留了组合(composition)这个最基础的特性。
声明及初始化 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 type person struct { name string age int }func main () { var P person P.name = "tom" P.age = 25 fmt.Println(P.name) P1 := person{"Tom1" , 25 } fmt.Println(P1.name) P2 := person{age: 24 , name: "Tom" } fmt.Println(P2.name) }
struct 的匿名字段(嵌入字段) 这是实现类似于继承的一种手段,但是其实不同于继承。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 type Human struct { name string age int weight int } tyep Student struct { Human speciality string } mark := Student(Human{"mark" , 25 , 120 }, "Computer Science" ) mark.name mark.age
能够实现字段继承,当字段名重复的时候,优先取外层的,可以通过指定 struct 名还决定取哪个
1 2 mark.Human = Human{"a" , 55 , 220 } mark.Human.age -= 1
struct 不仅可以使用 struct 作为匿名字段,自定义类型、内置类型都可以作为匿名字段,而且可以在相应字段上做函数操作
method 1 2 3 4 5 6 7 8 9 type Rect struct { x, y float64 width, height float64 }func (r ReciverType) funcName(params) (results) { }
Reciver 默认以值传递,而非引用传递,还可以是指针。 指针作为 Receiver 会对实例对象的内容发生操作,而普通类型作为 Receiv er 仅仅是以副本作为操作对象,而不对原实例对象发生操作。
如果一个 method 的 receiver 是*T
,调用时,可以传递一个T
类型的实例变量V
,而不必用&V
去调用这个 method
1 2 3 4 5 6 7 func (r *Rect) Area() float64 { return r.width * r.height }func (b *Box) SetColor(c Color) { b.color = c }
method 继承和重写 采用组合的方式实现继承
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 type Human struct { name string }type Student struct { Human School string }func (h *Human) SayHi() { fmt.Println(h.name) }func main () { h := Human{name: "human" } fmt.Print(h.name) h.SayHi() s := Student{Human{"student" }} s.SayHi() }
还可以进行方法重写
1 2 3 4 func (e *Student) SayHi() { e.Human.SayHi() fmt.Println(e.School) }
make 和 new make
用于内建类型(map, slice, channel) 的内存分配。
new
用于各种类型的内存分配。new
本质上和其他语言中同名函数一样,new(T)
分配了零值填充的T
类型的内存空间,并返回其地址,即一个*T
类型的值 即,返回一个指针,指向新分配的类型T
的零值 。
make(T, args)
,只能创建 slice、map、channel,并返回一个有初始值(非零值)的T
类型,而不是*T
。 本质来讲,导致这三个类型有所不同的原因是,指向数据结构的引用在使用前必须被初始化。
函数 函数是 Go 语言里面的核心设计,通过关键字func
来声明
1 2 3 4 func funcName (input type1, input2 type2) (output1 type1, output2 type2) { return value1, value2 }
基本语法 语法 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 func func_name (a int ) { println (a) }func func_name (a, b int , c string ) { println (a, b, c) }func func_name (a, b int ) int { return a + b }func func_name (a, b int ) (c int , err error ) { return a+b, nil }func SumAndProduct (A, B int ) (int , int ) { return A+B, A*B }
说明 Go 函数通过关键字func
声明 可以有一个或多个参数,每个参数后面带有类型。通过,
分隔函数可以返回多个值 返回值声明,可以只声明类型。 如果没有返回值,可以省略最后的返回信息;如果有返回值,必须在外层添加 return。
不支持:
嵌套(nested)
重载(overload)
默认参数(default parameters)
支持:
无需声明原型
不定长度变参
多返回值
命名返回值参数
匿名函数
闭包
注意:
函数使用func
开头,左大括号不能另起一行
小写字母开头的函数指在本包内可见,大写字母开头的函数才能被其他包调用
多返回值 可以像 python 那样返回多个结果,只是非 tuple。对于不想要的返回值,可以扔垃圾桶_
。
如果用命名返回参数,return 语句可以为空。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 package mainfunc change (a, b int ) (x, y int ) { x = a + 100 y = b + 100 return }func main () { a := 1 b := 2 c, d := change(a, b) println (c, d)
如果命名返回参数被代码块中的同名变量覆盖了,就必须使用显式 return 返回结果。 不需要强制命名返回值,但是命名后的返回值可以让代码更加清晰,可读性更强。
参数传递 传值与传指针 指针, Go 保留指针,用.
而非”->”操作指针目标对象成员。 操作符:&
取变量地址*
通过指针间接访问目标函数
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 func add1 (a int ) int { a = a + 1 return a } x := 3 x1 := add1(x) x x1 传值,x1的值没有改变func add2 (a *int ) int { *a = *a + 1 return *a } x := 3 x1 := add2(&x) x x1
传指针的好处:
传指针多个函数能操作同一个对象;
传指针比较轻量级(8byte),只是传内存地址,我饿们可以用指针来传递体积大的结构体。
Go 语言中,string、slice、map 这三种类型的实现机制类似指针,所以可以直接传递,而不用取地址后传指针 注意,若函数需要改变 slice 长度,仍需要取地址传指针
可变参数 变参本质上就是一个 slice,且必须是最后一个形参。 将 slice 传递给变参函数时,注意用…
展开(类似 Java 中的可变参数),否则会被当做单个参数处理,和 python 类似。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 package mainfunc sum (s string , args ...int ) { var x int for _, n := range args { x += n } println (s, x) }func main () { sum("1+2+3=" , 1 , 2 , 3 ) x := []int {0 ,1 ,2 ,3 ,4 } sum("0+1+2+3=" , x[:4 ]...) }
...type
类型只能作为函数的参数类型存在,并且是最后一个参数 本质上是一个数组切片,即[]type
。
传入任意类型的不定参数:
1 2 func Printf (format string , args ...interface {}) { }
匿名函数 1 2 3 f := func (x,y int ) int { return x + y }
函数作为值、类型 在 Go 语言中,函数也是一种变量,可以通过type
来定义它,它的类型就是所有拥有相同的参数,相同的返回值的函数。
语法:
1 type typeName func (input1 inputType1, input2 inputType2 [, ....]) (result1 resultType1 [,....])
用法 1 (这种用法,在写接口的时候非常有用) e.g.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 type testInt func (int ) bool func filter (slice []int , f testInt) []int { var result []int for _, value := range slice { if f(value) { result = append (result, value) } } }func isOdd (integer int ) bool { if integer % 2 == 0 { return false } return true } filter(a, isOdd)
用法 2 可以定义函数类型,也可以将函数作为值进行传递(默认值nil
) e.g.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 package maintype callback func (s string ) func test (a, b int , sum func (int , int ) int ) { println ( sum(a,b) ) }func main () { var cb callback cb = func (s string ) { println (s) } cb("hello world" ) test(1 , 2 , func (a, b int ) int {return a + b}) }
结果:
main
函数和init
函数Go 里面有两个保留的函数:init
函数(能够应用于所有的package
)和main
函数(只能应用于package main
)。这两个函数在定义时不能有任何的参数和返回值。虽然一个package
里面可以写任意多个init
函数,但这无论是对于可读性还是以后的可维护性来说,我们都强烈建议用户在一个package
中每个文件只写一个init
函数。
Go 程序会自动调用init()
和main()
,所以你不需要在任何地方调用这两个函数。每个package
中的init
函数都是可选的,但package main
就必须包含一个main
函数。
程序的初始化和执行都起始于main
包。如果main
包还导入了其它的包,那么就会在编译时将它们依次导入。有时一个包会被多个包同时导入,那么它只会被导入一次(例如很多包可能都会用到fmt
包,但它只会被导入一次,因为没有必要导入多次)。当一个包被导入时,如果该包还导入了其它的包,那么会先将其它包导入进来,然后再对这些包中的包级常量和变量进行初始化,接着执行init
函数(如果有的话),依次类推。等所有被导入的包都加载完毕了,就会开始对main
包中的包级常量和变量进行初始化,然后执行main
包中的init
函数(如果存在的话),最后执行main
函数。
参考资料: 谢大的《Go Web 编程》 wklken 的 Golang 笔记