持续更新

记录 golang 相关的常识,以及使用过程中遇到的问题。


1. 资料


1.1 官方资料

官网:https://go.dev/

官方文档合集:https://go.dev/doc/

language specification: https://go.dev/ref/spec

library 文档: https://pkg.go.dev/std

a tour of go: https://go.dev/tour/welcome/1

go by example: https://gobyexample.com/

go playground: https://go.dev/play/


2. 常识


2.1 数组 (array)

1、数组的长度是其类型的一部分,数组不能改变大小。

2、基本格式是:[n]type,如 var x = [6]int

3、数组是值类型,用它传参的时候,是拷贝一份数据的,如果要避免拷贝,可以传递它的指针。[2]

Go’s arrays are values. An array variable denotes the entire array; it is not a pointer to the first array element (as would be the case in C). This means that when you assign or pass around an array value you will make a copy of its contents. (To avoid the copy you could pass a pointer to the array, but then that’s a pointer to an array, not an array.) One way to think about arrays is as a sort of struct but with indexed rather than named fields: a fixed-size composite value.

4、数组可以这样初始化:


b := [2]string {"hello", "world"}

或者让编译器帮忙计数:


b := [...]string {"hello", "world"}


2.2 切片 (slice)

参考: Go Slices: usage and internals

slice 的构造包含三个部分:指向 array 的指针,len,cap。

len 比较好理解,就是它实际引用的区域,比如


arr := [5]int{1,2,3,4,5}
s := arr[2:4]
// s == []int{3,4}

此时,s 的 len 是 2,那 cap 呢?cap 实际上就是 cap(arr) - 2 = 3。


1、切片就像数组的引用,它并不实际存储数据,它只是描述了底层数组的一段。

2、更改切片的元素会修改其底层数组中对应的元素,和它共享此数组的其他切片也会观测到这些修改。

3、基本形式:[]type

4、切片字面量
s := []int{1,2,3,4} 这样做的本质是,先创建了一个数组:[4]int{1,2,3,4},再创建一个引用此数组的切片。

5、容量缩小之后,重新切片的时候,长度不得超过此容量,有点像一个滑动窗口。

6、nil 切片的长度和容量都为 0。

7、切片的边界,下界的默认值是 0,上界的默认值是该切片的长度,要重点记住,是长度,而不是容量。

在二次切片时,最好显式的给出上边界,避免出错,比如这样:


a := make([]int, 0, 5)
b := a[:2] // 此时 b 的长度是 2,容量是 5
c := b[3:] // not ok,会报错,相当于 c := b[3:2] 
c := b[3:cap(b)] // ok,相当于 c := b[3:5]

8、创建一个二维切片


m := 5
n := 6

vec := make([][]int, m)
for i := 0; i < n; i++ {
    vec[i] = make([]int, n)
    fmt.Println(vec[i])
}


2.3 type 关键字

来自官方的定义:”A type declaration binds an identifier, the type name, to a type. Type declarations come in two forms: alias declarations and type definitions.” [1]

翻译过来是:将一个类型名绑定到一个类型上,有两种形式,别名声明和类型定义。


例子1:别名声明

type MyFloat float64,这里面 MyFloat 作为 float64 的别名。


例子2:类型定义

type Vertex struct { X, Y float }, 这里面 struct { X, Y float } 定义了一种结构体,而 type Vertex 则用 Vertex 这个别名来指代它。

实际上,类型名不是必须的,譬如可以这样定义一个 struct 切片。


v := []struct{ X, Y float64 }{
    {2.1, 2.2},
    {3.1, 3.2},
}


2.4 接口值

接口值可以看做包含值和具体类型的元组 (value, type),它保存了一个具体类型的具体值,接口值调用方法时,也会执行其底层类型的同名方法。


2.5 nil 接口值与底层值为 nil 的接口是不同的

假设有这样定义的接口和结构体:


type I interface {
    M()
}
type T struct {
    S string
}

1、nil 接口值是指这个接口值既无具体值,也无类型。比如底下的 i 就是一个 nil 接口值:


var i I

2、底层值为 nil 的接口是的指无具体值,但有类型。比如底下的 i 就是一个无具体值的接口,此时 i 的底层值是 nil,但底层类型是 T:


var i I
var t T
i = t


2.6 空接口

空接口可以保存任何类型的值,这样就可以声明一个空接口类型的变量了: var i interface{},不需要特意用 type 写这个的别名。


