Go语言学习笔记

背景

这两年go语言比较火,尤其是在服务器并发方面表现很好。一直想学一下,但是拖到现在。最近正好有这个机会,把go语言算是入了个门吧。

概括

学了两周,总体感觉如下:

  1. 和C++在一些语法有点像,但少了很多东西,比如说分号,for循环以及if语句的括号(在go语言中是可有可无的)
  2. 没有了引用类型,都使用指针替代,指针获取变量也是采用点分的形式
  3. 强类型语言,类型有点强到吐血,比C++还强
  4. 不支持函数重载、函数默认参数
  5. 编译限制太多,有未被使用的变量或者导入了未使用的库,都会编译失败
  6. 简化了异常处理,只有panicdefer
  7. 缩进,默认的8格缩进真是挺难看的
  8. go语言的资源在天朝是被墙着的

一些坑

这里记录的一些坑,都是我在使用的过程中遇到过的已经解决的或者是没有完美解决的。

1.array类型与slice类型

使用一门语言的时候数据结构肯定是一个必修课题,其中的数组又是所有语言中最基础的数据结构。
对于以下这两个类型[]int[3]int起初我以为是一样的,于是写了以下这样的代码:

1
2
3
4
5
// package, imports 略
// error msg: cannot use [3]int literal (type [3]int) as type []int in return argument
func return_array_but_slice_needed() []int {
return [3]int{1, 2, 3}
}

结果可想而知,编译错误,原来在go中数组有两种(准确的说是无穷多种,因为[2]int[3]int也是不同的类型),分别是array和slice。可以从C++的角度去理解:

  1. Go中的var array [3]int 相当于是C++中的int array[3],数组的长度是定长的,不能扩展,但能修改数组中的值。
  2. Go中的var slice []int 相当于是C++中的std::vector<int> slice,数组长度可伸缩,可修改,可索引。

值得一提的是slice的内存分配和C++的vector也很相似,它有长度和容量两个概念,容量总是大于长度的。对于slice作append操作时,如果还有剩余容量,则直接附加,否则重新开辟一块长度是2倍的内存(如果slice本身数据特别多,可能不到2倍),把原来数据复制进去,再作附加操作。

其实slice是一个包含了array指针的数据结构,还有长度,容量这两个属性。slice赋值给另外一个slice的时候,不同的仅仅是两个slice对象在内存中的位置,而它们指向的array是相同的,仔细体会一下下面这个例子就可以理解,注释中写得很明确。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
func main(){
a := make([]int, 3, 6)
var b = a // 赋值,但a,b底层所指的数组是同一份,在堆中

b[0] = 1 // 修改b中的值同时也会修改a中的值
fmt.Printf("%p\n", &a) // 0x820246000
fmt.Printf("%p\n", &b) // 0x820246020
fmt.Println(a) // [1 0 0]
fmt.Println(b) // [1 0 0]

b = append(b, 4) // 把4填到剩下三个空格中的第一格,并把b的长度设为4
b[1] = 2
fmt.Printf("%p\n", &a) // 0x820246000
fmt.Printf("%p\n", &b) // 0x820246020
fmt.Println(a) // [1 2 0] 其实和b是同一份数据,因为a的长度还是3,所以只输出了3个
fmt.Println(b) // [1 2 0 4]

a = append(a, 5) // a的长度变成4, 值填到三个空格中的第一格(会覆盖掉b中的第四个数字)
a = append(a, 6) // a的长度变成5, 值填到剩下两个空格的第一格
a = append(a, 7, 8) // 超过了a的容量,重新分配a,至此,a和b已经不会再互相影响
fmt.Printf("%p\n", &a) // 0x820246000 a对象中的array指针地址已变,指向了更长的数组,但a变量本身并不会更改位置
fmt.Printf("%p\n", &b) // 0x820246020
fmt.Println(a) // [1 2 0 5 6 7 8]
fmt.Println(b) // [1 2 0 5]

var c = b[2: 6] // b的第三个数字到第六个数字作为slice分配给c,b和c共享一段内存
fmt.Println(c) // [0 5 6 0] 虽然b没有显示这个6,因为b的长度没到那里,但这个6确实在那段内存,由c打印出来
}

2.安装外部依赖包

