本文共 9015 字,大约阅读时间需要 30 分钟。
上文 中我们介绍了golang 基本类型的常见用法,本文将介绍 golang 中的复合数据类型,常用的复合数据类型有 array 数组,slice 切片,map 字典 和 struct 四种。
数组是一个由固定长度的特定类型元素组成的序列,由于长度固定,在实际业务场景下使用较为不便,因此在 go 中很少直接使用。
数组的长度是数组类型的一个组成部分,因此 [3]int 和 [4]int 是两种不同的数组类型。数组的长度必须是常量表达式,因为数组的长度需要在编译阶段确定。
和大多数语言一样,go 的数组下表也是从 [0] 开始,到 [len - 1] 结束
var a [3]intvar q [3]int = [3]int{ 1,2,3} // 1,2,3var r [3]int = [3]int{ 1,2} // 1,2,0a[0] = 1a[1] = 2a[2] = 3
也可以使用 “…” 来声明数组,如果在数组的长度位置出现的是“…”省略号,则表示数组的长度是根据初始化值的个数来计算,因此,上面 q 数组的定义可以简化为
q := [...]int{ 1,2,3}fmt.Println(reflect.TypeOf(q)) // 输出 [3]int
对于较大的数组,可以指定一个索引和对应值列表的方式初始化,如下,定义了一个含有100个元素的数组r,索引为 13 的位置值为 21,最后一个元素被初始化为 -1,其它元素都是用 0 初始化。
r := [...]int{ 13: 21, 99: -1}
Slice(切片)代表变长的序列,序列中每个元素都有相同的类型。
go 的 slice 底层是由数组实现的,一个slice由三个部分构成:指针、长度和容量。创建 slice 时,指针指向第一个slice元素对应的底层数组元素的地址,长度对应slice中元素的数目,容量表示创建数组时分配空间的初始大小,长度不能超过容量,容量一般是从slice的开始位置到底层数据的结尾位置。
和数组不同的是,slice 之间不能使用 ==
互相比较,slice 唯一合法的比较操作是和nil比较, 但是通常更推荐使用 len(s) == 0
来判断,而不是 s == nil
,否则对 []int{}
这种形式的 slice 判断会出现问题,可以参考如下代码。
var s []int // len(s) == 0, s == nils = []int(nil) // len(s) == 0, s == nils = []int{ } // len(s) == 0, s != nil
使用 make([]Type, len, cap)
初始化 slice, cap 参数可以缺省, 缺省时默认 cap 等于 len
sl_null := []int{ } // 创建空slicesl0 := make([]int, 0, 3) // 创建 cap 为 3 的空 slice []sl1 := make([]int, 0) // 创建 []sl2 := make([]int, 3) // 创建 [0,0,0]// 创建有初始内容的 slicesl4 := []int{ 1,2,3} // 类型为 []int// 注意区分 slice 和 数组的初始化// 数组的初始化为 s := [...]{1,2,3}, 类型为 [3]int
添加元素时,如果添加完成后 len 大于当前 cap, slice 会进行扩容, 每次扩容 cap 的大小会翻一倍
sl3 = append(sl3, 4) // 添加单个元素sl3 = append(sl3, 5, 6, 7) // 添加多个元素sl4 := []int{ 8, 9, 10}sl3 = append(sl3, sl4...) // 将 sl4 展开,把所有元素添加到 sl3 末尾// 修改 sl3 不会改变 sl4 的元素
内置的 len
和 cap
函数分别返回slice的长度和容量。
len_sl3 = len(sl3) // intcap_sl3 = cap(sl3) // int
没有内置函数,需要自己遍历实现, 对 slice 遍历时取到两个值,第一个为 slice 的下标,第二个为下标对应的值。
for _, num := rang num_slice { if num == target { // ... }}
go 没有直接删除元素的库函数,通过切片赋值来实现
(插入操作也是类似)
// 从起始位置删除a = a[1:] // 删除开头1个元素a = a[N:] // 删除开头N个元素// 从中间位置删除,其实是将两个 slice 片段合并,注意后半段需要使用 ... 展开a = append(a[:i], a[i+1:]...) // 删除中间1个元素a = append(a[:i], a[i+N:]...) // 删除中间N个元素// 从尾部删除a = a[:len(a)-1] // 删除尾部1个元素a = a[:len(a)-N] // 删除尾部N个元素
a := []int{ 1,2,4,5,6,7,8}temp := append([]int{ 3}, a[2:]...)a = append(a[:2], temp...)// output a: [1 2 3 4 5 6 7 8]//下面的写法等价:a = append(a[:2], append([]int{ 3},a[2:]...)...)
介绍切片和复制之前,需要先简单介绍一下浅拷贝和深拷贝。
浅拷贝:重新在堆中创建内存,拷贝前后对象的基本数据类型互不影响,但拷贝前后对象的引用类型因共享同一块内存,会相互影响。
深拷贝:从堆内存中开辟一个新的区域存放新对象,对对象中的子对象进行递归拷贝, 拷贝前后的两个对象互不影响。
切片操作为浅拷贝,sli[m:n]
会取得从 sli[m]
到 sli[n - 1]
之间的元素,但是不会复制切片的数据。 它创建一个指向原始数组的新切片值。
因此,修改重新切片的元素会修改原始切片的元素。实际使用中如果忽视了这一点,就很容易发生预期之外的错误。
注意: 与 python 不同, golang 的切片索引不能为负数
sli := []int{ 1,2,3,4,5,6} // [1 2 3 4 5 6]sli2 := sli[2:5] // [3 4 5]sli2[0] = 10 // [10 4 5]fmt.Println(sli) // [1 2 10 4 5 6]
复制操作为深拷贝,copy 后, 修改 t 的内容不会改变 s 的内容
如果 t 的 len 小于 s, 则将 s 的内容截断后复制
s := []int{ 1,2,3,4,5,6,7,8,9,10}t := make([]int, len(s), cap(s))copy(t,s) // 将 s 复制给 t [1 2 3 4 5 6 7 8 9 10]t[0] = 10 // [10 2 3 4 5 6 7 8 9 10]fmt.Println(s) // [1 2 3 4 5 6 7 8 9 10]
s := []int{ 1,2,3,4,5,6,7,8,9,10}for i, j := 0, len(s) - 1; i < j; i, j = i + 1, j - 1 { s[i], s[j] = s[j], s[i]}
golang 的 map 为哈希表,是一个无序的 key/value 对的集合,其中所有的 key 都是不同的,通过给定的 key 可以在常数时间复杂度内检索、更新或删除对应的 value。
map 的 key 必须是可比较的数据类型,value 的类型没有要求(如 slice 不能做 key,只能做 value)
// 声明变量,默认 map 是 nilvar map_variable map[key_data_type]value_data_type // 声明 类型为 map[K]Vmap_variable = make(map[key_data_type]value_data_type) // 定义// 使用 make 函数map_variable := make(map[key_data_type]value_data_type) // 如 dic := map[string]int// 等价于map_variable := map[key_data_type]value_data_type{ }// 这种方式可以构建带初始值的map,如:age:= map[string]int { "Tony" : 35, "Wallace" : 24, // 注意最后也要带逗号}
// 判断某个 key n 是否在字典中// 若存在,ok 为 True,否则为 Falseif _, ok := map_name[n]; ok { // ...}
若 key 存在则为修改,否则为添加
map_name[key] = value // 如 age["Tony"] = 35
Go 的删除操作和访问操作都是安全的,即使访问或者删除的 key 不存在,也不会报错,访问不存在的 key 返回 0
delete(age, "Tony")
注意:map 可以对 value 进行各种数值操作,但是 value 并不是一个变量,因此不能对 map 的元素进行取地址操作。禁止对map元素取址的原因是map可能随着元素数量的增长而重新分配更大的内存空间,从而可能导致之前的地址无效。
n = &age["bob"] // Compile Error: cannot take address of map element
Map的迭代顺序是随机的,并且不同的哈希函数实现可能导致不同的遍历顺序
for key, value := range age { fmt.Println(key, value)}
golang 没有内置的集合功能,可以使用 map 来实现集合,因为 key 是唯一的(如使用 key 代表集合元素,value 全部置为 1)
结构体是一种聚合的数据类型,是由零个或多个任意类型的值聚合成的实体。每个值称为结构体的成员。
type employee struct { // 定义 Employee 类型的结构体 id int name string address string title string}var wallace employee // 声明一个 Employee 类型的变量 wallacewallace.id = 12345 // 使用点操作符访问结构体变量tony := Employee { 3333, "Tony", "Beijing", "Engineer"} // 初始化 Employee 变量cassie := Employee{ id: 3333 name: "cassie" address: "shenzhen" title: "engineer"}
注意:
通过再方法内部需要改变结构体成员变量值,则必须要使用结构体指针进行传值,因为在 go 中,所有的函数参数都是值拷贝传入的,函数参数并不是函数调用时的原始变量。
func AwardAnnualRaise(e *Employee) { e.Salary = e.Salary * 105 / 100}
此外,一个命名为S的结构体类型将不能再包含S类型的成员,但是S类型的结构体可以包含*S
指针类型的成员,这可以让我们创建递归的数据结构,比如链表和树结构等。
type tree struct { value int left *tree right *tree}
而且用指针作为参数效率会更高,因此通常函数的入参和回参中的结构体都会使用指针的方式。
推荐使用 %+v 而不是 %v,输出会包括结构体的成员名称和值
package mainimport "fmt"type info struct { name string id int}func main() { v := info{ "Nan", 33} fmt.Printf("%v\n", v) fmt.Printf("%+v\n", v) fmt.Printf("%#v\n", v) fmt.Printf("%T\n", v)}
运行结果如下:
{ Nan 33}{ name:Nan id:33}main.info{ name:Nan id:33}main.info
为了便于维护,通常会将有相同功能的属性独立出来,如:
type Point struct { X, Y int}type Circle struct { Center Point Radius int}type Wheel struct { Circle Circle Spokes int}
但是这种修改会导致访问成员变量变得非常繁琐:
var w Wheelw.Circle.Center.X = 8w.Circle.Center.Y = 8w.Circle.Radius = 5w.Spokes = 20
可以使用匿名成员解决这个问题,只声明成员的数据类型而不指定成员的名字,匿名成员的数据类型必须是命名的类型或指向一个命名的类型的指针。
type Point struct { X, Y int}type Circle struct { Point Radius int}type Wheel struct { Circle Spokes int}// 得益于匿名嵌入的特性,我们可以直接访问叶子属性而不需要给出完整的路径var w Wheelw.X = 8 // equivalent to w.Circle.Point.X = 8w.Y = 8 // equivalent to w.Circle.Point.Y = 8w.Radius = 5 // equivalent to w.Circle.Radius = 5w.Spokes = 20// 不幸的是,结构体字面值并没有简短表示匿名成员的语法, 因此下面的语句都不能编译通过:w = Wheel{ 8, 8, 5, 20} // compile error: unknown fieldsw = Wheel{ X: 8, Y: 8, Radius: 5, Spokes: 20} // compile error: unknown fields// 正确的初始化方式为下列两种形式,两种形式是等价的w = Wheel{ Circle{ Point{ 8, 8}, 5}, 20}w = Wheel{ Circle: Circle{ Point: Point{ X: 8, Y: 8}, Radius: 5, }, Spokes: 20, // NOTE: trailing comma necessary here (and at Radius)}
由于简洁性、可读性和流行程度等原因,JSON是结构化信息的标准协议中应用最广泛的一个。JSON 使用键值对的方式表示结构化信息,并用逗号分隔,要注意的是在最后一个成员或元素后面并没有逗号分隔符,形如:
{ "id" : 1, "name": "wallace", "teams":{ "team_id":1, "team_name":"wallace team" }, "skills":["go","mysql","redis"], "description": "test"}
将一个 Go 语言中的结构体 slice 转为 JSON 的过程叫编码(marshaling),编组通过调用json.Marshal
函数完成,转换为 json 的 key 值为结构体中 json tag 指定的内容。
如以下结构体:
type Team struct { TeamID int `json:"team_id" db:"team_id"` TeamName string `json:"team_name" db:"team_name"`}type Member struct { ID int `json:"id" db:"id"` Name string `json:"name" db:"name"` Teams *Team `json:"teams" db:"teams"` Skills []string `json:"skills" db:"skills"` Description string `json:"description" db:"description"`}
对结构体对象进行 marshaling,编码为 json 字符串
wallace := &Member{ ID: 1, Name: "wallace", Teams: &Team{ TeamID: 1, TeamName: "team 1", }, Skills: []string{ "go", "mysql", "redis"}, Description: "test",}js, err := json.Marshal(wallace)if err != nil { log.Fatalf("JSON marshaling failed: %s", err)}fmt.Printf("%s\n", js)
输出为:
{ "id":1,"name":"wallace","teams":{ "team_id":1,"team_name":"team 1"},"skills":["go","mysql","redis"],"description":"test"}
也可以使用 json.MarshalIndent
函数产生整齐缩进的输出。该函数有两个额外的字符串参数用于表示每一行输出的前缀和每一个层级的缩进:
data, err := json.MarshalIndent(wallace, "", " ")if err != nil { log.Fatalf("JSON marshaling failed: %s", err)}fmt.Printf("%s\n", data)
输出为:
{ "id": 1, "name": "wallace", "teams": { "team_id": 1, "team_name": "team 1" }, "skills": [ "go", "mysql", "redis" ], "description": "test"}
在编码时,只有可导出的结构体成员才会被编码。
编码的逆操作是解码(unmarshaling),将 json 字符串转换为结构体。
member := &Member{ }err = json.Unmarshal(data, member)if err != nil { log.Fatalf("JSON unmarshaling failed: %s", err)}fmt.Printf("%+v", *member)
输出为:
{ ID:1, Name:"wallace", Teams:(*timetest.Team)(0xc42000a3e0), Skills:[]string{ "go", "mysql", "redis"}, Description:"test"}
有一点需要注意,对于结构体中没有赋值的数组,json.Unmarshal 会解析为 null,有可能会导致前端框架的报错,所以建议把结构体中的空数组都初始化为 []
转载地址:http://phsyk.baihongyu.com/