learn golang

快速入门相关

数据类型

基础类型

  • 整形
    • 带符号:int、int8、int16、int32 和 int64

      负数,正数,0

    • 无符号:uint、uint8、uint16、uint32 和 uint64

      正数,0

    • 字节类型 byte,等价于uint8,定义一个字节
  • 浮点
    • float32
    • float64
  • 布尔
    • true / flase
  • 字符串
    • 可以表示为任意的数据,类型为 string
    • 可以使用++= 运算符连接
  • 零值
    • 变量的默认值,没有初始化时候就是零值
    • 数字为0
    • 布尔为false
    • 字符串为""

变量

变量简短声明

1
变量名 := 表达式

指针
指针对应的是变量在内存中的存储位置
通过 & 获取变量的指针,通过 * 获取指针对应的变量值,指针对应的类型表示为 *类型

常量

1
const 常量名 = 表达式

只允许布尔型字符串数字类型这些基础类型作为常量

iota
常量生成器,它可以用来初始化相似规则的常量,避免重复的初始化。

1
2
3
4
5
const (
one = iota + 1 // 1
two // 2
three // 3
)

字符串

字符串和数字互转

  • 与 int 转换:使用 strconv.Atoistrconv.Itoa 方法
  • 与 float 转换:使用 strconv.ParseFloatstrconv.FormatFloat
  • 与 bool 转换:使用 strconv.ParseBoolstrconv.FormatBool
  • 数字互转:使用类型(要转换的变量)float64(int)int(float)

strings 包
处理字符串的工具包,如查找字符串、去除字符串的空格、拆分字符串、判断字符串是否有某个前缀或者后缀等

1
2
3
strings.HasPrefix(s, "h")
strings.Index(s, "l")
strings.ToUpper(s)

逻辑

if…else

  1. 条件表达式不使用()
  2. 条件分支需要 {}
  3. { 及 else 前的 } 不能独占一行
  4. 可以多个 else if ...
  5. if 语句中,可以有一个简单的表达式语句

switch 语句

  1. case 从上到下逐一进行判断,一旦满足条件,立即执行对应的分支并返回
  2. fallthrough 语句忽略当前分支,执行下面一条分支
  3. 可以用一个简单的语句来做初始化,同样也是用分号 ; 分隔
  4. case 后的值就要和初始化表达式的结果类型相同

for 循环语句

  1. for 循环由三部分组成,其中,需要使用两个 ; 分隔
  2. 三部分组成都不是必须的
    1. 省略初始化和更新句,只保留循环条件,达到 while 的效果
  3. 支持使用 continuebreak 控制 for 循环

数据结构

数组

数组存放的是固定长度、相同类型的数据,而且这些存放的元素是连续的。

1
2
3
4
5
6
7
list := [len]type{...}

list := [5]int{1,2,3,4,5} // len(list) == 5

list := [...]int{1,2,3,4} // 自动推导长度为4

list := [5]string{1: 'a', 3: 'c'} // 初始化某几个元素
数组循环

使用 for range 直接输出对应的索引和值,不需要获取的值用 _ 下划线丢弃

切片

切片是基于数组实现的,它的底层就是一个数组。

1
slice := array[start:end]

但是经过切片后,切片的索引范围改变了。使用 len() 获取切片长度,使用 cap() 获取容量(即底层数组)。

切片声明
1
2
slice := make([]type, len, cap)
slice := []type{...}
追加
1
2
newSlice := append(oldSlice, ele1, ele2, ele3)
newSlice := append(oldSlice, slice1...)

在创建新切片的时候,最好要让新切片的长度和容量一样,这样在追加操作的时候就会生成新的底层数组,从而和原有数组分离,就不会因为共用底层数组导致修改内容的时候影响多个切片。

Map

  • map 是一个无序的 K-V 键值对集合,结构为 map[K]V
  • Key 必须具有相同的类型,Value 也同样,但 Key 和 Value 的类型可以不同。
  • Key 的类型必须支持 == 比较运算符,这样才可以判断它是否存在,并保证 Key 的唯一
  • len(map) 获取元素个数
创建
1
2
map := make(map[string]int)
map := map[string]int{"a":1}
获取和删除
1
2
3
ele, ok := map["a"] // ok 标记是否存在,存在为true

delete(map, "a") //删除
遍历
1
for k, v := range map {...}

map 的遍历是无序的。可以先排序 key,然后循环获取对应的 value

String 和 []byte

字符串 string 也是一个不可变的字节序列,所以可以直接转为字节切片 []byte。

1
2
s := "hello 世界"
bs := []byte(s)

函数和方法

