之前一直有了解 Go 语言,但没系统的学习,最近兴趣盎然生计所迫,得学一学了。整体来说 Go 语法很简洁,有其它语言基础的话学起来很快的。本文记录了一些学习笔记,加深理解。
基础语法
学语言,最重要的就是官方教程(https://go.dev/tour/list),建议不要一开始就跟着杂七杂八的教程学习。
变量
我们都知道 Go 语言是强类型的语言,所以定义变量的时候需要定义其类型,但像 C、Java,都是把类型放在前面:
1 2 3 4 5 6 | // C int i = 1; // Java int i = 1; // Go var i int = 1 |
咋一看有些别扭,但这有讲究的,比如符合英语系自然语言习惯,还有写起来就很方便,更突出变量名而不是类型(而且类型可以省略),官方博客有更多的介绍:Go's Declaration Syntax。还有,Go 和 Python一样,不需要写分号。
也还有简写的方式,因为定义变量的时候其实类型是确定的,编译器很容易推导,可以不用写类型。
1 2 3 4 5 | var i int = 1 // 去掉类型 var i = 1 // 进一步简写 i := 1 |
看到var i = 1
是不是有 JS 和 Java lombok/JDK10 的感觉,这样就很亲切了。
Methods are functions
一个函数定义:func add(x int, y int) int {}
,参数类型,返回值类型一目了然,这里可以看出还是类型后置,这点其实后面 Python 3.5 加入类型提示(限制)也是这样:def add(x: int, y: int) -> int:
。这里顺带提一下,Python、PHP 这些无类型声明的语言并不是真的无类型(动态类型,运行时检查),而且新版都增加了类型检查的语法,解决了重构等痛点问题。
方法和函数其实一样,只是接收方参数不一样。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 | type Vertex struct { X, Y float64 } // 方法 func (v Vertex) Abs() float64 { return math.Sqrt(v.X*v.X + v.Y*v.Y) } func main() { v := Vertex{3, 4} fmt.Println(v.Abs()) } // 函数 func Abs(v Vertex) float64 { return math.Sqrt(v.X*v.X + v.Y*v.Y) } func main() { v := Vertex{3, 4} fmt.Println(Abs(v)) } |
不过 Go 的函数没有 Python 的那么强大,没有默认参数、关键字参数等,虽然说 Go 讲究简洁,但这些语法糖还是很有用的,典型场景就是 Python requests 库:https://docs.python-requests.org/en/latest/api/#requests.request。
Defer
延迟调用,这个比较特殊,defer
定义的操作会在方法 return 后执行,但比如传的参数就是定义时那样,官方例子修改下:
1 2 3 4 5 6 7 8 9 10 11 | package main import "fmt" func main() { var i = 0 defer fmt.Println(i) i = 1 fmt.Println("hello") } |
输出
1 2 | hello 0 |
这个有什么作用呢,其实类似 Java 中 try/catch 中的 finally,可以用于资源的清理(经典应用就是释放锁)。官方博客有介绍:Defer, Panic, and Recover。
Pointers
Go 的指针语法上来看,比 C 简单些,没有特殊的指针变量,&
就是取地址,*
就是取值(或者定义指针类型),也没有指针运算(其实也可以)。说到指针,又会牵扯到一个话题,传值还是传引用,简单来说 Go 传的是值,但并不代表改动不影响原数据,因为 map、slice等,可以理解本身数据结构就是指针,这些类型修改会影响原有数据,表现和其它语言一致。还有一个就是性能问题,本能的觉得传引用(指针)性能更高(其实还会带来 GC 等问题),比如字符串。
1 2 3 4 5 6 7 8 9 10 11 12 | package main import "fmt" func main() { s1 := "abc" s2 := "abc" s3 := s1 s4 := &s1 fmt.Println(s1, s2, s3, *s4) fmt.Printf("%p %p %p %p", &s1, &s2, &s3, s4) } |
输出为:
1 2 | abc abc abc abc 0xc00018a050 0xc00018a060 0xc00018a070 0xc00018a050 |
咋一看 s2, s3 都是一个新的内存地址,这不影响性能嘛。但再看看:
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 | package main import "fmt" import "unsafe" import "reflect" func main() { s1 := "abc" s2 := "abc" s3 := s1 s4 := &s1 fmt.Println(s1, s2, s3, *s4) fmt.Printf("%p %p %p %p\n", &s1, &s2, &s3, s4) fmt.Printf("%p %p %p %p\n", (*reflect.StringHeader)(unsafe.Pointer(&s1)).Data, (*reflect.StringHeader)(unsafe.Pointer(&s2)).Data, (*reflect.StringHeader)(unsafe.Pointer(&s3)).Data, (*reflect.StringHeader)(unsafe.Pointer(s4)).Data ) s1 = "abcd" fmt.Printf("%p %p %p %p\n", (*reflect.StringHeader)(unsafe.Pointer(&s1)).Data, (*reflect.StringHeader)(unsafe.Pointer(&s2)).Data, (*reflect.StringHeader)(unsafe.Pointer(&s3)).Data, (*reflect.StringHeader)(unsafe.Pointer(s4)).Data ) } |
输出:
1 2 3 4 5 | abc abc abc abc 0xc000010240 0xc000010250 0xc000010260 0xc000010240 %!p(uintptr=4966888) %!p(uintptr=4966888) %!p(uintptr=4966888) %!p(uintptr=4966888) %!p(uintptr=4967058) %!p(uintptr=4966888) %!p(uintptr=4966888) %!p(uintptr=4967058) |
可以看出 Go 对字符串等有两个优化:
1. 定义变量 literals(字面量)一致时(s1 和 s2),会指向同一个地址。
2. 变量传值时(s3 := s1)复制的是 string 这个数据结构。
1 2 3 4 | struct String { char *data; int length; }; |
data 指针指向的内容没有复制。因为要存储这个新的结构体,所以 &s3 是这个结构体的内存地址,而 s1,s3 结构体中 data 指向的地址是一样的。
指针这块细节可能理解不是太准确,但意思是语言层面已经做了很多优化,正常使用即可,不要过度优化到处使用指针。 Continue Reading...