Go 中的名称

Go 中函数、变量、常量、类型、语句标签和包的名称遵循一个简单的规则:名称的开头是一个字母(Unicode 中的字符即可)或下划线,后面可以跟任意数量的字符、数字和下划线,并区分大小写。

关键字

共25个关键字,只能用在语法允许的地方,不能作为名称:

break //退出循环default //选择结构默认项(switch、select)func //定义函数interface //定义接口select //channelcase //选择结构标签chan //定义channelconst //常量continue //跳过本次循环defer //延迟执行内容(收尾工作)go //并发执行map //map类型struct //定义结构体else //选择结构goto //跳转语句package //包switch //选择结构fallthrough //switch里继续检查后面的分支if //选择结构range //从slice、map等结构中取元素type //定义类型for //循环import //导入包return //返回var //定义变量内置预声明

内置的预声明的常量、类型和函数:

常量true、falseiotanil类型int、int8、int16、int32、int64uint、uint8、uint16、uint32、uint64、uintptrfloat32、float64、complex128、complex64bool、byte、rune、string、error函数make、len、cap、new、append、copy、close、deletecomplex、real、imag : 复数相关panic、recover

这些名称不是预留的,可以在声明中使用它们。也可能会看到对其中的名称进行重声明,但是要知道这会有冲突的风险。

命名规则

单词组合时,使用驼峰式。如果是缩写,比如:ASCII或HTML,要么全大写,要么全小写。比如组合 html 和 escape,可以是下面几种写法:

htmlEscapeHTMLEscapeEscapeHTML

但是不推荐这样的写法:

Escapehtml : 这样完全区分不了html是一个词,所以这样HTML要全大写EscapeHtml : 这样虽然能区分,但是违反了全大写或全小写的建议基础数据类型

Go的数据类型分四大类:

基础类型(basic type)数字(number)字符串(string)布尔型(boolean)聚合类型(aggregate type)数组(array)结构体(struct)引用类型(reference type)指针(pointer)切片(slice)散列表(map)函数(function)通道(channel)接口类型(interface type)整数

二元操作符
二元操作符分五大优先级,按优先级降序排列:

* / % << >> & &^+ - | ^== != < <= > >=&&||

位运算符
位运算符:

符号说明集合&AND交集|OR并集^XOR对称差&^位清空(AND NOT)差集<<左移N/A>>右移N/A

位清空,表达式 z=x&^y ,把y中是1的位在x里对应的那个位,置0。
差集,就是集合x去掉集合y中的元素之后的集合。对称差则是再加上集合y去掉集合x中的元素的集合,就是前后两个集合互相求差集,之后再并集。

布尔值

逻辑运算符
逻辑运算符 &&(AND) 以及 ||(OR) 的运算可能引起短路行为:如果运算符左边的操作数已经能够直接确定总体结果,则右边的操作数不会做计算。
关于优先级,&& 较 || 优先级更高,这里有一个方便记忆的窍门。&& 表示逻辑乘法,|| 表示逻辑加法,这不仅仅指优先级,计算结果也很相似。

布尔转数值
布尔值无法隐式转换成数值,反之也不行。如果需要把布尔值转成0或1,需要显示的使用if:

i := 0if b { i = 1}

如果转换操作使用频繁,值得专门写成一个函数:

func btoi(b bool) int { if b { return 1 } return 0}func itob(i int) bool { return i != 0}

反向转换比较简单,所以无需专门写成函数了。不过为了与btoi对应,上面也写了一个。

字符串和字节切片(bytes包)

字节切片 []byte 类型,其某些属性和字符串相同。但是由于字符串不可变,因此按增量方式构建字符串会导致多次内存分配和复制。这种情况下,使用 bytes.Buffer 类型更高效。
bytes 包为高效处理字节切片提供了 Buffer 类型。Buffer 初始值为空,其大小随着各种类型数据的写入而增长,如 string、byte 和 []byte。bytes.Buffer 变量无须初始化,其零值有意义:

package mainimport ( "bytes" "fmt")// 函数 intsToString 与 fmt.Sprintf(values) 类似,但插入了逗号func intsToString(values []int) string { var buf bytes.Buffer buf.WriteByte('[') for i, v := range values { if i > 0 { buf.WriteString(", ") } fmt.Fprintf(&buf, "%d", v) } buf.WriteByte(']') return buf.String()}func main() { fmt.Println(intsToString([]int{1, 2, 3})) // "[1, 2, 3]" fmt.Println([]int{1, 2, 3}) // "[1 2 3]"}复合数据类型