在编译go的时候总是会被它严格的编译限制弄得很头疼,比如我在调试的时候需要fmt这个库来打印一些东西,有时会注释某个打印,此时又不得不注释import "fmt",等到再打印的时候,又要把那个注释释放出来,很麻烦。stackoverflow上的这个链接跟我是同样的烦恼。答案中推荐了goimports这样一个工具,编译的时候会自动去掉不需要的依赖,使用下来还觉得挺不错的,但安装的过程确实感觉稍微有点坑。

官网说安装需要执行这样的命令:

1
go get golang.org/x/tools/cmd/goimports

但是http://golang.org在中国是被墙了的,当然可以从中国的某些镜像下载,但是还需要手动改一些文件夹的名字,很麻烦。因为`go get`命令,支持从官网的库中下载,也支持从github下载,所以我决定从github下载。
但不管我采用以下的哪种,都无法下载:

1
2
go get https://github.com/bradfitz/goimports.git
go get git@github.com:bradfitz/goimports.git

一通乱试,终于成功了:

1
go get github.com/bradfitz/goimports

在看了GOPATH目录的文件结构后我大概终于终于明白了这种个形式的意义,因为命令中的路径其实也表达了文件夹中的路径,不管是官网的命令还是最后我采用的命令,它会下载到对应的文件夹:

1
2
3
4
# 执行官网的命令,会下载到以下路径
$GOPATH/golang.org/x/tools/cmd/goimports
# github下载会下载到以下路径
$GOPATH/github.com/bradfitz/goimports

3.如何实现泛型

Go语言是强类型语言,简直强到不能再强,不能像javascript或者python那样随心所欲地传值,又不能像C++中那样做函数重载。所以如何实现一个泛型的接口在go语言中是一个较为麻烦的事情。
好在Go语言并没有赶尽杀绝,他还有interface{}这样一个类型的参数,可以接受所有类型的其它参数。只不过有这样参数的接口,如果需要对类型作判断并且执行相应的操作的话还需要reflect库的帮助。这个库很强大,接口也有很多,有时间有精力的话推荐读一下它的源码。

interface{}的泛型能力还是很强的,他可以接受所有类型的参数,刚开始我还小瞧了它,写出了这样的代码:

1
2
3
4
// cannot use a (type []int) as type []interface {} in argument to array_of_interface
func array_of_interface(ai []interface{}){
fmt.Println(ai)
}

结果编译出错。其实参数不需要定义成slice,直接i interface{}就够了。如果在接口中需要判断类型,我推荐一个库的源码encoding/json,这个库就是go语言中对json数据的序列化反序列化操作,可想而知,它需要实现大量的泛型操作(也是采用了reflect库),贴一点关键部分的代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
// newTypeEncoder constructs an encoderFunc for a type.
// The returned encoder only checks CanAddr when allowAddr is true.
func newTypeEncoder(t reflect.Type, allowAddr bool) encoderFunc {
// 一部分代码略去,有兴趣的读者可以直接去阅读源码
switch t.Kind() {
case reflect.Bool:
return boolEncoder
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
return intEncoder
case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Uintptr:
return uintEncoder
case reflect.Float32:
return float32Encoder
case reflect.Float64:
return float64Encoder
case reflect.String:
return stringEncoder
case reflect.Interface:
return interfaceEncoder
case reflect.Struct:
return newStructEncoder(t)
case reflect.Map:
return newMapEncoder(t)
case reflect.Slice:
return newSliceEncoder(t)
case reflect.Array:
return newArrayEncoder(t)
case reflect.Ptr:
return newPtrEncoder(t)
default:
return unsupportedTypeEncoder
}
}

4.其它

待补充:
nil之坑
pointer receiver VS value receiver

有用的链接

当然,官方文档是非常棒的,基本上所有的问题在官网文档或者github项目的wiki中都有多提到,如果提前能够把这里的一些注意事项以及best practise看完,会非常有帮助的。只是内容比较多,另外自己动手操作以后再看文档的效果会比在还没有足够了解的情况下看文档好很多。

下面这些链接是我在学习过程中个人觉得挺有帮助的一些问答或博客,也一起分享出来吧。

  1. https://segmentfault.com/q/1010000003505053
  2. http://stackoverflow.com/questions/14025833/range-over-interface-which-stores-a-slice
  3. http://stackoverflow.com/questions/19560334/how-to-disable-golang-unused-import-error
  4. http://stackoverflow.com/questions/24878737/what-is-the-difference-between-go-get-and-go-install
  5. http://stackoverflow.com/questions/23542989/pointers-vs-values-in-parameters-and-return-values/23551970#23551970
  6. http://stackoverflow.com/questions/25025409/delete-element-in-a-slice