1. 数组(array)(OK)
数组 | |
数组的概念 | 数组是具有固定长度且拥有零个或多个相同数据类型元素的序列 |
i. 元素的数据类型相同 ii. 长度固定的序列 iii. 零个或多个元素的序列 | |
与 slice 对比 | 由于数组的长度固定,所以在 Go 里面很少直接使用; slice 的长度可以增长和缩短,故 slice 使用得更多 |
定义数组语法 | var 数组名 [数组长度]元素类型 var arrName [arrLen]eType |
访问单个元素 | 数组中的每个元素是通过索引(下标)来访问的,索引从 0 到数组长度减 1 |
获取数组长度 | Go 内置的函数 len 可以返回数组中的元素个数 |
代码示例 | var a [3]int // 定义长度为 3 的 int 型数组 fmt.Println(a[0]) // 输出第一个元素 fmt.Println(a[len(a) - 1]) // 输出最后一个元素 // 输出索引和元素 for i, v := range a { fmt.Print("%d %d\n", i, v) } // 仅输出元素,丢弃索引 for _, v := range a { fmt.Printf("%d\n", v) } |
数组的零值 | 默认情况下,一个新数组中的元素初始值为元素类型的零值; 数字的零值为 0;字符串的零值为 "" ;布尔的零值为 false |
声明并初始化 | 可以使用数组字面量,即根据一组值来初始化一个数组; 若元素个数与数组长度不一致,缺省的元素为元素类型的零值 |
var q [3]int = [3]int{1,2,3} var r [3]int = [3]int{1,2} fmt.Println(r[2]) // "0" | |
确定数组长度 | 在声明数组的同时给出数组长度; var a [3]int |
用数组字面量初始化,如果省略号 "..." 出现在数组长度的位置, 那么数组的长度由初始化数组的字面量中的元素个数决定; 注意:省略号 "..." 只能出现在数组字面量的数组长度的位置 q := [...]int{1,2,3} var p [...]int = [3]int{1,2,3} // 编译错误 | |
数组长度示例 | q := [...]int{1,2,3} fmt.Printf("%T\n", q) // "[3]int" 使用 %T 来打印对象的类型 |
数组长度特别说明 | i. 数组的长度是数组类型的一部分,即长度是数组的固有属性 [3]int 和 [4]int 是两种不同的数组类型 ii. 数组的长度必须是常量表达式 这个常量表达式的值在程序编译时就必须确定 |
数组类型示例 | q := [3]int{1,2,3} q = [4]int{1,2,3,4} // 编译错误:不可以将 [4]int 赋值给 [3]int |
数组字面量的 默认值 (索引-值 初始化) | 数组 、slice 、map 、结构体 的字面语法都是相似的; 上面的数组例子,是按顺序给出一组值; 也可以向下面这样给出一组元素,元素同时具有索引和值 |
// 声明类型别名 type Currency int // 定义一组常量 const ( USD Currency = iota EUR GBP RMB ) // 声明数组,用数组字面量初始化 symbol := [...]string{USD :"$",EUR :"€",GBP :"£",RMB :"¥"} fmt.Println(RMB,symbol[RMB]) // "3 ¥" | |
在这种情况下,元素(索引-值)可以按照任意顺序出现,索引有时候还能省略; 没有指定值的索引位置的元素,其值为数组元素类型的零值 | |
// 下标为 99 的元素值为 -1 ,则前 99 个元素均为 0 r := [...]int{99:-1} | |
数组的比较 | 如果一个数组的元素类型是可比较的,那么这个数组也是可比较的 可以直接使用 " == " 操作符来比较两个(同类型的)数组,比较的结果是两边元素的值是否完全相同 使用 " != " 来比较两个数组是否不一样 不同类型(长度不同 或 元素类型不同)的数组不能比较,否则会编译报错 |
代码示例 | a := [2]int{1,2} b := [...]int{1,2} c := [2]int{1,3} fmt.Println(a == b ,a == c ,b == c) // "true false false" d := [3]int{1,2} // d[2] == 0 fmt.Println(a == d) // 编译错误 :无法比较 [2]int == [3]int |
数组比较 真实示例 | 举一个更有意义的例子,crypto/sha256 包里面的函数 Sum256 用来为存储在任意字节 slice 中的消息使用 SHA256 加密散列算法生成一个摘要。摘要信息为 256 位,即 [32]byte 。如果两个摘要信息相同,那么很有可能这两条原始消息就是相同的;如果这两个摘要信息不同,那么这两条原始消息就是不同的。 |
下面的程序输出并比较了 "x" 和 "X" 的 SHA256 散列值: import "crypto/sha256" func main() { c1 := sha256.Sum256([]byte("x")) c2 := sha256.Sum256([]byte("X")) fmt.Printf("%x\n%x\n%t\n%T\n", c1, c2, c1 == c2, c1) } 这两个原始消息仅有一位(bit)之差,但是它们生成的摘要消息有将近一半的位不同 注意,上面的格式化字符串 %x 表示将一个数组或者 slice 里面的字节按照十六进制的方式输出,%t 表示输出一个布尔值,%T 表示输出一个值的类型 | |
Go 函数默认 传值调用 | 当调用一个函数的时候,每个传入的参数都会创建一个副本,然后赋值给对应的函数变量(形参),所以函数接受的是一个副本,而不是原始的参数; |
函数使用传值调用的方式接收大的数组会变得很低效, 并且在函数内部对数组的任何修改都仅仅影响副本,而不是原始数组 | |
这种情况下,Go 函数把数组和其他的类型都看成 "值传递" | |
在其他语言中,数组都是隐式第使用引用传递 | |
传递指针给函数 直接修改原始数据 | 当然,也可以显式地传递一个 "数组的指针" 给函数,这样在函数内部,对数组的任何修改都会反映到原始数组上 |
数组清零程序 | 下面的程序演示如何将一个数组 [32]byte 的元素清零 func zero(ptr *[32]byte) { for i := range ptr { ptr[i] = 0 } } |
数组字面量 [32]byte{} 可以生成一个拥有 32 个字节元素的数组; 数组中每个元素的值都是字节类型的零值,即 0 | |
另一个版本的数组清零程序: func zero(ptr *[32]byte) { *ptr = [32]byte{} } | |
使用数组指针 | i. 使用数组指针是高效的; ii. 允许被调函数修改调用方数组中的元素; |
数组很少被使用 | 因为数组长度是固定的,所以数组本身是不可变的; 例如,上面的 zero 函数不能接收一个 [16]byte 这样的数组指针, 也无法为数组添加或删除元素; |
由于数组的长度不可改变,除了在特殊的情况下,很少使用数组 |
2. 切片(slice)
slice | |
slice 基本概念 | slice 表示一个拥有相同类型元素的 " 可变长度 " 的序列 |
slice 通常写成 [ ]T ,其中元素的类型是 T ; 看上去像没有长度的数组类型 | |
底层数组 | 数组和 slice 是紧密关联的 |
slice 是一种轻量级的数据结构,可以用来访问数组的部分或者全部元素; 而这个数组称为 slice 的 " 底层数组 " | |
slice 的属性 | slice 有 3 个属性 :指针 、长度 、容量 |
指针 :指向数组的第一个可以从 slice 中访问的元素 注意 :这个元素不一定是数组的第一个元素 | |
长度 :slice 中的元素个数,长度不能超过 slice 的容量 | |
容量 :通常是从 slice 的起始元素到底层数组的最后一个元素之间的元素个数 | |
len | Go 的内置函数 len 用来返回 slice 的长度 |
cap | Go 的内置函数 cap 用来返回 slice 的容量 |
一个底层数组可以对应多个 slice ,这些 slice 可以引用数组的任何位置; 这些 slice ,彼此之间的元素还可以重叠 | |
下图展示了月份名称的字符串数组和两个元素存在重叠的 slice ; | |
数组声明如下 : // 索引式的数组字面量 months := [...]string{1:"January" ,/* ... */ ,12:"December"} | |
所以 January 就是 months[1] ,December 是 months[12] ; 一般来讲,数组中索引 0 的位置存放数组的第一个元素,但由于月份是从 1 开始的, 因此我们可以不设置索引为 0 的元素,这样 months[0] 的值就为 " " | |
创建 slice | slice 操作符 s[ i : j ]( 其中, )创建了一个新的 slice |
这个新的 slice 引用了序列 中,从 到 索引位置的所有元素 | |
这里的 既可以是数组,或者指向数组的指针,也可以是其他的 slice | |
起始/结束 索引位置缺省 | 新 slice 的元素个数是 个 |
如果表达式 s[ i : j ] 中省略了 i ,则新的 slice 的起始索引位置为 0 ,即 i = 0 ; s[ : j ] 相当于 s[ 0 : j ] | |
如果表达式 s[ i : j ] 中省略了 j ,则新的 slice 的结束索引位置为 len(s) - 1 , 即 j = len(s) ; s[ i : ] 相当于 s[ i : len(s) ] | |
说明 | 因此,slice months[1:13] 引用了所有的有效月份; 同样的写法可以是 months[1:] |
slice months[ : ] 引用了整个数组 | |
接下来,我们定义元素重叠的 slice ,分别用来表示第二季度的月份,北半球的夏季月份: | |
Q2 := months[ 4 : 7 ] // 一年中的第二季度是,4 、5 、6 这三个月 summer := months[ 6 : 9 ] // 北半球的夏季是 6 、7 、8 这三个月 fmt.Println(Q2) // [ "April" "May" "June" ] fmt.Println(summer) // [ "June" "July" "August" ] | |
元素 "June" 同时被包含在两个 slice 中; 用下面的代码来输出两个 slice 中的共同元素(虽然效率不高) | |
for _,s := range summer { for _,q := range Q2 { if s == q { fmt.Printf("%s appears in both\n",s) } } } | |
越界分类 | 如果 slice 的引用超过了被引用对象的容量,即 cap(s) ,那么会导致程序宕机; |
如果 slice 的引用超过了被引用对象的长度,即 len(s) ,那么最终 slice 会比原先的 slice 长 | |
代码示例 | fmt.Println(summer[ : 20 ]) // 宕机 :超过了被引用对象的边界 endlessSummer := summer[ : 5] // 在 slice 容量范围内扩展了 slice fmt.Println(endlessSummer) // "[June July August Septmber October]" |
子串与切片 | 另外,注意 i. 求字符串(string)子串操作 ii. 对字节 slice( [ ]byte )做 slice 操作 这两者的相似性 |
相同点 | 它们都写作 x[m:n] ; 都返回原始字节的一个子序列 ; 同时两者的底层引用方式也是相同的,所以两个操作都消耗常量时间 |
不同点 | 如果 x 是字符串,那么 x[m:n] 返回的是一个字符串; 如果 x 是字节 slice ,那么 x[m:n] 返回的是一个字节 slice ; |
因为 slice 包含了指向数组元素的指针,所以将一个 slice 传递给函数的时候, 可以在函数内部修改底层数组的元素; | |
也就是说,创建一个数组的 slice ,等于为数组创建了一个别名 | |
下面的函数 reverse 就地反转了整型 slice 中的元素,它适用于任意长度的整型 slice // 就地反转一个整型 slice 中的元素 func reverse( s [ ]int ) { for i ,j := 0 ,len(s) - 1 ;i < j ;i ,j = i + 1 ,j - 1 { // i++ ,j-- s[ i ] ,s[ j ] = s[ j ] ,s[ i ] } } // 这里反转整个数组 a : a := [...]int{0,1,2,3,4,5} reverse( a[ : ] ) fmt.Println( a ) // "[ 5 4 3 2 1 0 ]" | |
将一个 slice 左移 n 个元素的简单方法 :连续调用 reverse 函数三次 第一次反转前 n 个元素,第二次反转剩下的元素,最后对整个 slice 再做一次反转 如果将 slice 右移 n 个元素,那么先做上面的第三次调用 | |
s := [ ]int{0,1,2,3,4,5} // 左移两个元素 reverse( s[ : 2 ] ) reverse( s[ 2 : ] ) reverse( s ) fmt.Println( s ) // "[ 2 3 4 5 0 1 ]" | |
注意,初始化 slice s 的表达式和初始化数组 a 的表达式,两者的区别 | |
slice 字面量 、数组字面量 ,两者很相似; 都是用逗号分隔,并用花括号 "{ }" 括起来的一个元素序列; 但是 slice 没有指定长度 | |
// 声明并初始化数组 a := [...]int{0,1,2,3,4,5} // 声明并初始化 slice s := [ ]int{0,1,2,3,4,5} | |
这种隐式区别的结果分别是创建具有固定长度的数组,创建指向数组的 slice | |
和数组一样,slice 可以按照顺序指定元素 ,也可以通过索引指定元素,或两者结合 | |
和数组不同的是,slice 无法做比较; 因此不能用 " == " 来测试两个 slice 是否拥有相同的元素(个数以及对应值都相等) | |
自行比较 slice | 标准库里面提供了高度优化的函数 bytes.Equal 来比较两个字节 slice( [ ]byte ); |
但是,对于其他类型的 slice ,我们必须自己写函数来比较两个 slice | |
func equal( x,y [ ]string ) bool { if len(x) != len(y) { // 长度不同则不同 return false } for i := range x { if x[ i ] != y[ i ] { return false } } return true } | |
这种深度比较看上去很简单,并且运行的时候并不比字符串数组使用 " == " 做比较多耗费时间,那为什么 slice 的比较不可以直接使用 " == " 操作符做比较呢? 这里有两个原因: | |
原因一 :slice 是对底层数组的引用 和数组元素不同,slice 的元素是 "非直接的" ,有可能 slice 可以包含它自身; 虽然有办法处理这种特殊的情况,但是没有一种方法是简单 、高效 、直观的 | |
原因二 : 由于 slice 的元素不是直接的,如果底层数组元素改变,则同一个 slice 在不同的时间会拥有不同的元素 由于散列表(例如 Go 中的 map 类型) 仅对元素的键做浅拷贝,这就要求,散列表里面的键,在散列表的整个生命周期内必须保持不变 因为 slice 需要深度比较,所以就不能用 slice 作为 map 的键 对于引用类型,例如指针和通道,操作符 " == " 检查的是 "引用相等性" ,即它们是否指向同一个对象; 如果有一个相似的 slice 相等性比较功能,它或许会比较有用,也能解决 slice 作为 map 键的问题,但是如果操作符 " == " 对 slice 和数组的行为不一致,会带来困扰 所以,最安全的方法就是,不允许直接比较 slice | |
比较操作 | slice 唯一允许的比较操作,是和 nil 作比较 |
if summer == nil { /* ... */ } | |
slice 零值 | slice 类型的零值是 nil |
值为 nil 的 sice 没有对应的底层数组 | |
值为 nil 的 slice ,其长度为 0 ,其容量为 0 | |
也有值不是 nil 的 slice ,其长度和容量都是零; 例如 [ ]int{ } 或 make( [ ]int ,3 )[ 3 : ] | |
对于任何类型,如果它们的值可以是 nil ,那么这个类型的 nil 值可以使用一种转换表达式,例如 [ ]int(nil) | |
var s [ ]int // len(s) == 0 ,s == nil s = nil // len(s) == 0 ,s == nil s = [ ]int( nil ) // len(s) == 0 ,s == nil s = [ ]int{ } // len(s) == 0 ,s != nil | |
空 slice | 所以,如果想检查一个 slice 是否为空,那么使用 len(s) == 0 ,而不是 s == nil ; 因为 s != nil 的情况下,slice 也有可能为空 |
除了可以和 nil 作比较之外,值为 nil 的 slice ,其行为,与其他长度为 0 的 slice 一样 | |
例如,reverse 函数调用 reverse( nil ) 也是安全的 | |
除非文档上面说明了与此相反,否则无论值是否为 nil ,Go 的函数都应该以相同的方式对待所有长度为 0 的 slice | |
内置make函数创建slice | 内置函数 make 可以创建一个具有指定 元素类型 、长度 、容量 的 slice |
其中,容量参数可以省略,则 slice 的长度和容量相等 | |
make( [ ]T,len ) make( [ ]T,len,cap ) // 和 make( [ ]T,cap )[ : len ] 功能相同 | |
深入研究下,其实 make 创建了一个无名数组,并返回了它的一个 slice ; 这个数组只能通过这个 slice 进行访问 | |
// 返回的 slice 引用了整个数组 make( [ ]T,len ) // 只引用了数组的前 len 个元素,但是 slice 的容量是数组的长度,预留了空间 make( [ ]T,len,cap ) |
2.1 append 函数
append 函数 | |
2.2 slice 就地修改
3. 字典(map)
字典(map) | |
创建 map | |
方式一 | 内置函数 make 可以用来创建一个(空)map students := make(map[int]string) // 学号到名字的映射 students["Jake"] = 31 // 添加元素 students["Mike"] = 54 // 添加元素 |
方式二 | 使用 map 的字面量来新建一个带初始化键值对的字典 students := map[int]string { 1 : "Alice", 2 : "Bob", 3 : "Charlie", 4 : "David", 5 : "Eva", 6 : "Frank", } |
方式三 | 直接创建一个新的空 map students := map[string]int{} students["Jake"] = 31 // 添加元素 students["Mike"] = 54 // 添加元素 |
添加元素 | |
students["Jake"] = 31 // 添加元素 students["Mike"] = 54 // 添加元素 如上面的操作所示: 如果键不存在,则为新增元素 如果键存在,则为修改元素 | |
访问 map 元素 | |
访问元素 | 使用 key(类似数组下标)来访问 map 中对应的 value students["Mike"] = 55 fmt.Println(students["Mike"]) |
map 使用给定的键来查找(访问)元素,如果对应的元素(键)不存在,则使用该元素(键值对),值为零值;即 value 初始化为默认值 话句话说:使用不存在的元素(通过 key 访问)相当于新增元素,value 为默认值 示例:下面的代码可以正常工作,尽管 "Bob" 还不是 map 的键,此时使用 teachers["Bob"] 的值为 0 teachers["Bob"] = teachers["Bob"] + 1 fmt.Println(teachers["Bob"]) // " 1 " 复合赋值运算(如 x += y 和 x++)对 map 中的元素同样适用 teachers["Bob"] += 1 或 teachers["Bob"]++ | |
移除元素(键值对) | |
delete 移除元素 | 可以使用内置函数 delete ,根据键,从 map 中移除一个元素(键值对) 即使键不在 map 中,delete 操作也是安全的 语法: delete(map_name,key) delete(students,"Mike") // 移除元素 students["Mike"] delete(students,"Carl") // 键 "Carl" 不存在,但这么操作是允许的 |
map 中的元素不是变量 | |
value 不是变量 | 但是 map 元素不是一个变量,不可以获取 value 的地址 错误操作如下: _ = &teachers["Bob"] // 编译错误,无法获取 map 元素的地址 |
无法获取 map 元素地址的第一个原因: map 的增长可能会导致已有元素被重新散列到新的存储位置,这样的话,之前获取的地址(可能已经存在某个变量中)与当前地址不一致(前面的地址无效) | |
遍历 map | |
使用迭代 for 循环遍历 map | 可以使用 for 循环(结合 range 关键字,迭代 for 循环)来遍历 map 中所有的键和对应的值 (就像遍历 slice 一样,range 遍历 slice ,返回下标和值;range 遍历 map ,返回键和值) 示例 :循环语句的每一次迭代,会将键赋予 name ,将值赋予 age for name,age := range teachers { fmt.Printf("%s\'s age is %d\n", name,age) } |
注意 | map 中元素的迭代顺序是不固定的,不同的实现方法会使用不同的散列算法,得到不同的元素顺序 实践中,我们认为这种顺序是随机的,从一个元素开始到后一个元素,依次执行; 这种设计是有意为之,这样可以使得程序在不同的散列算法实现下更健壮 |
按照键的顺序 有序遍历 map | 如果需要按照某种顺序来遍历 map 中的元素,必须显式地来给键排序 例如,如果键是字符串类型,可以使用 sort 包中的 Strings 函数来进行键的排序 import "sort" var names [ ]string for name := range teachers { names = append(names,name) // 先把 map 中的 Key 都保存在一个 slice 中 } sort.Strings(names) // 使保存 key 的 slice 变得有序 for _,name := range names { // 遍历有序的 slice ,依次获得 name ,再通过 name 访问 map 中的 age fmt.Printf("%s\'s age is %d\n",name,teachers[name]) } 优化: 因为一开始就知道 slice names 的长度,所以可以直接指定一个 slice 的长度,这样更高效 语法: make(容器类型,初始长度,容器容量) make(type,len,cap) 下面的语句,创建了一个初始元素为空,但容量足以容纳 map 中所有键的 slice names := make([ ]string ,0 ,len(teachers)) 第一个循环中,我们只需要 map teachers 的所有键,所以忽略了循环中的第二个变量; 第二个循环中,我们需要使用 slice names 中的元素值,所以使用空白符 _ 来忽略第一个变量,即元素索引 range 可以返回一个值,也可以返回两个值; 返回一个值:遍历序列则只返回下标,遍历字典则只返回键 返回两个值:遍历序列同时返回下标和元素,遍历字典同时返回键和值 只需要第一个值,则只返回一个值;只需要第二个值,则用 _ 来接收第一个值 m := map[int]int { 1 : 100, 2 : 200, 3 : 300, } s := make([]int, 0, len(m)) for v := range m { s = append(s, v) fmt.Printf("v is %d\n", v) } for k := range s { fmt.Printf("vkis %d\n", k) } for _, v := range s { fmt.Printf("%d : %d\n", v, m[v]) } |
map 是引用类型 | |
map 类型的零值是 nil ,也就是说,没有引用任何散列表 // 只是定义了 map 变量,未分配内存(map 是散列表的引用) // 未绑定具体的散列表,所以结果是:编译通过,运行失败 var name = "carol" var m map[string]int m[name] = 21 // 宕机:为零值 map 中的项赋值,编译通过,运行失败 fmt.Println(m == nil) // "true" // {} 即为一个空的散列表,绑定到变量 m 上 // 结果是:编译通过,运行成功 var name = "carol" m := map[string]int{} // 初始化 m[name] = 21 fmt.Println(m == nil) // "false" // make 函数根据类型 map[string]int 分配了一个空的散列表,绑定到变量 m 上 // 结果是:编译通过,运行成功 var name = "carol" m := make(map[string]int) // 初始化 m[name] = 21 fmt.Println(m == nil) // "false" fmt.Printf("%s' age is %d\n", name, m[name]) carol's age is 21 | |
大多数的 map 操作都可以安全地在 map 的零值 nil 上执行,包括查找元素,删除元素,获取 map 元素个数( len ),执行 range 循环等等,因为这和在空 map 上的行为一致 但是,向零值 map 中设置元素会导致错误 var name = "carol" var m map[string]int // 零值 map ,未初始化 m[name] = 21 // 宕机:为零值 map 中的项赋值,编译通过,运行失败 fmt.Println(m == nil) // "true" 设置元素之前,必须初始化 map | |
访问 map 注意点 | 通过下标(键)的方式访问 map 中的元素总是会有值; 如果键在 map 中,则获得键对应的值; 如果键不在 map 中,则获得 map 值类型的零值 |
判断 map 中是否存在某个元素 | |
判断元素存在 | 有时候需要知道一个元素是否在 map 中 |
例如,如果元素类型是数值类型,需要辨别一个不存在的元素,或者恰好这个元素的值是 0 age,ok := teachers["Bob"] if !ok { /* "Bob" 不是字典中的键,age == 0 */ } 合并成一条语句: if age,ok := teachers["Bob"];!ok { /* ... */ } 通过这种下标方式访问 map 中的元素输出两个值,第二个值是一个布尔值,用来报告该元素是否存在;这个布尔值一般叫作 ok ,尤其是它(ok)立即用在 if 条件判断中的时候 | |
比较 map | |
map 比较操作 | 和 slice 一样,(两个)map 不可比较 |
唯一合法的比较就是,map 变量与 nil 做比较 | |
为了判断两个 map 是否拥有相同的键和值,必须写一个循环: func equal(x,y map[string]bool) bool { if len(x) != len(y) { return false // 两个 map 长度不等,则这两个 map 不相等 } for k,xv := range x { // 某一个键 k 和键对应的值 xv // 键 k 对应的值 yv 是否存在;yv 存在的情况下,yv 与 xv 是否相等 // yv 不存在,或 yv 存在但与 xv 不相等 if yv,ok := y[k];!ok || yv != xv { // 错误写法 :xv != y[k] return false } } return true } 注意,如何使用 !ok 来区分 "元素不存在" 和 "元素存在但值为零" 的情况; 如果简单写成了 xv != y[k] ,那么下面的调用将错误地报告两个 map 相等 // 如果 equal 函数写法错误,结果为 True equal(map[string]int{"A" : 0},map[string]int{"B" : 42}) | |
使用 map 构造集合类型(Set) | |
Go 没有提供集合类型 | |
既然 map 的键都是唯一的,就可以用 map 来实现这个功能 | |
示例: 为了模拟这个功能,程序 dedup 读取一系列的行,并且只输出每个不同行一次; 程序 dedup 使用 map 的键来存储这些已经出现过的行,来确保接下来出现的相同行不会输出 func main() { seen := make(map[string]bool) // 字符串集合 input := bufio.NewScanner(os.Stdin) for input.Scan() { line := input.Text() if !seen[line] { seen[line] = true fmt.Println(line) } } if err := input.Err();err != nil { fmt.Fprintf(os.Stderr,"dedup: %v\n",err) os.Exit(1) } } | |
Go 程序员通常把这种使用 map 的方式描述成字符串集合; 但是请注意,并不是所有的 map[string]bool 都是简单的集合,有一些 map 的值会同时包含 true 和 false 的情况 | |
4. 结构体(struct)
结构体基础 | |
结构体概念 | 结构体是将零个或者多个任意类型的命名变量组合在一起的聚合数据类型 |
每个变量都叫做结构体的成员 | |
现实例子 | 在数据处理领域,结构体使用的经典实例是员工信息记录,记录中有唯一 ID 、姓名 、地址 、出生日期 、职位 、薪水 、直属领导等信息;所有的这些员工信息成员都作为一个整体组合在一个结构体中 |
结构体 整体操作 | (1). 可以复制一个结构体(变量) (2). 将结构体变量传递给函数 (3). 结构体变量作为函数的返回值 (4). 将结构体变量存储到数组中,等等 |
结构体声明 示例 | 下面的语句定义了一个叫 Employee 的结构体和一个结构体变量 dilbert : type Employee struct { ID int Name string Address string DoB time.Time Position string Salary int ManagerID int } var dilbert Employee |
访问成员 | 结构体对象的每一个成员都通过句点( . )方式进行访问 fmt.Println(dilbert.Name) 结构体对象是一个变量,其所有成员也都是变量,因此可以给结构体的成员赋值 dilbert.Salary -= 5000 // 代码量减少,降薪 |
获取成员变量的地址,然后通过指针来访问 position := &dilbert.Position *position = "Senior " + *position // 工作外包给 Elbonia ,所以升职 | |
句号( . )同样可以用在结构体指针上 var employeeOfTheMonth *Employee = &dilbert employeeOfTheMonth.Position += " (proactive team player)" 后面一条语句等价于: (*employeeOfTheMonth).Position += " (proactive team player)" | |
函数 EmployeeID 通过给定的参数 ID 返回一个指向 Employee 结构体的指针; 可以用句号( . )来访问其(结构体指针)成员变量 func EmployeeByID(id int) *Employee { /* ... */ } fmt.Println(EmployeeByID(dilbert.ManagerID).Position) id := dilbert.ID EmployeeByID(id).Salary = 0 最后一条语句更新了函数 EmplyeeByID() 返回的指针指向的结构体 Employee; 如果函数 EmployeeByID() 的返回值类型变成了 Employee 而不是 *Employee ,那么代码将无法通过编译,因为赋值表达式的左侧无法识别出一个变量 | |
结构体的成员变量,通常一行写一个,变量名称在类型的前面; 相同类型的连续成员变量可以写在一行上 type Employee struct { ID int Name,Address string DoB time.Time Position string Salary int ManagerID int } | |
成员变量(声明)的顺序对于结构体同一性(是否为同一个类型)很重要 如果将同为字符串类型的 Position 和 Name、Address 组合在一起或者互换了 Name 和 Address 的顺序,那么就是定义了一个不同的结构体类型 一般来说,我们只会组合相关的成员变量 | |
如果一个结构体的成员变量名称首字母大写,那么这个变量是可导出的(public); 这个是 Go 最主要的访问控制机制; 一个结构体可以同时包含可导出(首字母大写,public)和不可导出(首字母小写,private)的成员变量 | |
因为在结构体类型中,通常一个成员变量占据一行,所以结构体的定义比较长; 虽然可以在每次需要它(结构体完整定义)的时候写出整个结构体类型定义,即 "匿名结构体类型" ,但是重复完全没必要;所以通常我们会定义命名结构体类型,比如 Employee | |
命名结构体类型 S 不可以定义一个拥有相同结构体类型 S 的成员变量,也就是一个聚合类型不可以包含它自己(同样的限制对数组也适用) 但是 S 中可以定义一个 S 的指针类型,即 *S 这样就可以创建一些递归数据结构,比如链表和树 | |
下面的代码给出了一个利用二叉树来实现插入排序的例子 type tree struct { value int left,right *tree } // 就地排序 func Sort(values [ ]int) { var root *tree for _,v := range values { root = add(root,v) } appendValues(values[:0],root) } // appendValues 将元素按照顺序追加到 values 里面,然后返回结果 slice func appendValues(values [ ]int,t *tree) [ ]int { if t != nil { values = appendValues(values,t.left) values = append(values,t.value) values = appendValues(values,t.right) } return values } func add(t *tree,value int) *tree { if t == nil { // 等价于返回 &tree{value : value} t = new(tree) t.value = value return t } if value < t.value { t.left = add(t.left,value) } else { t.right = add(t.right,value) } } | |
结构体的零值,由结构体成员的零值组成; | |
通常情况下,我们希望零值是一个默认的、自然的、合理的值; 例如,在 bytes.Buffer 中,结构体的初始值就是一个可以直接使用的空缓存; 有时候,这种合理的初始值实现简单,但是有时候也需要类型的设计者花费时间来进行设计 | |
没有任何成员变量的结构体,称为 "空结构体" ,写做 struct{ } | |
空结构体没有长度,也不携带任何信息,但有时候会很有用 | |
有一些 Go 程序员用空结构体来替代被当作集合使用的 map 中的布尔值,来强调只有 "Key" 是有用的(不需要 value) 但由于这种方式节约内存很少且语法复杂,所以一般尽量避免这么用 | |
seen := make(map[string]struct{ }) // 字符串集合 // ... if _,ok := seen[s];!ok { seen[s] = struct{ } // ...首次出现 s... } |
4.1 结构体字面量
结构体类型的值,可以通过 "结构体字面量" 来设置,即通过设置结构体的成员变量来设置 | |
type Point struct{ X ,Y int } p := Point{ 1 ,2 } | |
结构体字面量格式 | |
格式一 | 按照正确的顺序,为每个成员变量指定一个值 type Point struct{ X ,Y int } p := Point{ 1 ,2 } // 1 是 X ,2 是 Y |
缺点: 这会给开发和阅读代码的人增加负担,因为他们必须记住每个成员变量的顺序; 这也使得未来结构体成员变量扩充或者重新排列的时候,代码维护性变差 应用: 所以,这种结构体字面量格式一般用在定义结构体类型的包中,或者一些有明显的成员变量顺序约定的小型结构体中 比如 image.Point{x,y} 或 color.RGBA{red,green,blue,alpha} | |
格式二 | 用得更多的是第二种格式,通过指定部分或者全部成员变量的名称和值,来初始化结构体变量 anim := gif.GIF{LoopCount : nframes} |
在这种初始化方式中,如果某个成员没有指定初始值,那么该成员的值就是该成员类型的零值; 因为指定了成员变量的名字,所以它们的顺序是无所谓的 | |
注意 | 两种初始化方式不可以混合使用; 不可以使用第一种初始化方式来绕过规则:不可导出变量无法在其他包中使用 |
package p type T struct{ a ,b int } // a 和 b 都是不可导出的 package q import "p" var _ = p.T{ a : 1 ,b : 2 } // 编译错误,无法引用 a 、b,小写已提示,不可用 var _ = p.T{ 1 ,2 } // 编译错误,无法引用 a 、b,小写未提示,也不可用 虽然上面的最后一行代码没有显式地提到不可导出变量,但是它们被隐式地引用了,所以这也是不允许的 | |
结构体类型的值,可以作为参数传递给函数或者作为函数的返回值 | |
下面的函数将 Point 缩放了一个比率 func Scale(p Point ,factor int) Point { return Point{p.X * factor ,p.Y * factor} } fmt.Println(Scale(Point{1,2},5)) // "{5,10}" | |
处于效率的考虑,大型的结构体,通常都使用结构体指针的方式,直接传递给函数,或者从函数中返回 (传递结构体指针给函数 、函数返回结构体指针) func Bonus(e *Employee ,percent int) int { return e.Salary * percent / 100 } | |
使用结构体指针的方式,在函数需要修改结构体内容的时候是必须的 在 Go 这种按值调用的语言中,调用的函数接收到的是实参的一个副本,并不是实参的引用(在函数内修改形参,不会影响函数外实参的状态) func AwardAnnualRaise(e *Employee) { e.Salary = e.Salary * 105 / 100 } | |
由于通常结构体都通过指针的方式使用,因此可以使用一种简单的方式来创建 、初始化一个 struct 类型的变量,并获取它的地址 pp := &Point{ 1,2 } 等价于: pp := new(Point) *pp = Point{ 1,2 } 但是 &Point{ 1,2 } 这种方式可以直接在一个表达式中使用,例如函数调用 |
4.2 结构体比较
如果结构体的所有成员变量都是可以比较的,那么这个结构体就是可比较的 | |
两个结构体可以使用 == 或者 != 进行比较 其中 == 操作符按照顺序比较两个结构体变量的成员变量 所以,下面的两个输出语句是等价的: type Point struct{ X ,Y int } p := Point{ 1,2 } q := Point{ 2,1 } fmt.Println(p.X == q.X && p.Y == q.Y) // "false" fmt.Println(p == q) // "false" | |
和其他可比较的类型一样,可比较的结构体类型都可以作为 map 的键类型 type address struct { hostname string port int } hits := make(map[address]int) hits[address{"golang.org" ,443}]++ |