有四种复合数据类型:

数组切片(slice)map结构体切片(slice)

反转和平移
就地反转slice中的元素:

package mainimport "fmt"func reverse(s []int) { for i, j := 0, len(s)-1; i < j; i, j = i+1, j-1 { s[i], s[j] = s[j], s[i] }}func main() { l := [...]int{1, 2, 3, 4, 5} // 这个是数组 fmt.Println(l) reverse(l[:]) // 传入切片 fmt.Println(l)}

将一个切片向左平移n个元素的简单方法是连续调用三次反转函数。第一次反转前n个元素,第二次返回剩下的元素,最后整体做一次反转:

func moveLeft(n int, s []int) { reverse(s[:n]) reverse(s[n:]) reverse(s)}func moveRight(n int, s []int) { reverse(s[n:]) reverse(s[:n]) reverse(s)}

切片的比较
与数组不同,切片无法做比较。标准库中提供了高度优化的函数 bytes.Equal 来比较两个字节切片([]byte)。但是对其他类型的切片,Go不支持比较。当然自己写一个比较的函数也不难:

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}

上面的方法也只是返回执行函数当时的结果,但是切片的底层数组可以能发生改变,在不同的时间切片所拥有的元素可能不同,不能保证整个生命周期都保持不变。总之,Go不允许直接比较切片。

初始化
像切片和map这类引用类型,使用前是需要初始化的。仅仅进行声明,是不分配内存的,此时值为nil。
完成初始化后(大括号或者make函数),此时就是已经完成了初始化,分配内存空间,值不为nil。

和nil比较
切片唯一允许的比较操作是和nil做比较。值为nil的切片长度和容量都是零,但是也有非nil的切片长度和容量也都是零的:

func main() { var s []int fmt.Println(s == nil) // true s = nil fmt.Println(s == nil) // true s = []int(nil) fmt.Println(s == nil) // true s = []int{} fmt.Println(s == nil) // flase}

所以要检查一个切片是否为空,应该使用 len(s) == 0,而不是和nil做比较。
另外,值为nil的切片其表现和其它长度为零的切片是一样的。无论值是否为nil,GO的函数都应该以相同的方式对待所有长度为零的切片。

map

引用类型
因为map类型是间接的指向它的 key/value 对,所以函数或方法对引用本身做的任何改变,比如设置值为 nil 或者使它指向一个不同的 map,都不会在调用者身上产生作用:

package mainimport "fmt"type map1 map[string]stringfunc change(m map1) { fmt.Println("change:", m) // change: map[k1:v1] m = map1{"k1": "v2"} // 将m指向一个新的map,但是并不会改变main中m1的值 fmt.Println("change:", m) // change: map[k1:v2]}func main() { m1 := map1{"k1": "v1"} fmt.Println("main:", m1) // main: map[k1:v1] change(m1) // m1 的值不会改变 fmt.Println("main", m1) // main map[k1:v1]}

main函数中创建了m1,然后把m1传递给change函数,引用类型传的是存储了m1的内存地址的副本。在change中修改m的值,指向了一个新创建的map,此时m就指向了新创建的map的内存地址。回到main函数中m1指向的内存地址并没有改变,而该地址对应的map的内容也没有改变。
下面这个函数,main函数中原来的map是会改变的。main函数中map的指向的地址没有变,但是地址对应的数据发生了变化:

func changeKeyValue(m map1, k, v string) { fmt.Println("change:", m) m[k] = v fmt.Println("change:", m)}

使用切片做key
切片是不能作为key的,并且切片是不可比较的,不过可以有一个间接的方法来实现切片作key。定义一个帮助函数k,将每一个key都映射到字符串:

var m = make(map[string]int)func k(list []string) string { fmt.Sprint("%q", list) }func Add(list []string) { m[k(list)]++ }func Count(list []string) int { return m[k(list)] }

这里使用%q来格式化切片,就是包含双引号的字符串,所以(["ab", "cd"] 和 ["abcd"])是不一样的。就是,当且仅当 x 和 y 相等的时候,才认为 k(x)==k(y)。
同样的方法适用于任何不可直接比较的key类型,不仅仅局限于切片。同样,k(x) 的类型不一定是字符串类型,任何能够得到想要的比较结果的可比较类型都可以。

集合
Go 没有提供集合类型,但是利用key唯一的特点,可以用map来实现这个功能。比如说字符串的集合:

package mainimport ( "bufio" "fmt" "os")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("Set:", line) } } if err := input.Err(); err != nil { fmt.Fprintf(os.Stderr, "dedup: %v\n", err) os.Exit(1) }}

