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的数据类型分四大类:
- 基础类型(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 := 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”远小于“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
}
有疑问加站长微信联系(非本文作者)