Go 基础速描

  • Go 基础速描,非教程.

  • 资料来源:

    https://tour.go-zh.org/list

  • 更新

    1
    2022.03.21 初始

导语

开始尝试在接下来的一段时间内以兴趣为主,过渡一段时间,找一下抽象的欲望是什么东西.

转行到 CS 到现在,似乎只是抽象的对计算机感兴趣,但是对计算机领域那一块最想当作事业,却非常不明确,这样也导致了很多问题.因此从计算机网络入手,开始找找抽象的欲望: 最想折腾什么?

这不是教程,不是教程,不是教程.

Go 速描

语法1

go 初看的感觉非常像替换 c 语言,然后夹杂了 kotlin/python 语法,又删掉了一些符号.

包管理

1
2
3
4
5
package main
import(
"fmt"
"math/rand"
)
  • 总是从 main 包开始运行
  • 包名与导入路径的最后一个元素一致,这里是 rand
  • import (**) 是分组导入,当然可以是单独写多个 import,不过分组导入更实用.
  • go 包中以大写字母开头,那就是已导出的,可以直接访问.如果是小写字母那包外无法直接访问.类似于 math.Pimath.pi.

函数

1
2
3
4
5
6
7
8
func add(x int, y int) int {
return x + y
}
func add(x,y int) int {
x = sum * 4 / 9
y = sum - x
return
}
  • int x -> x:int -> x int : 类型在后,没有冒号.初上手最奇怪的一点.
  • 多个类型相同时,可只保留最后一个.x,y int
  • 可以返回任意数量返回值
  • return 没有参数,直接返回已命名的参数.第二个示例实在有点雷人,直接返回 xy.

变量

1
2
3
var i int
var i,j int = 1,2
k := 3 //只能用在函数
  • 与函数参数类似,没有 :,定义的位置可以随意.
  • 简便写法 := 只能用于函数.
  • 简便写法的类型会是自动推导的,数字与其输入的精度有关.

常量

1
2
3
4
5
6
7
8
const Pi = 3.14
const (
// 将 1 左移 100 位来创建一个非常大的数字
// 即这个数的二进制是 1 后面跟着 100 个 0
Big = 1 << 100
// 再往右移 99 位,即 Small = 1 << 1,或者说 Small = 2
Small = Big >> 99
)
  • const 声明,包括类似 const(xx) 的写法.
  • 不支持 := 简便写法.
  • 未指定类型常量由上下文确定

类型

  • bool string
  • int int8 16 32 64
  • unit unit8 16 32 64 unitstr
  • float32 float64
  • complex 32 complex128
  • byte = unit8 rune = int32(unicode码点)
  • int unit uintpter 32位默认是 32,64 位默认是 64.
  • 没有初始化时,数字默认是 0,bool 是 false 字符串为 “”.
  • go 没有隐藏的类型转换,全部需要显式转换,即使是数字.

循环

1
2
3
4
5
6
7
8
9
10
11
12
13
14
for i := 0; i < 10; i++ {
xxx
}

sum := 1
for ; sum < 1000; {
sum += sum
}

for sum < 1000 {
sum += sum
}
for { //= while true
}
  • go 只有 for 循环,坑爹还是普天同庆😂?
  • 感觉与 c/java for 类似.
  • 没有小括号! 所以大括号必须的.
  • 通常意义上的 while 用 for 代替,写法类似 第二/三.

条件分支-if

1
2
3
4
5
6
7
8
9
if x < 0 {
xxx
}

if v := math.Pow(x, n); v < lim {
return v
}else {
return v
}
  • 与 c/java 类似,还是没有小括号(杠上了是吗?),大括号必须.
  • 如同作用域一样,if 条件处可以执行简单语句,这个变量的作用域仅在 if/else 内部

条件分支-switch

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
switch os := runtime.GOOS; os {
case "darwin":
xxx
case "linux":
xxx
default:
xxx
}

