learn golang
快速入门相关
数据类型
基础类型
- 整形
- 带符号:int、int8、int16、int32 和 int64
负数,正数,0
- 无符号:uint、uint8、uint16、uint32 和 uint64
正数,0
- 字节类型 byte,等价于uint8,定义一个字节
- 带符号:int、int8、int16、int32 和 int64
- 浮点
float32float64
- 布尔
true/flase
- 字符串
- 可以表示为任意的数据,类型为
string - 可以使用
+和+=运算符连接
- 可以表示为任意的数据,类型为
- 零值
- 变量的默认值,没有初始化时候就是零值
- 数字为
0 - 布尔为
false - 字符串为
""
变量
变量简短声明:
1 | 变量名 := 表达式 |
指针
指针对应的是变量在内存中的存储位置
通过 & 获取变量的指针,通过 * 获取指针对应的变量值,指针对应的类型表示为 *类型
常量
1 | const 常量名 = 表达式 |
只允许布尔型、字符串、数字类型这些基础类型作为常量
iota
常量生成器,它可以用来初始化相似规则的常量,避免重复的初始化。
1 | const ( |
字符串
字符串和数字互转
- 与 int 转换:使用
strconv.Atoi及strconv.Itoa方法 - 与 float 转换:使用
strconv.ParseFloat和strconv.FormatFloat - 与 bool 转换:使用
strconv.ParseBool和strconv.FormatBool - 数字互转:使用
类型(要转换的变量)如float64(int)和int(float)…
strings 包
处理字符串的工具包,如查找字符串、去除字符串的空格、拆分字符串、判断字符串是否有某个前缀或者后缀等
1 | strings.HasPrefix(s, "h") |
逻辑
if…else
- 条件表达式不使用
() - 条件分支需要
{} {及 else 前的}不能独占一行- 可以多个
else if ... - if 语句中,可以有一个简单的表达式语句
switch 语句
- case 从上到下逐一进行判断,一旦满足条件,立即执行对应的分支并返回
fallthrough语句忽略当前分支,执行下面一条分支- 可以用一个简单的语句来做初始化,同样也是用分号
;分隔 - case 后的值就要和初始化表达式的结果类型相同
for 循环语句
- for 循环由三部分组成,其中,需要使用两个
;分隔 - 三部分组成都不是必须的
- 省略初始化和更新句,只保留循环条件,达到 while 的效果
- 支持使用
continue、break控制 for 循环
数据结构
数组
数组存放的是固定长度、相同类型的数据,而且这些存放的元素是连续的。
1 | list := [len]type{...} |
数组循环
使用 for range 直接输出对应的索引和值,不需要获取的值用 _ 下划线丢弃
切片
切片是基于数组实现的,它的底层就是一个数组。
1 | slice := array[start:end] |
但是经过切片后,切片的索引范围改变了。使用 len() 获取切片长度,使用 cap() 获取容量(即底层数组)。
切片声明
1 | slice := make([]type, len, cap) |
追加
1 | newSlice := append(oldSlice, ele1, ele2, ele3) |
在创建新切片的时候,最好要让新切片的长度和容量一样,这样在追加操作的时候就会生成新的底层数组,从而和原有数组分离,就不会因为共用底层数组导致修改内容的时候影响多个切片。
Map
- map 是一个无序的 K-V 键值对集合,结构为
map[K]V。 - Key 必须具有相同的类型,Value 也同样,但 Key 和 Value 的类型可以不同。
- Key 的类型必须支持
==比较运算符,这样才可以判断它是否存在,并保证 Key 的唯一。 - len(map) 获取元素个数
创建
1 | map := make(map[string]int) |
获取和删除
1 | ele, ok := map["a"] // ok 标记是否存在,存在为true |
遍历
1 | for k, v := range map {...} |
map 的遍历是无序的。可以先排序 key,然后循环获取对应的 value
String 和 []byte
字符串 string 也是一个不可变的字节序列,所以可以直接转为字节切片 []byte。
1 | s := "hello 世界" |
函数和方法
1 | func funcName(params) result { |
- 函数
- 能够多值返回
- 命名返回参数,可以bare return,但不鼓励使用
- 可变参数:在参数类型前加三个点
…func funcName(params ...type) result {...}- 可变参数是slice,使用
for range遍历 - 可变参数一定要放在参数列表的最后一个
- 包级函数
- 私有函数首字母小写,包外不能访问
- 导出的首字母大写
- 任何一个函数从属于一个包
- 匿名函数
- 可以赋给一个变量,但变量不是函数的名字
- 函数嵌套,闭包
- 函数也是一种类型,它也可以被用来声明函数类型的变量、参数或者作为另一个函数的返回值类型。
- 方法
- 必须有一个接受者,是一个类型,方法与类型绑定,成为这个类型的方法
- 接受者可以是一个指针类型。接受者的副本调用函数,无法修改接受者的值,只能使用指针。
- Go 语言会自动转义
- 选择值类型还是指针类型,按照需求是否要改变接收者的值而定
- 不管方法是否有参数,通过方法表达式调用,第一个参数必须是接收者,然后才是方法自身的参数。
结构体
结构体是一种聚合类型,里面可以包含任意类型的值。
结构体也是一种类型。
定义
1 | type structName struct { fieldName typeName } |
声明和访问
1 | var p person |
结构体的字段可以是任意类型,也包括自定义的结构体类型。
接口
高度抽象的类型,不用和具体的实现细节绑定在一起。
以 type 关键字开始,但接口的关键字是 interface,表示自定义的类型是一个接口。
1 | type interfaceName interface { funcName() returnName } |
实现
接口的实现者必须是一个具体的类型。
必须实现接口的每个方法才算是实现了这个接口。
面向接口编程的,只要一个类型实现了接口,都可以得到相应的结果,而不用管具体的类型实现。
以值类型接收者实现接口的时候,不管是类型本身,还是该类型的指针类型,都实现了该接口。当指针类型作为接收者时,只有指针类型实现了该接口。
工厂函数
创建自定义的结构体,也可以创建一个接口。
1 | // erros/erros.go |
继承和组合
在 Go 语言中没有继承的概念,利用组合达到代码复用的目的。
- 接口组合:直接把接口组合一起,就具有所有方法
- 结构体组合:不用写字段名,直接把其他结构体加入
- 外部类型可以使用内部类型的字段和方法
- 相同方法,外部类型方法覆盖内部类型,但不影响内部类型方法的实现
类型断言
用来判断一个接口的值是否是实现该接口的某个具体类型。
1 | var s fmt.Stringer |
错误处理
各种 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 数据产生
- 接收,获取 channel 中的值,
- 关键字
同步原语
同步原语通常用于更复杂的并发控制,如果追求更灵活的控制方式和性能
资源竞争:同一块内存被不同 goroutine 同时访问导致无法预料的情况。
go build、go run、go test添加-race标识查看
互斥锁 sync.Mutex:在同一时刻只有一个协程执行某段代码,其他协程都要等待该协程执行完毕后才能继续执行。
使用 Mutex 的 Lock 和 Unlock 方法,并且总是一对使用,中间的区域称为“临界区”。可以使用 defer mutex.Unlock() 确保解锁。
读写锁 sync.RWMutex : 读的时候可以同时进行
sync.WaitGroup: 跟踪多个协程,等待执行完毕才退出
- 声明一个 wg,然后
Add()一个数字 - 每运行完一个协程就是
Done()一次,等于减少1 - 最后
Wait()到数字减到0
sync.Once:只执行一次。
适用于创建某个对象的单例、只加载一次的资源等只执行一次的场景。
sync.Cond:条件变量,它具有阻塞协程和唤醒协程的功能,在满足一定条件的情况下唤醒协程。
- 生成一个
*sync.Cond - 启动协程并
cond.Wait()等待信号,需要cond.L.Lock()锁 cond.Broadcast()发出信号cond.Signal()方法用于唤醒一个等待时间最长的协程
Context
Context 是一个接口,它具备手动、定时、超时发出取消信号、传值等功能,主要用于控制多个协程之间的协作,尤其是取消操作。
1 | type Context interface { |
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 循环。
- 无限循环模式
- 不断执行 default 语句
- 直到 done 通道关闭
- for range select 有限循环模式
- 有一个 done channel
- 一个 resultCh channel 用于接收循环的值
select timeout 模式
通过 time.After 函数设置一个超时时间
Pipeline 模式
- 由一道道工序组成,通过 channel 传递数据到下一道
- 每道工序对应一个函数,有协程处理数据,并放入channel返回
- 最终有一个组织者串联起来形成一个流水线
扇出和扇入模式
- 扇出,把数据同时发散出去
- 扇入,merge 组件把数据汇入
- 对 pipeline 模式的提升
Futures 模式
Futures 模式可以理解为未来模式,主协程不用等待子协程返回的结果,可以先去做其他事情,等未来需要子协程结果的时候再来取,如果子协程还没有返回结果,就一直等待。
大任务拆解到独立的并发子任务,最后得出大任务的结果。
深入理解
指针
- 不要对 map、slice、channel 这类引用类型使用指针;
- 如果需要修改方法接收者内部的数据或者状态时,需要使用指针;
- 如果需要修改参数的值或者内部数据时,也需要使用指针类型的参数;
- 如果是比较大的结构体,每次参数传递或者调用方法都要内存拷贝,内存占用多,这时候可以考虑使用指针;
- 像 int、bool 这样的小数据类型没必要使用指针;
- 如果需要并发安全,则尽可能地不要使用指针,使用指针一定要保证并发安全;
- 指针最好不要嵌套,也就是不要使用一个指向指针的指针,虽然 Go 语言允许这么做,但是这会使你的代码变得异常复杂。