Go 学习相关笔记
Go 官方的教学文档顺序不怎么友好,这里根据我自己的学习曲线来记录文档的查看顺序
基础知识
文档预备
- 新手先要看 Go 的模块管理介绍,这样才知道基础 Go 怎么导入外部包和进行本地的包管理
https://go.dev/doc/modules/managing-dependencies
这个包管理介绍的核心知识点:
- 使用
go mod init
初始化出一个 module - 使用
go mod edit -replace example.com/greetings=../greetings
来建立本地模块的导入
关系 - 代码里面 import 了相应的包之后,使用
go mod tidy
来让 go 自动建立依赖
文档没有提及的知识点:
- 一个文件夹内只可以放同一个
package
的代码文件
-
看完依赖管理后可以走一遍简单教程
-
完成简单教程后就可以进入正式的教程深入学习
走完上面三步,开发简单的项目就没什么问题了
vscode 搭建 Go 的开发环境
-
安装 Go 的插件
-
安装 dlv 工具支撑 Go 在 vscode debug
https://github.com/microsoft/vscode-go/blob/master/docs/Debugging-Go-code-using-VS-Code.md
数组
Go 传递数组是按值传递的,并不是像 C 语言那样传递首元素指针
- 初始化固定长度的数组
go
b := [2]string{"Penn", "Teller"}
- 让编译器自动计算数组长度
go
b := [...]string{"Penn", "Teller"}
切片
初始化一个切片(Slice), 和数组区别是切片的初始化表达式里不需要指定长度, 留意动态长度数组初始化和
切片初始化也是不一样的
go
letters := []string{"a", "b", "c", "d"}
- 切片也可以用
make
函数来创建
bash
func make([]T, len, cap) []T
对数组或者一个切片用坐标进行切分也会创建一个切片数据结构
go
b := []byte{'g', 'o', 'l', 'a', 'n', 'g'}
// b[1:4] == []byte{'o', 'l', 'a'}, sharing the same storage as b
切片底层
len 是切片相对 ptr 引用的元素数量, cap 是底层数组相对 ptr 的元素数量。
扩大切片容量的基本原理就是新建一个具有更大空间的切片,然后将旧切片的数据拷贝到新的切片中。
go
t := make([]byte, len(s), (cap(s)+1)*2)
copy(t, s)
s = t
官方提供了 append
函数来扩大一个切片
go
func append(s []T, x ...T) []T
展开运算符 ...
暂时只在 slice 细节介绍博客里面提到展开运算符
https://go.dev/blog/slices-intro
- 收集参数到一个切片中
go
func append(s []T, x ...T) []T
- 传递参数时展开一个切片作为参数列表
go
a := []string{"John", "Paul"}
b := []string{"George", "Ringo", "Pete"}
a = append(a, b...) // equivalent to "append(a, b[0], b[1], b[2])"
// a == []string{"John", "Paul", "George", "Ringo", "Pete"}
import 导入别名
详细看文档
https://go.dev/ref/spec#Import_declarations
type 建立类型别名
go
type rune = int32
type any = interface{}
type comparable interface{ comparable }
函数
参数列表
go
func add(x int, y int) int {
return x + y
}
// 如果多个参数的类型一致,也可以写成
func add(x, y int) int {
return x + y
}
返回多个结果以及接收多个结果
返回的类型在参数列表后面指定
go
func swap(x, y string) (string, string) {
return y, x
}
func main() {
a, b := swap("hello", "world")
fmt.Println(a, b)
}
命名返回的结果
如果对返回的结果不单指定了类型,还提供了名称,那么同名的变量也会被创建。
当函数 return 语句后面为空时,将返回符合命名的变量数据
go
func split(sum int) (x, y int) {
x = sum * 4 / 9
y = sum - x
return
// 等价于 return x, y
}
变量
声明
var
关键字可以声明变量列表,可以在全局和函数中声明
go
package main
import "fmt"
var c, python, java bool
func main() {
var i int
fmt.Println(i, c, python, java)
}
初始化
如果显示给出了初始化的值,则可以省略变量的类型声明,编辑器会自动推导类型,复杂类型还是显示写出
类型比较好
go
package main
import "fmt"
var i, j int = 1, 2
func main() {
var c, python, java = true, false, "no!"
fmt.Println(i, j, c, python, java)
}
当在函数内部声明变量时,使用特殊的赋值符号 :=
可以省略 var
关键字,这个操作会在初始化变量
的同时推导其数据类型
但是全局的语句必须以关键字开头,所以全局变量的声明必须带上 var
关键字,无法使用 :=
基本类型
go 内置的基本数据类型
go
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
缺省默认值
变量声明时没有初始化时,会被赋予相应类型的缺省值
go
0 for numeric types,
false for the boolean type, and
"" (the empty string) for strings.
(nil for slice)
类型转换
用 T(v)
将值 v 转换到 类型 T
go 的所有类型转换都需要显示写出转换函数,不会像 C 那样存在自动转换规则
go
var i int = 42
var f float64 = float64(i)
var u uint = uint(f)
常量
使用 const
关键字声明一个常量
go
const Pi = 3.14
流程控制
for 循环
go 只有一个循环语句 for
用分号分隔 初始语句; 条件语句; 循环后语句
初始语句 和 循环后语句 可以省略
go
func main() {
sum := 0
for i := 0; i < 10; i++ {
sum += i
}
fmt.Println(sum)
}
类 C 的 while
语句写法, 移除分号
go
func main() {
sum := 1
for sum < 1000 {
sum += sum
}
fmt.Println(sum)
}
无限循环
go
func main() {
for {
}
}
if
if
语句可以在进行判断前执行一条语句, 如果在这条语句中创建了变量,那么变量的将仅在这个 if
块内可见
go
func pow(x, n, lim float64) float64 {
if v := math.Pow(x, n); v < lim {
return v
}
fmt.Println(lim)
return lim
}
switch...case
go 的 switch...case
语句可以像 if
那样在进行判断前执行一条语句。而且 go 的 switch...case
和 C 语言的 switch...case
的区别是不需要在每个 case
语句末尾添加显示的 break
go
func main() {
fmt.Print("Go runs on ")
switch os := runtime.GOOS; os {
case "darwin":
fmt.Println("OS X.")
case "linux":
fmt.Println("Linux.")
default:
// freebsd, openbsd,
// plan9, windows...
fmt.Printf("%s.\n", os)
}
}
go 的 switch...case
不一定是要常量或者整数,可以是其他的值,具体细节这里不去展开
go
func main() {
var x interface{}
switch i := x.(type) {
case nil:
fmt.Printf(" x 的类型 :%T",i)
case int:
fmt.Printf("x 是 int 型")
case float64:
fmt.Printf("x 是 float64 型")
case func(int) float64:
fmt.Printf("x 是 func(int) 型")
case bool, string:
fmt.Printf("x 是 bool 或 string 型" )
default:
fmt.Printf("未知型")
}
}
没有条件语句的 switch
等效于 switch true
,此时的 case
可以加入判断语句。
这种写法相当于写一段长的 if-else
语句
go
func main() {
t := time.Now()
fmt.Println(t.Hour())
switch {
case t.Hour() < 12:
fmt.Println("Good morning!")
case t.Hour() < 17:
fmt.Println("Good afternoon.")
default:
fmt.Println("Good evening.")
}
}
defer
defer
语句将推迟一个函数的执行直到当前的函数块 return 之后,常常用来完成清理现场的工作,
注意函数的控制权也是在所有的 defer
调用完毕后才会移交给上级函数
go
func main() {
fmt.Println("counting")
for i := 0; i < 10; i++ {
defer fmt.Println(i)
}
fmt.Println("done")
}
多个 defer
语句按照后进先出的顺序执行 (栈式调用)
在 defer
推迟的函数调用中,传递的函数的参数是已经完成了求值,并不会受到后续语句的影响。
defer
的函数调用可能会读取或者修改所在调用函数块的命名返回变量,这种特性是为了方便修改错误的
函数返回值
defer panic recover
当一个函数 F 调用了 panic
,F 将停止执行,然后按照栈顺序将所有的 defer
的函数,然后执行权移交给 F 的调用函数 G,并且此时 F 的执行效果等价于执行了 panic
。一个函数中如果调用了 recover
,将会取到此时 panic
传入的值,
然后将当前函数从 panic
状态恢复到正常执行的状态。因为 recover
只能在 defer
函数里调用,等同于只要执行过 recover
,后续的 defer
执行完后,
该函数会正常结束,不会再到调用函数中触发 panic
。如果在没有 panic
的函数内调用 recover
,返回值是 nil
。
指针
指针类型声明
go
var p *int
和 C 语言类似的取指针操作
go
i := 42
p = &i
取指针的值
go
fmt.Println(*p) // read i through the pointer p
*p = 21 // set i through the pointer p
go 不能像 C 语言那样对指针进行算术运算
结构
声明一个结构
go
type Vertex struct {
X int
Y int
}
使用 .
访问结构成员
go
func main() {
v := Vertex{1, 2}
v.X = 4
fmt.Println(v.X)
}
结构指针也可以通过 .
访问成员
go
func main() {
v := Vertex{1, 2}
p := &v
p.X = 1e9
// 或者 (*p).X
fmt.Println(v)
}
结构字面量
字面量书写的顺序和结构成员的声明的顺序保持一致;
可以通过命名成员的方式初始化一个结构,此时成员的初始值和书写顺序无关;
若没有显示写出初始值,各个成员的初始值将会隐示指定;
可以用 &
仅返回初始化的结构的指针;
go
var (
v1 = Vertex{1, 2} // has type Vertex
v2 = Vertex{X: 1} // Y:0 is implicit
v3 = Vertex{} // X:0 and Y:0
p = &Vertex{1, 2} // has type *Vertex
)
func main() {
fmt.Println(v1, p, v2, v3)
}
Range
range
形式的 for
循环可以遍历一个 slice
或者 map
range
每次迭代返回两个值,第一个是下标,第二个是该下标对应的值
go
var pow = []int{1, 2, 4, 8, 16, 32, 64, 128}
func main() {
for i, v := range pow {
fmt.Printf("2**%d = %d\n", i, v)
}
}
可以使用 _
来忽略某个位置的值的赋值
go
for i, _ := range pow
for _, value := range pow
也可以只使用第一个数值
go
for i := range pow
Maps
maps
建立一个键值对
可以通过 make
函数来创建指定类型的 maps
go
package main
import "fmt"
type Vertex struct {
Lat, Long float64
}
var m map[string]Vertex
func main() {
m = make(map[string]Vertex)
m["Bell Labs"] = Vertex{
40.68433, -74.39967,
}
fmt.Println(m["Bell Labs"])
}
字面量
可以用字面量来创建并初始化一个 maps
go
package main
import "fmt"
type Vertex struct {
Lat, Long float64
}
var m = map[string]Vertex{
"Bell Labs": Vertex{
40.68433, -74.39967,
},
"Google": Vertex{
37.42202, -122.08408,
},
}
func main() {
fmt.Println(m)
}
maps 的操作
插入新的键值对
go
m[key] = elem
获取值
go
elem = m[key]
删除键
go
delete(m, key)
判断键是否在 maps
中
go
elem, ok = m[key]
key
在 m
中时,ok
为 true
, 反之为 false
ok
为 false
时,elem
就会是对应类型的零值
注意 elem
和 ok
没有提前声明时可以使用赋值声明的方式
go
elem, ok := m[key]
函数
go 里面的函数也是一种数值,意味着可以当成参数传递给另一个函数或者作为函数的返回值
go
package main
import (
"fmt"
"math"
)
func compute(fn func(float64, float64) float64) float64 {
return fn(3, 4)
}
func main() {
hypot := func(x, y float64) float64 {
return math.Sqrt(x*x + y*y)
}
fmt.Println(hypot(5, 12))
fmt.Println(compute(hypot))
fmt.Println(compute(math.Pow))
}
方法与类型
go 没有类的概念,但是可以定义类型上的方法
在函数 func
关键字和函数名之间添加一个特殊的 receiver
参数,就可以在指定类型
上定义一个方法
以下示例在 Vertex
上定义了一个 Abs
方法
go
package main
import (
"fmt"
"math"
)
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())
}
类型的方法只能在与类型定义相同的包中定义,也就是不能定义另一个包的类型上面的方法
定义类型方法时使用类型指针
如果定义类型方法时 receiver
是一个类型指针,那么这个方法就可以修改到原始类型内部的值。如果只是传递类型,方法内部对结构上的值的修改不会影响原始结构。
指针传递可以避免结构值的拷贝,提升效率
在类型方法的定义中,指针传递和值传递影响的是 interface
的方法检测行为,参考下节
Interfaces
一组方法的签名可以定义为一个 interface
类型
只要实现了相应的方法签名,一个值就可以认为是匹配的 interface
类型
下面的示例说明,在 *Vertex
上实现了 Abs 方法时,只有 Vertex
的指针可以赋值给 Abser 类型,单纯的 Vertex
赋值给 Abser 类型会报错。
go
package main
import (
"fmt"
"math"
)
type Abser interface {
Abs() float64
}
func main() {
var a Abser
f := MyFloat(-math.Sqrt2)
v := Vertex{3, 4}
a = f // a MyFloat implements Abser
a = &v // a *Vertex implements Abser
// In the following line, v is a Vertex (not *Vertex)
// and does NOT implement Abser.
a = v
fmt.Println(a.Abs())
}
type MyFloat float64
func (f MyFloat) Abs() float64 {
if f < 0 {
return float64(-f)
}
return float64(f)
}
type Vertex struct {
X, Y float64
}
func (v *Vertex) Abs() float64 {
return math.Sqrt(v.X*v.X + v.Y*v.Y)
}
未初始化的类型值
一个类型没有初始化时,其值一般为 nil
, 此时定义的类型方法中接收到的 receiver
也是 nil
建议实现类型方法时考虑 nil
的情形
go
package main
import "fmt"
type I interface {
M()
}
type T struct {
S string
}
func (t *T) M() {
if t == nil {
fmt.Println("<nil>")
return
}
fmt.Println(t.S)
}
func main() {
var i I
var t *T
i = t
describe(i)
i.M()
i = &T{"hello"}
describe(i)
i.M()
}
func describe(i I) {
fmt.Printf("(%v, %T)\n", i, i)
}
没有初始化 interface
类型的变量就调用其方法,会报错
空的 interface
interface{}
是一个没有方法签名的,空的 interface
类型,其意义是表示任意一种类型。常用在需要接受未知类型参数的方法中。
底层
interface
底层可以视为一个 (value, type)
的元组
类型断言
t := i.(T)
用来断言 i
是一个 T
类型,并将转换成功的 i
值赋值给 t
如果断言失败,上面的代码就会触发一个 panic
可以通过接受两个返回值来规避 panic
go
t, ok := i.(T)
断言成功,t
就是转换成功的值,ok
为 true
断言失败,t
为零值,ok
为 false
type switch
type switch 是一种代码结构, 常用来组织多个类型断言, 同时 default
分支的存在
也可以规避 panic
的触发
go
package main
import "fmt"
func do(i interface{}) {
switch v := i.(type) {
case int:
fmt.Printf("Twice %v is %v\n", v, v*2)
case string:
fmt.Printf("%q is %v bytes long\n", v, len(v))
default:
fmt.Printf("I don't know about type %T!\n", v)
}
}
func main() {
do(21)
do("hello")
do(true)
}
下面脱离了 switch
的类型断言会触发一个 panic
go
package main
import "fmt"
func do(i interface{}) {
a := i.(int)
fmt.Printf("%v, %T", a, a)
}
func main() {
do(21)
do("hello")
do(true)
}
内置 interface
Stringer
fmt
打印值时会寻找下面的接口
String 方法返回一个描述自身的字符串
go
type Stringer interface {
String() string
}
Errors
go
type error interface {
Error() string
}
io.Reader
实现以下方法的类型就是一个 Reader
go
func (T) Read(b []byte) (n int, err error)
该方法接收一个 byte
类型的切片,填充读到的数据到这个切片中,然后返回填充的数量和一个 error
表示是否读取结束(io.EOF
)
go
package main
import (
"fmt"
"io"
"strings"
)
func main() {
r := strings.NewReader("Hello, Reader!")
b := make([]byte, 8)
for {
n, err := r.Read(b)
fmt.Printf("n = %v err = %v b = %v\n", n, err, b)
fmt.Printf("b[:n] = %q\n", b[:n])
if err == io.EOF {
break
}
}
}
类型参数
表示一个函数接收的类型需要满足 comparable
约束,comparable 是一个 interface
go
func Index[T comparable](s []T, x T) int
泛型声明
go
package main
// List represents a singly-linked list that holds
// values of any type.
type List[T any] struct {
next *List[T]
val T
}
func main() {
}
并发
一个 goroutine
是由 Go 运行时管理的一种轻量线程。
将 goroutine
视为协程是因为它具有协程的核心特点------在协程之间的调度不需要涉及任何系统调用或任何阻塞调用。
一般的线程是需要经由操作系统来进行调度,底层涉及到了各种同步性原语,如互斥锁,信号量等。但是
goroutine
是完全由 Go 运行时管理。
go
go f(x, y, z)
上面会启动一个 goroutine
。
goroutine
和主程序共享内存地质空间,所以 goroutine
对内存的访问需要同步进行,sync
包提供了同步支持。
Channels
Channels
是一种带有类型的管道,你可以通过它来传递和接收数据,操作符是 <-
go
ch <- v // Send v to channel ch.
v := <-ch // Receive from ch, and
// assign value to v.
Channels
在使用前也需要声明
go
ch := make(chan int)
默认情况下,Channels
在传递和接收数据时都会阻塞等待其中一边准备就绪,这个特点可以用来在
goroutine
之间进行数据同步
go
package main
import "fmt"
func sum(s []int, c chan int) {
sum := 0
for _, v := range s {
sum += v
}
c <- sum // send sum to c
}
func main() {
s := []int{7, 2, 8, -9, 4, 0}
c := make(chan int)
go sum(s[:len(s)/2], c)
go sum(s[len(s)/2:], c)
x, y := <-c, <-c // receive from c
fmt.Println(x, y, x+y)
}
Channels
可以在声明时指定缓存大小,这样如果往 Channels
发送数据,就会在 Channels
缓存满时阻塞。同理,从 Channels
接收数据时,会在缓存空时阻塞。
go
ch := make(chan int, 100)
Range 和 Close
Channels
的发送者可以关闭一个 Channels
,此时 Channels
的接收者可以通过接收两个
参数来获取关闭状态
go
v, ok := <-ch
如果 Channels
没有更多的数据并且 Channels
被关闭了的话,ok
就为 false
下面的循环会不断的从 Channels
c 中读取数据直至它被关闭
go
for i := range c
go
package main
import (
"fmt"
)
func fibonacci(n int, c chan int) {
x, y := 0, 1
for i := 0; i < n; i++ {
c <- x
x, y = y, x+y
}
close(c)
}
func main() {
c := make(chan int, 10)
go fibonacci(cap(c), c)
for i := range c {
fmt.Println(i)
}
}
Select
使用 select
和 Channels
进行结合,可以写出强大的多条件同步判断。
select
会对所有的 case
进行阻塞,直到其中一个接收到值为止,如果此时有多个 case
准备就绪,
select
会随机地选择一个进行执行
go
import "fmt"
func fibonacci(c, quit chan int) {
x, y := 0, 1
for {
select {
case c <- x:
x, y = y, x+y
case <-quit:
fmt.Println("quit")
return
}
}
}
func main() {
c := make(chan int)
quit := make(chan int)
go func() {
for i := 0; i < 10; i++ {
fmt.Println(<-c)
}
quit <- 0
}()
fibonacci(c, quit)
}
注意,一般不要写 select
的 default
情况,否则 default
的分支会被执行多次。
部分可以使用 default
的示例:
go
package main
import (
"fmt"
"time"
)
func main() {
tick := time.Tick(100 * time.Millisecond)
boom := time.After(500 * time.Millisecond)
for {
select {
case <-tick:
fmt.Println("tick.")
case <-boom:
fmt.Println("BOOM!")
return
default:
fmt.Println(" .")
time.Sleep(50 * time.Millisecond)
}
}
}
互斥锁
Go 也提供了互斥的锁的相关数据类型 sync.Mutex
, 其常用方法是 Lock
和 Unlock
。
也可以结合 defer
的使用来确保锁的释放。
go
package main
import (
"fmt"
"sync"
"time"
)
// SafeCounter is safe to use concurrently.
type SafeCounter struct {
mu sync.Mutex
v map[string]int
}
// Inc increments the counter for the given key.
func (c *SafeCounter) Inc(key string) {
c.mu.Lock()
// Lock so only one goroutine at a time can access the map c.v.
c.v[key]++
c.mu.Unlock()
}
// Value returns the current value of the counter for the given key.
func (c *SafeCounter) Value(key string) int {
c.mu.Lock()
// Lock so only one goroutine at a time can access the map c.v.
defer c.mu.Unlock()
return c.v[key]
}
func main() {
c := SafeCounter{v: make(map[string]int)}
for i := 0; i < 1000; i++ {
go c.Inc("somekey")
}
time.Sleep(time.Second)
fmt.Println(c.Value("somekey"))
}