1
2
3
func funcName(params) result {
// funcBody
}
  1. 函数
    1. 能够多值返回
    2. 命名返回参数,可以bare return,但不鼓励使用
    3. 可变参数:在参数类型前加三个点
      1. func funcName(params ...type) result {...}
      2. 可变参数是slice,使用 for range 遍历
      3. 可变参数一定要放在参数列表的最后一个
    4. 包级函数
      1. 私有函数首字母小写,包外不能访问
      2. 导出的首字母大写
      3. 任何一个函数从属于一个包
    5. 匿名函数
    6. 可以赋给一个变量,但变量不是函数的名字
    7. 函数嵌套,闭包
    8. 函数也是一种类型,它也可以被用来声明函数类型的变量、参数或者作为另一个函数的返回值类型。
  2. 方法
    1. 必须有一个接受者,是一个类型,方法与类型绑定,成为这个类型的方法
    2. 接受者可以是一个指针类型。接受者的副本调用函数,无法修改接受者的值,只能使用指针。
    3. Go 语言会自动转义
    4. 选择值类型还是指针类型,按照需求是否要改变接收者的值而定
    5. 不管方法是否有参数,通过方法表达式调用,第一个参数必须是接收者,然后才是方法自身的参数。

结构体

结构体是一种聚合类型,里面可以包含任意类型的值。

结构体也是一种类型

定义

1
2
3
4
5
6
type structName struct { fieldName typeName }

type person struct {
name string
age int
}

声明和访问

1
2
3
4
5
6
var p person
p := person{name: "Jacky", 40}
p := person{"Jacky", 40}

p.name
p.age

结构体的字段可以是任意类型,也包括自定义的结构体类型。

接口

高度抽象的类型,不用和具体的实现细节绑定在一起。

以 type 关键字开始,但接口的关键字是 interface,表示自定义的类型是一个接口。

1
2
3
4
5
type interfaceName interface { funcName() returnName }

type Stringer interface {
String() string
}

实现

接口的实现者必须是一个具体的类型。

必须实现接口的每个方法才算是实现了这个接口。

面向接口编程的,只要一个类型实现了接口,都可以得到相应的结果,而不用管具体的类型实现。

值类型接收者实现接口的时候,不管是类型本身,还是该类型的指针类型,都实现了该接口。当指针类型作为接收者时,只有指针类型实现了该接口。

工厂函数

创建自定义的结构体,也可以创建一个接口。

1
2
3
4
5
6
7
8
9
10
// erros/erros.go
// 工厂函数,返回一个 error 接口 (*errorString 结构体实现了该接口)
func New(text string) error { return &errorString(text) }

// errorString 结构体
type errorString struct { s string }

// 实现 error 接口
// error 接口需要实现一个Error()方法,通过实现这个方法,让 *errorString 实现了 error 接口
func (e *errorString) Error() string { return e.s }

继承和组合

在 Go 语言中没有继承的概念,利用组合达到代码复用的目的。

  • 接口组合:直接把接口组合一起,就具有所有方法
  • 结构体组合:不用写字段名,直接把其他结构体加入
    • 外部类型可以使用内部类型的字段和方法
    • 相同方法,外部类型方法覆盖内部类型,但不影响内部类型方法的实现

类型断言

用来判断一个接口的值是否是实现该接口的某个具体类型。

1
2
3
4
var s fmt.Stringer
s = p1
p2:=s.(*person) // var.(interfaceName) 接口断言
fmt.Println(p2)

错误处理

各种 errors

defer 关键字用于修饰一个函数或者方法,使得该函数或者方法在返回前才会执行,也就说被延迟,但又可以保证一定会执行。

可以有多个defer,倒序执行,和堆栈一样后进先出。

Panic 异常,运行时的问题会引起 panic 异常。

1
func panic(v interface{}) // interface{} 是空接口的意思,在 Go 语言中代表任意类型。

如果是不影响程序运行的错误,不要使用 panic,使用普通错误 error 即可。

内置的 recover 函数恢复 panic 异常,返回的值就是通过 panic 函数传递的参数值。一般配合 defer 关键字使用。

并发

进程和线程

  • 进程:启动一个软件,操作系统创建一个进程,是该软件工作空间,包括相关资源等。
  • 线程:进程的执行空间,一个进程可以有多个线程
    • 主线程,退出主线程,软件就退出
    • Go 语言中没有线程的概念
  • 协程(Goroutine),比线程更加轻量
    • 关键字 go function()
    • 不阻塞 main goroutine
    • 通过 channel 通信, ch := make(chan string)
      • 接收,获取 channel 中的值,<- chan
      • 发送,向 channel 发送值,chan <-
      • 无缓冲,发送和接收是同时进行,数据不停留,容量为0
      • 有缓冲,可阻塞队列,先进先出,指定容量 cachedCh := make(chan string, 5)
        • 发送:队尾插入,阻塞等待一个goroutine处理完成
        • 接收:对头弹出,队列为空等待一个goroutine执行插入元素
      • 通过 close(ch) 关闭channel,不能发送,但仍然能接收
      • 单向 channel,声明时带方向
      • select {case...} 类似 switch,case是可以操作的 channel,并自动监听 channel 数据产生

