Golang笔记–01–基础篇

Overview

前言

年前把iOS开发语言迁移到Swift,年后因为业务需要抽时间学了Go,工作太忙,两个都还属于需要进一步深入学习的状态,在这里记录下一些自己碰到的常用点。主要的参考资料来自官方的三份文档:

  1. The Go Programming Language Specification
  2. FAQ
  3. Effective Go

安装、运行及打包

开发、编译环境需要安装Go运行环境,如果是运行Go可执行程序则不需要,Go可执行文件通常打包了运行时和所有相关文件,如go-bindata就是一个可以从文件生成Go代码的工具,可执行文件能直接在容器或者VPS上运行

1// macOS
2brew install go
3// Ubuntu
4add-apt-repository ppa:gophers/archive && apt-get update && apt-get install golang-1.8

安装后需要配置GOROOT,GOPATH,GOBIN路径

运行go程序和C一样需要一个包含main方法的go文件,例如hello.go,在$GOPATH/src/hello目录下新建hello.go文件,内容如下

1package main
2
3import "fmt"
4
5func main() {
6    fmt.Printf("hello, world\n")
7}

直接运行:go run hello.go

构建和运行:go build && ./hello

使用go install命令会编译可执行文件,并安装到$GOBIN目录下,使用go clean命令可以将可执行程序从$GOBIN目录清除

默认情况下,go build命令生成的可执行文件包含符号链接和调试信息,体积相对大,可以追加ldflags来移除它们:

1go build -ldflags "-s -w"

另外在构建时,使用默认的GOARCH与GOOS,如果需要为使用arm处理器的linux设备编译可执行程序,可以在构建时传入这两个环境变量:

1GOOS=linux GOARCH=arm go build -ldflags "-s -w"

Tips

字符串

替换

func Replace(s, old, new string, n int) string

采取覆盖的方式,将输入字符串中匹配old的部分替换为new,n表示替换n次,若n小于0或n大于字符串中匹配到的old个数,则执行全部替换

插入与拼接

1target := "targetStringSlice"
2join := target[:5]+"test"+target[5:]

可以像使用slice一样使用字符串,同样需要注意越界问题

搜索

func Contains(s, substr string) bool

间接调用Index,判断是或包含字符串

拆分

func SplitN(s, sep string, n int) []string

以sep为分割字符,将输入字符串最多分割为n个子字符串,n小于0时,将字符串完全分隔,与这个作用相反的是

func Join(a []string, sep string) string

以sep为分割字符,将输入的slice合并为一个字符串

格式化

func Sprintf(format string, a ...interface{}) string

字符串间可以直接使用加号拼接,但如果要格式化其他类型的变量,可以直接使用格式化输出的方式,或者转化为string后拼接

按值传递

golang与C一样,所有的数据都是按值传递的,假如一个方法的参数a为struct类型,调用方法后,会在方法内部持有一份参数a的副本,假如参数a为指向struct的指针,调用方法后,同样会在方法内部持有一份指针的副本。两种情况下,都发生了数值拷贝,直接修改副本不会影响到原始的变量

 1package main
 2
 3import "fmt"
 4
 5func main() {
 6	a := 666
 7	fmt.Println("outside", a)
 8	fuckX(&a)
 9	fmt.Println("outside", a)
10}
11
12func fuckX(x *int) {
13	fmt.Println("inner a", x)
14	z := 777
15	x = &z
16	fmt.Println("inner a", x)
17}

上面例子的输出如下,直接修改参数x存储的数据地址,并不会影响到外部变量a,只是修改副本

1outside 666
2inner a 0xc4200160b0
3inner a 0xc4200160c8
4outside 666</code></pre>

在C语言的swap函数例子中,我们知道需要向swap函数传递两个int类型的指针,这样在调用后通过指针访问,才能改变变量存储的数值,golang在这点也是一样的

对于大型的struct对象,按值传递会消耗更多资源,而使用指针传递只拷贝指针本身,另外可以获取到对象的内存地址,在函数的修改可以应用在通过指针获取到的对象上

在对象作为方法接收者的情况也是一样,不过golang自动处理了指针与数值之间的转换

 1package main
 2
 3import "fmt"
 4
 5type suck struct {
 6	a int
 7	b float64
 8}
 9
10func main() {
11	b := suck{
12		a: 64,
13		b: 233.0,
14	}
15	b.aloha()
16	c := &b
17	c.aloha()
18	fmt.Println("************")
19	b.ahola()
20	c.ahola()
21}
22
23func (a *suck) aloha() {
24	fmt.Println("call aloha", *a)
25}
26
27func (a suck) ahola() {
28	fmt.Println("call ahola", a)
29}

运行后输出如下

1call aloha {64 233}
2call aloha {64 233}
3************
4call ahola {64 233}
5call ahola {64 233}

interface

上面提到golang会自动转换数值类型与指针类型的接收者之间进行切换,但在实现interface定义的方法时,并不是这样,下面的代码摘取自A Tour of Go