从标准输出获取字符串,用map来存储已经出现过的行,只有首次出现的字符串才会打印出来。

使用空结构体作value
上面的集合中使用bool来作为map的value,而bool也有true和false两种值,而实际只使用了1种值。
这里还可以使用空结构体(类型:struct{}、值:struct{}{})。空结构体,没有长度,也不携带任何信息,用它可能是最合适的。但由于这种方式节约的内存很少并且语法复杂,所以一般尽量避免这样使用。

位向量集合

Go 语言的集合通常使用 map[T]bool 来实现,其中T是元素类型。使用 map 的集合扩展性良好,但是对于一些特定的问题,一个专门设计过的集合性能会更优。比如,在数据流分析领域,集合元素都是小的非负整型,集合拥有许多元素,而且集合的操作多数是求并集和交集,位向量是个理想的数据结构。

基础类型和方法

位向量使用一个无符号整型值的切片,每一位代表集合中的一个元素。如果设置第 i 位的元素,则表示集合包含 i。下面是一个包含了三个方法的简单位向量类型:

package intsetimport ( "bytes" "fmt")// 这是一个包含非负整数的集合// 零值代表空的集合type IntSet struct { words []uint64}// 集合中是否存在非负整数xfunc (s *IntSet) Has(x int) bool { word, bit := x/64, uint(x%64) return word < len(s.words) && s.words[word]&(1<<bit) != 0}// 添加一个数x到集合中func (s *IntSet) Add(x int) { word, bit := x/64, uint(x%64) for word >= len(s.words) { s.words = append(s.words, 0) } s.words[word] |= 1 << bit}// 求并集,并保存到s中func (s *IntSet) UnionWith(t *IntSet) { for i, tword := range t.words { if i < len(s.words) { s.words[i] |= tword } else { s.words = append(s.words, tword) } }}// 以字符串"{1 2 3}"的形式返回集合func (s *IntSet) String() string { var buf bytes.Buffer buf.WriteByte('{') for i, word := range s.words { if word == 0 { continue } for j := 0; j < 64; j++ { if word&(1<<uint(j)) != 0 { if buf.Len() > len("{") { buf.WriteByte(' ') } fmt.Fprintf(&buf, "%d", 64*i+j) } } } buf.WriteByte('}') return buf.String()}

每一个 word 有64位,为了定位第 x 位的位置,通过 x/64 结果取整,就是 word 的索引,而 x%64 取模运算是 word 内位的索引。
这里还自定义了以字符串输出 IntSet 的方法,就是一个 String 方法。在 String 方法中 bytes.Buffer 经常以这样的方式用到。
因为 Add 方法和 UnionWith 方法需要对 s.word 进行赋值,所以需要用到指针。所以该类型的其他方法也都使用了指针,就是 Has 方法和 String 方法是不需要使用指针的,但是为了保持一致,就都使用指针作为方法的接收者。

并集、交集、差集、对称差

上面只给了并集的示例,这里提到的4种集合的计算,简单参考一下前面的“位运算符”的介绍,很简单的通过修改一下位运算的符号就能实现了。
并集和对称差,需要把s.words中没有的而t.words中多的那些元素全部加进来。而交集和差集,直接无视这部分元素就好了:

// 并集 Union,上面的示例中已经有了func (s *IntSet) UnionWith(t *IntSet) { for i, tword := range t.words { if i < len(s.words) { s.words[i] |= tword } else { s.words = append(s.words, tword) } }}// 交集 Intersectionfunc (s *IntSet) IntersectionWith(t *IntSet) { for i, tword := range t.words { if i < len(s.words) { s.words[i] &= tword } }}// 差集 Differencefunc (s *IntSet) DifferenceWith(t *IntSet) { for i, tword := range t.words { if i < len(s.words) { s.words[i] &^= tword } }}// 对称差 SymmetricDifferencefunc (s *IntSet) SymmetricDifferenceWith(t *IntSet) { for i, tword := range t.words { if i < len(s.words) { s.words[i] ^= tword } else { s.words = append(s.words, tword) } }}

