前言

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

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

安装、运行及打包

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

1
2
3
4
// macOS
brew install go
// Ubuntu
add-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文件,内容如下

package main

import "fmt"

func main() {
    fmt.Printf("hello, world\n")
}

直接运行:go run hello.go

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

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

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

1
go build -ldflags "-s -w"

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

1
GOOS=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个数,则执行全部替换

插入与拼接

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

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

搜索

func Contains(s, substr string) bool

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

拆分

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

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
<div>
  以sep为分割字符,将输入字符串最多分割为n个子字符串,n小于0时,将字符串完全分隔,与这个作用相反的是
</div>

<div>
  <div>
    <strong>func Join(a []string, sep string) string</strong>
  </div>

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

格式化

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

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

按值传递

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

package main

import “fmt”

func main() { a := 666 fmt.Println(“outside”, a) fuckX(&a) fmt.Println(“outside”, a) }

func fuckX(x *int) { fmt.Println(“inner a”, x) z := 777 x = &z fmt.Println(“inner a”, x) }

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

outside 666
inner a 0xc4200160b0
inner a 0xc4200160c8
outside 666

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

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

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

package main

import “fmt”

type suck struct { a int b float64 }

func main() { b := suck{ a: 64, b: 233.0, } b.aloha() c := &b c.aloha() fmt.Println(”************“) b.ahola() c.ahola() }

func (a *suck) aloha() { fmt.Println(“call aloha”, *a) }

func (a suck) ahola() { fmt.Println(“call ahola”, a) }

运行后输出如下

call aloha {64 233}
call aloha {64 233}


call ahola {64 233} call ahola {64 233}

interface

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

type I interface {
    M()
}

type T struct { S string }

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

func (t T) M() {
    fmt.Println(t.S)
}

func main() { var i I = T{“hello”} i.M() var t I = &T{“hello”} t.M() }

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

func (t *T) M() {
    fmt.Println(t.S)
}

func main() { var t I = &T{“hello”} t.M() var i I = T{“hello”} i.M() }

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

cannot use T literal (type T) as type I in assignment:
T 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()的调用转化为&©.aloha(),关于可寻址的操作数,文档里给出的解释也并不是很清晰,当操作数是一个变量,指向指针的指针,可寻址的结构体操作数的字段,切片的索引,指向可寻址数组的数组索引等情况,即它是可寻址的。或者说更明白点,可以在代码中显示获取到操作数的实际内存地址,这时的数值类型是可以获取到对应的指针类型的内存地址,那么就可以调用相应的方法,将数值类型赋予interface后,由于无法直接寻址,因此会提示该类型为声明指定的方法。

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

切片与数组(slice and array)

切片与数组是完全不同的

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

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

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

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

类型转换与类型断言

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

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

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

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

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

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
func main() {
    var i interface{} = "hello"

    s := i.(string)
    fmt.Println(s)

    s, ok := i.(string)
    fmt.Println(s, ok)

    f, ok := i.(float64)
    fmt.Println(f, ok)

    f = i.(float64) // panic
    fmt.Println(f)
}

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