1type I interface {
2	M()
3}
4
5type T struct {
6	S string
7}

先定义一个interface类型与struct类型,然后为struct的值类型实现M方法,并以数值和指针的方式调用,可以正常输出两个hello

 1func (t T) M() {
 2	fmt.Println(t.S)
 3}
 4
 5func main() {
 6	var i I = T{"hello"}
 7	i.M()
 8	var t I = &T{"hello"}
 9	t.M()
10}

但是如果为struct的指针类型实现M方法,会无法运行

 1func (t *T) M() {
 2	fmt.Println(t.S)
 3}
 4
 5func main() {
 6	var t I = &T{"hello"}
 7	t.M()
 8	var i I = T{"hello"}
 9	i.M()
10}

在使用数值类型赋值的那一行,错误信息如下

1cannot use T literal (type T) as type I in assignment:
2T does not implement I (M method has pointer receiver)

interface是一种包含一系列方法定义的引用类型,所有的类型都满足空的interface类型(即没有定义任何方法的interface),假如一个struct类型TA实现了interface类型TB定义的所有方法,则TA的数值类型与指针类型都满足TB,可以将类型TA的一个实例a赋予TB类型的变量b。

那为什么在之前的例子中,数值类型可以调用接收者为指针类型的方法,而在这里却不行呢?

方法集

不知道翻译成方法集是否合适,但英文原文是Method Sets,官方文档在这里:Method_sets

如文档里所说,一个interface类型的方法集为该类型定义的所有方法的集合,一个T类型的方法集为所有接收者为T类型的方法的集合,而一个T类型的方法集,包含所有接受者为T与T类型的方法的集合。一个类型的方法集决定了该类型实现的接口,以及该类型可以调用的所有方法。

这就解释了上面的例子中*T类型可以调用T类型的所有方法,而数值类型在哪种情况下可以调用指针类型的方法?

StackOverflow上有一个回答:Golang Method Sets (Pointer vs Value Receiver)

在官方文档的这个部分有解释:Address_operators

前面提到golang自动处理了指针类型与数值类型接收者之间的转换,对于可寻址的操作数(addressable operand),数值类型调用接收者为指针类型的方法时,golang会自动将c.aloha()的调用转化为&(c).aloha(),关于可寻址的操作数,文档里给出的解释也并不是很清晰,当操作数是一个变量,指向指针的指针,可寻址的结构体操作数的字段,切片的索引,指向可寻址数组的数组索引等情况,即它是可寻址的。或者说更明白点,可以在代码中显示获取到操作数的实际内存地址,这时的数值类型是可以获取到对应的指针类型的内存地址,那么就可以调用相应的方法,将数值类型赋予interface后,由于无法直接寻址,因此会提示该类型为声明指定的方法。

确实真是有一点绕,StackOverflow上回答也是说,对于一个实现了interface定义且接收者为指针的方法,只有指针类型可以赋值给该类型的变量,值类型是无效的。

切片与数组(slice and array)

切片与数组是完全不同的

数组具由固定长度和元素类型确定,例如[6]int类型与[7]int类型是两种不同的类型

切片是一个指向数组的索引(指针),一个切片包含元素类型、length、capacity三个参数,length表示切片现有的元素个数,capacity表示切片指向的数组可以容纳的元素个数,length小于等于capacity

golang明确了数组作为参数传递时,是按值传递的,如果需要传递一个长的数组时,可以先转换为切片,切片是按引用传递,避免不必要的复制

1a := [6]int{1,2,3,4,5,6} //声明一个长度为6,元素类型为int的数组
2b := []int //声明一个元素类型为int的空的切片
3c := a[:] //声明一个包含a所有元素的切片
4b := append(b,c...) //将c中的所有元素追加到b

类型转换与类型断言

基本数据类型的转换,如int与float互转

1func main() {
2	a, b := 3, 4
3	var c float64 = float64(a*b)
4	var d int = int(c*22.0)
5	fmt.Println(a, b, c, d)
6}

数值转字符串时,可以使用fmt的Sprintf方法格式化输出字符串,也可以使用strconv包中的Format开头的方法,直接从数值类型转换为string

字符串转数值,使用strconv包中的Parse开头的方法,扫描字符串,生成对应的数值类型

而从interface类型转换为其他类型时,无法保证转换成功,直接转换失败时,会出现panic,需要使用类型断言的方式,检查转换是否成功,下面是官方教程中的示例代码

 1func main() {
 2var i interface{} = "hello"
 3
 4s := i.(string)
 5fmt.Println(s)
 6
 7s, ok := i.(string)
 8fmt.Println(s, ok)
 9
10f, ok := i.(float64)
11fmt.Println(f, ok)
12
13f = i.(float64) // panic
14fmt.Println(f)
15}

对于具有相同数据结构的struct类型,也可以使用断言的方式进行转换