Go程序设计语言读书笔记

警告
本文最后更新于 2021-05-24,文中内容可能已过时。

重新系统学习 Golang。之前学习Golang都是用什么学什么,不系统,不全面,很多知识点一知半解。这次通过阅读 《Go程序设计语言》这本书来系统的学习一下。

这是我第一次知道 Golang 其实是可以从启动是直接输入参数的,之前看到很多库都是使用 flag,也让我一度认为启动时传入参数必须使用 flag,比较这也好理解,谁让 Golang 的 main 函数和 java 或者 c 不太一样呢?

1
2
3
4
5
// java
public static void main(String[] args)

// c
int main(int argc, char *argv[])

Golang 直接 func main() ,想传入参数也不知道如何进行。

原来 Golang 是有个 os.Args 这个方法,Args的底层数据结构是一个字符串切片 var Args []string,这样就方便的获取到从命令行输入的参数。但是这里的 Args[0] 并不是传入的第一个元素,而是该程序的名字。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
package main

import (
	"fmt"
	"os"
)

func main() {
	fmt.Println(os.Args[0]) 
    // 输出结果 C:\Users\YOUNGX~1\AppData\Local\Temp\go-build2749142914\b001\exe\echo1.exe
}

所以要获得该程序从命令行传入的参数应该为 Args[1:],这样可以获取全部传入的值。

首先指针未赋值初始化为 nil

其他零值

官方对其他类型做出了规定 The Zero value

类型初始值
boolfalse
numeric0
string""
pointers,functions,interfacesnil
slices,channels,mapnil

同时指针是可比较的,当且仅当指向同一个变量或者都为 nil 的时候才相同

new() 函数通过传入一个类型,得到该类型的指针。同时每次执行new() 返回的指针值是不一样的。但是有一些例外:两个变量的类型不携带任何信息且是零值,它们有相同的地址,例如struct{}1.16 版本实现好像已经更改。

Go 的基本数据类型分为四类:基本类型,聚合类型,引用类型和接口类型。

  • 基本类型:数字,字符串,布尔
  • 聚合类型:数组,结构体
  • 引用类型:指针,slice,map,function,函数,通道

字符串是不可变类型。这一点和 Java 是一致的,但是为什么呢?不可变意味着两个字符串可以安全地公用同一段底层内存,使得复制任何长度字符串的开销都低廉。

字符串可以与字节 slice 相互转换。

1
2
3
s := "abc"
b := []byte(s)
s2  := string(b)

之前听过这样一句话,一个语言的成熟与否要看他对字符串的方法多不多。Golang 提供了以下包对字符串进行处理。bytesstringsstrconvunicode

对于数组来说, [3]int[4]int 是两个不同的数组类型。数组的长度必须是常量表达式(要你有何用?)。

Golang 的数组还挺有意思的,例如下面这样

1
symbol := [...]int{99:-1}

表示长度为 100 的数组,其中前99个都是 0,最后一个为 -1。

Golang 在数组和其他类型上都是值传递。需要引用传递使用指针。

1
2
3
func zero(ptr *[32]byte) {
	*prt = [32]byte{}
}

Golang 中的数组和 PHP 的数组还有点相似,可以作为k-v使用,但是 k 只能是数。arr := []string{0: "1", 2: "2"} ,这个数组遍历后的结果为如下:

1
2
3
0 1
1 
2 2

会多出一个新的索引 1

Slice 的底层为一个可变数组。

1
2
3
4
5
6
// src/runtime/slice.go
type slice struct {
	array unsafe.Pointer
	len   int
	cap   int
}

slice 无法用 == 比较,与 nil 可以,标准库中只有 bytes.Equal 比较两个 byte 的slice。 其他的就要自己实现了。(难道是因为没有泛型,作者要写多个实现?)

struct 结构体嵌套,而且匿名嵌套

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
type Point struct {
	x int
	y int
}

type Circle struct {
	Point
	Radius int
}

type Wheel struct {
	Circle
	Spokes int
}

这里的 PointCircle 可以采用这种匿名方式。而调用可以直接调用,像下面这种情况,第3行和第4行为相同的调用。

1
2
3
4
5
func main() {
	var w Wheel
	w.x = 5
	w.Circle.Point.x = 5
}

但是不允许在同一个结构体中定义两个相同类型的匿名成员。

匿名函数可以作为函数的参数和返回值。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
package main

import "fmt"

func squares() func() int {
	var x int
	return func() int {
		x++
		return x * x
	}
}

func main() {
	f := squares()
	fmt.Println(f())
	fmt.Println(f())
	fmt.Println(f())
	fmt.Println(f())
}

之前只用来作为资源关闭的操作,没想到还有 其他sao 操作。

defer后面可以为一个函数,通过函数调用来实现更多功能。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
func bigSlowOperation() {
	defer trace("bigSlowOperation")()
	time.Sleep(10 * time.Second)
}

func trace(msg string) func() {
	start := time.Now()
	log.Printf("enter %s", msg)
	return func() {
		log.Printf("exit %s (%s)", msg, time.Since(start))
	}
}

使用 defer 也有很多需要注意的地方。例如在循环中使用 defer。这个是无效的,并不能及时的回收资源,最好是将循环体和 defer 封装为一个函数,每次调用函数后会执行 defer。

宕机会引起程序异常退出。宕机代表的程序执行的终止,但是 Golang 提供了宕机恢复函数 recoverrecover 会终止当前的宕机状态,并且返回宕机值。

Golang 中对方法和函数有定义,emmmm🦤,方法是特殊的函数。

1
2
3
4
5
6
7
8
9
// 函数
func name(parameter-list) (result-list) {
    body
}

// 方法
func (t Type)name(parameter-list) (result-list) {
    body
}