t := time.Now()
switch {
case t.Hour() < 12:
xxx
case t.Hour() < 17:
xxx
default:
xxx
}
  • 与 c/java 语法类似,没有小括号.
  • go switch 只能匹配一个,除非 fallthrough.
  • 无需 break, case 无需是常量.
  • 没有条件的 switch 相当于一串 if/else,这样写更简洁.

defer

1
2
3
// 先执行 xx2 然后才执行 xx1
defer xx1
xx2
  • 突然跳出 defer 有点突然,这个是为何?替换 goto?
  • defer 会推迟执行直到外层函数执行完毕.

语法2

逃了 c/c++ 还是没能逃过结构体和指针…

指针

1
2
3
4
var p *int //int 类型指针
i := 42
p = &i
*p = 43
  • 谢天谢地没有指针运算.
  • 未初始化默认值: nil
  • &i 取地址 *p 取值,还是熟悉的味道.

结构体

1
2
3
4
5
6
7
8
9
10
11
12
13
14
type Vertex struct {
X int
Y int
}

func main() {
v := Vertex{1, 2}
v.X = 4

v2 = Vertex{X: 1} //v2.X 被直接赋值为 1,v2.Y 隐式为 0

p := &v
p.X
}
  • 语法与 c 类似.
  • 结构体定义后,与普通类型使用基本相同.
  • 初始化时能仅初始化一部分成员的值 {Name:xx},其他成员默认初始值.

数组与切片

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
var a [10]int //创建数组
var s []int = a[1:4] // 切片
// 函数内
q := [6]int{2, 3, 5, 7, 11, 13} //创建并初始化一个数组
r := []bool{true, false, true, true, false, true} //创建并初始化数组并返回指向数组的切片.
// make 创建切片
a := make([]int, 5) //指定长度
b := make([]int, 0, 5) //指定长度+容量

append(a,b) //b 添加到 a,a 容量不足会创建一个容量够的新底层数组
board := [][]string{
[]string{"_", "_", "_"},
[]string{"_", "_", "_"},
[]string{"_", "_", "_"},
}
board[0][0] = "X"

for i, v := range a{i,v}
for _, v := range a{v}
  • go 数组大小不可变,与 c 相同,但go 提供了灵活的切片,变相起到了动态数组的作用.
  • 切片
    • 取切片语法与 python 相近,都是不包括下界,定义时多了类型声明.
    • 切片可以理解为指向数组指针的 go 简化版,任何对切片元素的修改会直接反映在原数组上.
  • 数组/切片的初始化方式,倒是挺新鲜…
  • 切片与 py 最大的区别是有长度和容量的概率
    • 长度: 元素的个数 len()
    • 容量: 第一个切片元素到数组最后一个元素的长度 cap()
    • 话说为啥要定义容量这个概念?
  • 空切片: 直接指向 nil,长度 0 容量 0,字符串输出是 []
  • make 创建新的切片,可指定长度/容量.
  • 切片算是简化版指针? 指向切片的切片也是可以的.
  • append 向切片添加元素,底层数组容量不足,会直接创建新的底层数组…这就是动态数组了吧?
  • 循环
    • 默认 for 循环是循环 下标+元素副本
    • 类似 py 可用 _ 忽略 下标 or 元素副本.

映射: 怎么感觉这么像集合的变体?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
var m map[string]Vertex
m = make(map[string]Vertex)

var m = map[string]Vertex{
"Bell Labs": Vertex{40.68433, -74.39967,},
"Google": Vertex{37.42202, -122.08408,},
}
var m = map[string]Vertex{
"Bell Labs": {40.68433, -74.39967},
"Google": {37.42202, -122.08408},
}

// m[key] = elem,elem = m[key]
// delete(m, key)
elem, ok = m[key] //双赋值检测 key
elem, ok := m[key] //甚至直接建立变量
  • 键映射到值,未初始化是 nil,make 可建立映射.
  • 直接初始化时,顶级类型可省略.
  • 访问和集合一样
  • 双赋值
    • key 存在,ok true,不存在,ok false.
    • elem 要不然是真正的值,要不然是 0.