2.7 fmt print 的基本格式

参照: https://pkg.go.dev/fmt@go1.22.5#hdr-Printing

General:


%v	the value in a default format
	when printing structs, the plus flag (%+v) adds field names
%#v	a Go-syntax representation of the value
%T	a Go-syntax representation of the type of the value
%%	a literal percent sign; consumes no value

The default format for %v is:


bool:                    %t
int, int8 etc.:          %d
uint, uint8 etc.:        %d, %#x if printed with %#v
float32, complex64, etc: %g
string:                  %s
chan:                    %p
pointer:                 %p


2.8 基本类型


bool

string

int  int8  int16  int32  int64
uint uint8 uint16 uint32 uint64 uintptr

byte // alias for uint8

rune // alias for int32
     // represents a Unicode code point

float32 float64

complex64 complex128

变量的零值:

数值类型为 0,
布尔类型为 false,
字符串为 “” (空字符串)。


2.9 协程与信道

1、只应由发送者关闭信道,而不应由接收者关闭。向一个已经关闭的信道发送数据会引发程序 panic。

2、for i := range c 会不断从信道 c 中接收值,则到它被关闭。

3、信道与文件不同,通常情况下无需关闭它们,只有在必须告诉接收者不再有需要发送的值时才需要,比如终止一个 range 循环。


2.10 ... 的两种用法

1、用于函数的不定参数,比如 func x(args ...string)

2、用于将 slice 打散,比如这样:


ans := []byte{}
nums := []int{10, 20, 30}
for _, x := range nums {
    // strconv.Itoa 的结果是 string,而 string... 是打散成 n 个 byte
    ans = append(ans, strconv.Itoa(x)...) 
}
fmt.Println(string(ans))

或者这样:


func test(nums ...int) {
	for _, x := range nums {
		fmt.Println(x)
	}
}

func main() {
	nums := []int{10, 20, 30}
    // nums 被打散成 3 个 int,作为入参传给 test
	test(nums...)  
}


3. 术语


3.1 短变量声明

:= 是短变量声明。

1、只能在函数内使用。

2、使用时,左侧必须至少有一个未声明过的变量,比如:


f := 3.14
f, yy := 4.0, true // ok,左侧的 yy 是未声明过的
f := 5.0           // not ok,左侧只有一个 f,且 f 已经声明过了


3.2 隐式解引用

指针.字段名 就是【隐式解引用】,正规的写法应该是 (*指针).字段名


3.3 方法与函数

1、方法是一类带特殊的 接收者 参数的函数。所以,方法是函数的一种。

2、可以将 struct 作为接收者,也可以将类型别名作为接收者,比如 type MyFloat float64,这里的 MyFloat 就可以作为接收者。

3、只能为同一个包中定义的接收者类型声明方法,不能为其他别的包中定义的类型声明方法。

所以不能直接为 float64 声明方法,因为它不在当前这个包里。只能通过定义别名的方式来实现这样的效果。

4、方法与指针重定向
类似于隐式解引用,当方法的接收者是指针,使用 值.方法名 时,go 会自动的解释为 (&值).方法名

反过来也一样,当方法的接收者是值,使用 指针.方法名 时,go 会自动解释为 (*指针).方法名


4. 用法


4.1 命令行转圈等待


package main
import (
	"fmt"
	"time"
)
func spinner(delay time.Duration) {
	for {
		for _, r := range `-\|/` {
			fmt.Printf("\r%c", r)
			time.Sleep(delay)
		}
	}
}
func main() {
	go spinner(100 * time.Millisecond)
	// do some other things
}


4.2 go tour 的 rot13 的简单做法


func rot13Trans(b byte) byte {
	if b >= byte('A') && b <= byte('M') {
		return byte(b+13)
	} else if b > byte('M') && b <= byte('Z') {
		return byte(b-13)
	} else if b >= byte('a') && b <= byte('m') {
		return byte(b+13)
	} else {
		return byte(b-13)
	}
}

func (r13 rot13Reader) Read(b []byte) (int, error) {
	n, err := r13.r.Read(b)
	for i := 0; i < n; i++ {
		b[i] = rot13Trans(b[i])
	}
	return n, err
}


5. 参考

[1] go.dev. Type declarations. Available at https://go.dev/ref/spec#Type_declarations.

[2] go.dev. Go Slices: usage and internals. Available at https://go.dev/blog/slices-intro, 2011-1-5.