把这里的三个新的方法添加到最初定义的包中就可以使用。

计算置位个数

就是统计集合中元素的总数,下面分别讲3种实现的算法:

查表法:空间换时间。右移循环算法:最简单,最容易想到。快速法:如果输入整数中“1”远小于“0”(稀疏),可以通过一些针对性算法来提高效率。

查表法
先使用 init 函数来针对每一个可能的8位值预计算一个结果表 pc,这样之后只需要将每次快查表的结果相加而不用进行一步步的计算:

// pc[i] 是 i 的 population countvar pc [256]bytefunc init() { for i := range pc { pc[i] = pc[i/2] + byte(i&1) }}// 返回元素个数,查表法func (s *IntSet) Len() int { var counts int for _, word := range s.words { counts += int(pc[byte(word>>(0*8))]) counts += int(pc[byte(word>>(1*8))]) counts += int(pc[byte(word>>(2*8))]) counts += int(pc[byte(word>>(3*8))]) counts += int(pc[byte(word>>(4*8))]) counts += int(pc[byte(word>>(5*8))]) counts += int(pc[byte(word>>(6*8))]) counts += int(pc[byte(word>>(7*8))]) } return counts}

右移循环算法
在其实际参数的位上执行移位操作,每次判断最右边的位,进而实现统计功能:

// 返回元素个数,右移循环算法func (s *IntSet) Len2() int { var count int for _, x := range s.words { for x != 0 { if x & 1 == 1 { count++ } x >>= 1 } } return count}

快速法:
使用 x&(x-1) 可以清除x最右边的非零位,不停地进行这个运算直到数值变成0。其中进行了几次运行就表示有几个1了:

// 返回元素个数,快速法func (s *IntSet) Len3() int { var count int for _, x := range s.words { for x != 0 { x = x & (x - 1) count++ } } return count}添加其他方法

继续为我们的位向量类型添加其他方法:

// 一次添加多个元素func (s *IntSet) AddAll(nums ...int) { for _, x := range nums { s.Add(x) }}// 移除元素,无论是否在集合中,都把该位置置0func (s *IntSet) Remove(x int) { word, bit := x/bitCounts, uint(x%bitCounts) if word < len(s.words) { s.words[word] &^= 1 << bit } // 移除高位全零的元素 for i := len(s.words)-1; i >=0; i-- { if s.words[i] == 0 { s.words = s.words[:i] } else { break } }}// 删除所有元素func (s *IntSet) Clear() { *s = IntSet{}}// 返回集合的副本func (s *IntSet) Copy() *IntSet { x := IntSet{words: make([]uint, len(s.words))} copy(x.words, s.words) return &x}// 返回包含集合元素的 slice,这适合在 range 循环中使用func (s *IntSet) Elems() []int { var ret []int for i, word := range s.words { if word == 0 { continue } for j := 0; j < bitCounts; j++ { if word&(1<<uint(j)) != 0 { ret = append(ret, bitCounts*i+j) } } } return ret}自适应32或64位平台

这里每个字的类型都是 uint64,但是64位的计算在32位的平台上的效率不高。使用 uint 类型,这是适合平台的无符号整型。除以64的操作可以使用一个常量来代表32位或64位。
这里有一个讨巧的表达式: 32<<(^uint(0)>>63) 。在不同的平台上计算的结果就是32或64。

const bitCounts = 32 << (^uint(0) >> 63) // 使用这个常量去做取模和取余的计算

对应的要把代码中原本直接使用数字常量64的地方替换成这个常量,比如 Has 方法:

const bitCounts = 32 << (^uint(0) >> 63) // 32位平台这个值就是32,64位平台这个值就是64// 集合中是否存在非负整数xfunc (s *IntSet) Has(x int) bool { word, bit := x/bitCounts, uint(x%bitCounts) return word < len(s.words) && s.words[word]&(1<<bit) != 0}