函数值: 所以 go 自带高阶函数?

1
2
3
4
5
6
7
8
func compute(fn func(float64, float64) float64) float64 {
return fn(3, 4)
}
hypot := func(x, y float64) float64 {
return math.Sqrt(x*x + y*y)
}
hypot(5, 12)
compute(hypot)
  • 函数也可以当作值传递,函数参数/函数返回值都行.

闭包: 有点迷?

1
2
3
4
5
6
7
func adder() func(int) int {
sum := 0
return func(x int) int {
sum += x
return sum
}
}
  • go 的闭包和匿名函数,还需之后细致研究.
  • 闭包也是一个函数,但是能引用定义在闭包以外的函数体的变量,访问/修改.

方法和接口

方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
type Vertex struct {
X, Y float64
}
func (v Vertex) Abs() float64 {
return xxx
}
// 传入指针,可以修改结构体值
func (v *Vertex) Scale(f float64) {
v.X = v.X * f
v.Y = v.Y * f
}

v := Vertex{3, 4}
v.Scale(10)
v.Abs()

type MyFloat float64 //内建类型 type 转换为自定义类型
func (f MyFloat) Abs() float64 {
return float64(f)
}
  • 指定接收者的特殊函数 func (接收者) 方法名 函数参数{}
  • 接收者定义与使用其定义的方法必须在同一个包
  • 不能为内建类型声明方法(type 转一下)
  • 指针接收者
    • 类似传入指针,能修改指向结构体实例的成员,非常常用.
    • go 通过指针访问还是通过变量名访问,语法相同,因此可能有点混淆.

接口

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
type Abser interface {
M()
}

func (t T) M() {
fmt.Println(t.S)
}
var i I = T{"hello"}

func (t *T) M() {
if t == nil { //传入实现为空时处理
return
}
fmt.Println(t.S)
}
// 空接口
var i interface{}
  • 接口是一组方法的集合,用法似乎比 kotlin 更加简洁
  • 接口可以当作类型使用,定义变量,用在返回值都行.
  • 实现一个接口时,无需显式的 implements 关键词.
  • 内部,接口是 (value,type)
    • value 是值,type 是具体实现的类型
  • 接口实现为空时,go 有对应的处理
  • interface{} 空接口就有意思了,因为是空,所以能承载任意类型.一般用于处理未知类型值.
  • t, ok := i.(T) 断言,判断接口实现是不是为空,与映射的用法相同.

错误处理: 据说是 go 的一点让人不爽的地方

1
2
3
4
5
6
7
8
9
10
11
12
13
```

- 没有 try-catch,go 只是判断返回值 != nil 来进行错误处理
- 当然上面仅是官方,第三方的框架很多.

## 并发

来到 go 协程的主场了,似乎与 kotlin 协程在概念上有很多相似.

- 也别纠结在概念,就当作用户控制的轻量级线程罢了.

```go
go f(x, y, z) // 启动一个协程

协程直接交换信息: chan or sync

chan 信道

1
2
3
4
5
6
7
8
9
10
11
12
ch := make(chan int) //初始化信道
ch := make(chan int,100) //信道带缓存
ch <- v // 发送
v := <-ch // 接受

close(ch) //关闭信道,只要发送方才能调用
for i := range ch {} //从信道接受直到信道关闭.

select {
case c <- x:
xx
}
  • 箭头是数据流向,倒是挺直观.
  • 默认是发送后就阻塞了,直到有接受才能再次发送,创建时手动设置缓存,一直发直到彻底灌满.
  • 只有发送方才能 close 关闭信道,一般无需关门信道,除非是需要显示通知接收方,例如在循环中,跳出循环.
  • select 配合 chan 倒是头一次见,阻塞一直到某个 case 可以执行,多个随机选一个,不想要阻塞 -> 增加 default 分支.

sync 互斥锁

1
2
3
mux := sync.Mutex
mux.Lock()
mux.Unlock()
  • 与 c 的使用基本相同,上锁解锁.