Golang笔记–01–基础篇
文章目录
前言
年前把iOS开发语言迁移到Swift,年后因为业务需要抽时间学了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类型,也可以使用断言的方式进行转换