Go 中的名称

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

关键字

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

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

内置预声明

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

  • 常量
    • true、false
    • iota
    • nil
  • 类型
    • int、int8、int16、int32、int64
    • uint、uint8、uint16、uint32、uint64、uintptr
    • float32、float64、complex128、complex64
    • bool、byte、rune、string、error
  • 函数
    • make、len、cap、new、append、copy、close、delete
    • complex、real、imag : 复数相关
    • panic、recover

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

命名规则

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

  • htmlEscape
  • HTMLEscape
  • EscapeHTML

但是不推荐这样的写法:

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

基础数据类型

Go的数据类型分四大类:

  1. 基础类型(basic type)
    • 数字(number)
    • 字符串(string)
    • 布尔型(boolean)
  2. 聚合类型(aggregate type)
    • 数组(array)
    • 结构体(struct)
  3. 引用类型(reference type)
    • 指针(pointer)
    • 切片(slice)
    • 散列表(map)
    • 函数(function)
    • 通道(channel)
  4. 接口类型(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 := 0
if 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 main

import (
	"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 main

import "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 main

import "fmt"

type map1 map[string]string

func 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 main

import (
	"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 intset

import (
	"bytes"
	"fmt"
)

// 这是一个包含非负整数的集合
// 零值代表空的集合
type IntSet struct {
	words []uint64
}

// 集合中是否存在非负整数x
func (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)
		}
	}
}

// 交集 Intersection
func (s *IntSet) IntersectionWith(t *IntSet) {
	for i, tword := range t.words {
		if i < len(s.words) {
			s.words[i] &= tword
		}
	}
}

// 差集 Difference
func (s *IntSet) DifferenceWith(t *IntSet) {
	for i, tword := range t.words {
		if i < len(s.words) {
			s.words[i] &^= tword
		}
	}
}

// 对称差 SymmetricDifference
func (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. 查表法:空间换时间。
  2. 右移循环算法:最简单,最容易想到。
  3. 快速法:如果输入整数中“1”远小于“0”(稀疏),可以通过一些针对性算法来提高效率。

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

// pc[i] 是 i 的 population count
var pc [256]byte

func 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)
	}
}

// 移除元素,无论是否在集合中,都把该位置置0
func (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

// 集合中是否存在非负整数x
func (s *IntSet) Has(x int) bool {
	word, bit := x/bitCounts, uint(x%bitCounts)
	return word < len(s.words) && s.words[word]&(1<<bit) != 0
}