同步原语

同步原语通常用于更复杂的并发控制,如果追求更灵活的控制方式和性能

资源竞争:同一块内存被不同 goroutine 同时访问导致无法预料的情况。

go buildgo rungo test 添加 -race 标识查看

互斥锁 sync.Mutex:在同一时刻只有一个协程执行某段代码,其他协程都要等待该协程执行完毕后才能继续执行。

使用 Mutex 的 LockUnlock 方法,并且总是一对使用,中间的区域称为“临界区”。可以使用 defer mutex.Unlock() 确保解锁。

读写锁 sync.RWMutex : 读的时候可以同时进行

sync.WaitGroup: 跟踪多个协程,等待执行完毕才退出

  1. 声明一个 wg,然后 Add()一个数字
  2. 每运行完一个协程就是 Done() 一次,等于减少1
  3. 最后 Wait() 到数字减到0

sync.Once:只执行一次。
适用于创建某个对象的单例、只加载一次的资源等只执行一次的场景。

sync.Cond:条件变量,它具有阻塞协程和唤醒协程的功能,在满足一定条件的情况下唤醒协程。

  1. 生成一个 *sync.Cond
  2. 启动协程并 cond.Wait() 等待信号,需要 cond.L.Lock()
  3. cond.Broadcast() 发出信号
  4. cond.Signal() 方法用于唤醒一个等待时间最长的协程

Context

Context 是一个接口,它具备手动、定时、超时发出取消信号、传值等功能,主要用于控制多个协程之间的协作,尤其是取消操作。

1
2
3
4
5
6
type Context interface {
Deadline() (deadline time.Time, ok bool) // 获取设置的截止时间
Done() <-chan struct{} // 方法返回一个只读的 channel
Err() error // 返回取消的错误原因
Value(key interface{}) interface{} // 获取该 Context 上绑定的值,是一个键值对
}

Context 树 通过 go 不同函数生成不同 ctx 组成一棵 ctx 树。根本是一个空ctx,由context.Background()生成。

  • 空 ctx : context.Background()
  • 可取消 ctx : WithCancel(parent ctx)
  • timer ctx :
    • 定时取消:WithDeadline(parent ctx, d time.Time)
    • 超时取消:WithTimeout(parent ctx, timeout time.Time)
  • 值 ctx: WithValue(parent ctx, ke, val interface{}) 用于存储一个 key-value 键值对

使用原则:

  • 不要放在结构体中,要以参数的方式传递
  • 作为函数的参数时,要放在第一位
  • context.Background 函数生成根节点的 Context
  • Context 传值只传递必须的值
  • Context 多协程安全

高效并发模式

for select 循环模式

多路复用的并发模式,哪个 case 满足就执行哪个,直到满足一定的条件退出 for 循环。

  1. 无限循环模式
    1. 不断执行 default 语句
    2. 直到 done 通道关闭
  2. for range select 有限循环模式
    1. 有一个 done channel
    2. 一个 resultCh channel 用于接收循环的值

select timeout 模式

通过 time.After 函数设置一个超时时间

Pipeline 模式

  1. 由一道道工序组成,通过 channel 传递数据到下一道
  2. 每道工序对应一个函数,有协程处理数据,并放入channel返回
  3. 最终有一个组织者串联起来形成一个流水线

扇出和扇入模式

  1. 扇出,把数据同时发散出去
  2. 扇入,merge 组件把数据汇入
  3. 对 pipeline 模式的提升

Futures 模式

Futures 模式可以理解为未来模式,主协程不用等待子协程返回的结果,可以先去做其他事情,等未来需要子协程结果的时候再来取,如果子协程还没有返回结果,就一直等待。

大任务拆解到独立的并发子任务,最后得出大任务的结果。

深入理解

指针

  1. 不要对 map、slice、channel 这类引用类型使用指针;
  2. 如果需要修改方法接收者内部的数据或者状态时,需要使用指针;
  3. 如果需要修改参数的值或者内部数据时,也需要使用指针类型的参数;
  4. 如果是比较大的结构体,每次参数传递或者调用方法都要内存拷贝,内存占用多,这时候可以考虑使用指针;
  5. 像 int、bool 这样的小数据类型没必要使用指针;
  6. 如果需要并发安全,则尽可能地不要使用指针,使用指针一定要保证并发安全;
  7. 指针最好不要嵌套,也就是不要使用一个指向指针的指针,虽然 Go 语言允许这么做,但是这会使你的代码变得异常复杂。