文件组成
特点:
- Go是一门编译型语言,Go语言的工具链将源代码及其依赖转换成计算机的机器指令(译注:静态编译)
- Go语言原生支持Unicode,它可以处理全世界任何语言的文本
组成:
- 通过包(package)的方式组织代码
import
- 组成程序的声明种类
- 常量 -
const
- 变量 -
var
- 函数 -
func
- 类型 -
type
- 常量 -
- 初始化函数
init
- 可执行程序入口函数
main
例子:
// 声明一个可执行程序(main 比较特殊)
package main
// 通过包组织代码运行和调用
import "fmt"
// 常量 & 变量
const LEVEL = "V5"
var name string
// 初始化
func init() {
name = "中国"
}
// 程序执行入口
func main() {
fmt.Println(name, LEVEL, ":Hello word!")
}
基础语法
命名
一个名字必须以一个字母(Unicode字母)或下划线开头
,后面可以跟任意数量的字母、数字或下划线
区分大小写
开头字母的大小写决定了名字在包外的可见性。大写表示外部可见
( 中文汉字现在默认为小写,未来不一定),否则为包内可见
尽量使用驼峰式命名
方式,避免缩写单词大小写混用(setByCas => setByCAS
)
保留关键字如下,保留关键字不允许使用到命名中:
break default func interface select
case defer go map struct
chan else goto package switch
const fallthrough if range type
continue for import return var
预定义名称(可以使用作为命名):
内建常量: true false iota nil
内建类型: int int8 int16 int32 int64
uint uint8 uint16 uint32 uint64 uintptr
float32 float64 complex128 complex64
bool string
byte // 自动类型转换, 默认:int8 中文:int32,可用ASCII码 10进制、16进制、单引号 表示
rune // int32 的别名,代表一个Unicode码
error
内建函数: make len cap new append copy close delete
complex real imag
panic recover
变量
语法
var 变量名[ [*]类型] = 值
值和类型可以省略其中一个:
- 省略类型: 根据初始化表达式来推导变量的类型信息。
- 省略值 : 用零值初始化该变量。
短变量声明
- 声明和初始化
局部变量
- 允许部分变量已经声明;执行部分初始化,部分声明+初始化
- 不允许全部变量都声明;全部声明的变量不应该用
短变量声明
;应该用元组赋值-多重赋值
- 类型根据表达式来自动推导
零值
数值类型 `0`
布尔类型 `false`
字符串 `""`(空字符串)
接口或引用类型(包括slice、指针、map、chan和函数) nil
数组或结构体等聚合类型 每个元素或字段都是对应该类型的零值
指针
一个指针的值是另一个变量的地址。一个指针对应变量在内存中的存储位置。
p := &s
fmt.Printf("类型:%T \n", p)
fmt.Printf("指针变量地址:%p 指针变量内容:%v 调用时的地址:%p \n", &p, p, &*p)
fmt.Printf("底层数据地址:%p \n", &s)
// 类型:*string
// 指针地址:0xc000006030 指针内容:0xc00003a1f0 调用时的地址:0xc00003a1f0
// 底层数据地址:0xc00003a1f0
生命周期
- 全局变量:位于包一级的变量。它们的生命周期和整个程序的运行周期是一致的。
- 局部变量:位于函数内的变量。每次从创建一个新变量的声明语句开始,直到该变量不再被引用为止,然后变量的存储空间可能被回收
变量逃逸
:
针对局部变量的回收特性, 如果将指向短生命周期对象的指针保存到具有长生命周期的对象中,特别是保存到全局变量时,会阻止对短生命周期对象的垃圾回收(从而可能影响程序的性能) 。
说人话:当全局变量
指针指向局部变量
的内存地址(global = &part),就会导致内存不回收。
赋值
- 类型必须完全匹配,在赋值语句左边的变量和右边最终的求到的值必须有相同的数据类型
- nil可以赋值给任何指针或引用类型的变量
元组赋值
允许同时更新多个变量的值(多重赋值
可以用于交换两个变量)- 先右后左
- 将右边各个表达式的值赋值给左边对应位置的各个变量
类型
type 类型名字 底层类型
包
包的惯例
- 必须在头部声明自己的包名
- 每个包全部都在一个单独的目录里。一个目录不存在多个包、一个包不会存在多个目录。
- 包名应保持小写、简洁
main包
- 编译程序会将这种名字的包编译为二进制可执行文件。 可执行程序必须有一个
main
的包 - 必须存在一个
main()
函数
导入
导入使用关键字 import
;导入的方式分为以下三种:
- 远程导入
import "github.com/spf13/viper"
- 命名导入
import myviper "mylib/viper"
。一般用于解决冲突或者别名 - 空白标识符导入
import _ "db/driver/mysql"
。一般用于初始化代码才会如此
init函数
- 每个包可以包含多个
init()
函数。- 支持单个文件多个
- 支持多个文件多个
- 在
main()
之前运行
PS - 部分场景:在 init()
中初始化向某个插件注册功能。可执行程序处引用包含init()
的文件、包
执行顺序
- import subPkg. 引入包
- const. 常量定义
- var. 变量定义
- subPkg.init(). 引入包执行 init
- 以导入声明的顺序初始化
- init(). 优先包含 main函数的 init(), 然后其他 init()
- main(). 当前main
常见工具
go env
.查看环境变量go list
.查看依赖包go build
.编译包和依赖项go build -race
用竞争检测器来编译并执行
go run
.编译并运行Go程序go vet
.代码错误检查go fmt
.代码格式化go get
.下载 && 安装软件包和依赖(远程)go install
.编译 && 安装软件包和依赖项(本地)
数据类型
Go语言将数据类型分为四类:基础类型
、复合类型
、引用类型
和接口类型
。
- 基础类型:数字、字符串和布尔型。
- 复合类型:数组、结构体 是通过组合简单类型,来表达更加复杂的数据结构。
- 引用类型:指针、切片、映射、函数、通道
- 接口类型:接口
基础类型
运算符
优先级排序:
* / % << >> & &^
+ - | ^
== != < <= > >=
&&
||
%(取模):仅用于整数
间的运算。取模结果符号与被取模数值一致
-5%3 // -2
-5%-3 // -2
/(除法):用于整数时会采用去尾法
5/2 // 2
5.0/2.0 // 2.5
算数运算时,高位溢出直接丢弃
,无关符号
var u uint8 = 255
fmt.Println(u, u+1, u*u) // "255 0 1"
类型转换时:
- 大尺寸的整数类型转为一个小尺寸的整数类型 (int32 -> int16)
- 将一个浮点数转为整数,可能会改变数值或丢失精度 (float64 -> int32)
整型
带符号和无符号的不同位整型:int8、int16、int32、int64、uint8、uint16、uint32、uint64
int、uint
rune 通常用于表示一个Unicode码点(等价于 int32)
byte 字节内容(**等价于 uint8 **)
*uintptr 一般是写底层用的
浮点型
float32、float64
复数
complex64、complex128
布尔值
true、false
字符串
8位字节序列构成的字符串,约定但不必须是utf-8编码的文本。 字符串可以为空但不能是nil,其值不可变
字符串可以用==和<进行比较;比较通过逐个字节比较
完成的,因此比较的结果是字符串自然编码的顺序。
文本字符串通常被解释为采用UTF8编码的Unicode码点(rune)序列
len函数 可以返回一个字符串中的字节数目(不是rune字符数目)
len("汉") // 3. 中文 utf8 下为三个字节
连接通过 "+"号:
fmt.Println("你" + "好")
Unicode 符号和二进制编码的对应关系;
UTF-8 针对Unicode的可变长度字符
编码方式。最长的符号达到6个字节了(汉字一般是3个,还有其他的)
- 目前最长的符号达到6个字节了
- 汉字一般3字节,但还有其他的4字节;详见unicode码表
- mysql中常见的 utf8mb4 是为了兼容宽字节(也就是4个字节)的情况
tempStr := "汉1"
fmt.Printf("rune(char):%q\n", []rune(tempStr)) // rune(char):['汉' '1']
fmt.Printf("rune(hex):%x\n", []rune(tempStr)) // rune(hex):[6c49 31] -- Unicode 码值
fmt.Printf("bytes(hex):% x\n", []byte(tempStr)) // bytes(hex):e6 b1 89 31 -- UTF-8 编码后
// len 访问时
fmt.Println(len(tempStr)) // 4 => 字节数
// 访问指定偏移量时
fmt.Println(tempStr[2]) // 137 => 字节序访问
// 直接遍历字符串的时候
for _, val := range tempStr {
fmt.Println(val) // 27721 49 => unicode码值
}
// 上面的三个步骤说明如下:
// 1. 按照一个个字符拆分
// 2. 字符转为 unicode码表
// 3. 使用UTF-8转换为字节序
fmt.Println([]rune(tempStr)) // [27721 49] 十进制的
fmt.Println(string(27721), string(0x6c49)) // 汉 汉
// 递归字节切片
for _, val := range []byte(tempStr) {
fmt.Println(val)
}
// 230 177 137 49
常量
常量的数值
来源为基础类型
。表达式在编译期间计算,而不是运行中计算。
值不可修改
可使用的函数: len、cap、real、imag、complex和unsafe.Sizeof
iota 常量生成器
让编译器为每个常量复制相同的表达式
,直到声明区结束,或者遇到一个新的赋值语句。
- 只能在常量的表达式中使用
- 从0开始,每次递增1
- 每次 const 出现时,都会让 iota 初始化为0
- 同一代码块中允许多次使用 iota (不会重新计数),通过"="重新赋值后需要使用 iota 继续应用表达式
- 支持自定义枚举类型
- 也就是语言
基本类型
和对应的命名类型 - 注意
byte
和strings
- 也就是语言
- 可通过 “_” 跳过值
- 可通过 “=” 赋值其他值
声明示例:
const A = iota // 0
const (
B = iota // 0 -- 每次 const 出现时,都会让 iota 初始化为0
C // 1
D = "A" // A
E // A
F // A
)
const A = iota // 0
const (
B = iota // 0 -- 每次 const 出现时,都会让 iota 初始化为0
C // 1 -- 从0开始,每次递增1
D = "A" // A -- 可通过 "=" 赋值其他值
E = iota // 3 -- 同一代码块中允许多次使用 iota,通过"="重新赋值后需要使用 iota 继续应用表达式
F // 4
)
type Stereotype int
const (
TypicalNoob Stereotype = 2 * iota // 2 * 0 => 0 -- 支持自定义枚举类型
TypicalHipster // 2 * 1 => 2
TypicalUnixWizard // 2 * 2 => 4
TypicalStartupFounder // 2 * 3 => 6
)
const (
_ = iota // 0 -- 可通过 "_" 跳过值
_ // 1
V2 // 2
_ // 3
V4 // 4
)
常见用法:
type ByteSize float64
const (
_ = iota
KB ByteSize = 1 << (10 * iota) // 1 << (10*1)
MB // 1 << (10*2)
GB // 1 << (10*3)
TB // 1 << (10*4)
PB // 1 << (10*5)
EB // 1 << (10*6)
ZB // 1 << (10*7)
YB // 1 << (10*8)
)
复合类型
数组
底层特点
- 长度固定
- 元素类型相同
- 占用内存连续分配(元素间相邻)
- 访问任意下标元素的速度相同。(原因是内存连续 + 类型相同 可以定位指定偏移量)
- 函数传参时通过 值传递
使用示例
// 简单的声明 - 初始化为 "零值"
var array1 [5]int
fmt.Println(array1) // [0 0 0 0 0]
// 赋值初始化 - 初始化所有数值
var array2 = [5]int{10, 20, 30, 40, 50}
fmt.Println(array2) // [10 20 30 40 50]
// 自动推导长度
array3 := [...]int{10, 20, 30, 40, 50}
fmt.Println(array3) // [10 20 30 40 50]
// 赋值初始化 - 初始化指定下标
array4 := [5]int{1: 10, 2: 20}
fmt.Println(array4) //[0 10 20 0 0]
// 数组指针
array5 := [5]*int{0: new(int), 2: new(int)}
*array5[0] = 10
*array5[1] = 20
fmt.Println(array5) // [0xc000010148 <nil> 0xc000010180 <nil> <nil>
// 复制
var array6a [5]string
var array6b := [5]string{"Red", "Blue", "Green", "Yellow", "Pink"}
array6a = array6b // 长度、元素类型 相同才可以允许相互赋值
fmt.Println(array6a) // [Red Blue Green Yellow Pink]
// 指针 复制
// 指针数组 相互赋值 两边的指针依然是 --- "指向 相同的内存地址"
结构体
声明和使用
// 声明一个结构类型
type Man struct {
Name string
Age int
Height float64
int // 匿名字段. 必须不重复. 访问时为字段类型名
}
// 使用结构类型声明变量 & 初始化为零值
var zhangsan Man
// 使用结构类型声明变量 & 初始化信息
lisi := Man{
Name: "李四",
}
// 注意零值(默认值)
fmt.Println(zhangsan == Man{}, lisi) // true {李四 0 0}
内嵌结构体
- 默认访问最外层 字段、方法
- 最外层可以重新声明嵌入类型定义的 字段、方法
- 可以通过层级
独立
访问内嵌类型的方法
type Admin struct {
// 内嵌的结构体
User Man
Level int
}
teacher := Admin{
User: Man{
Name: "李四",
},
Level: 127,
}
引用类型
切片
底层特点
- 是一种数据结构。指针、长度、容量(指针指向
底层数组
下首个元素
的内存地址
) - 长度动态变更。通过
append
扩容、通过二次切片
缩小 函数传参
、return返回
时 复制切片本身,不涉及底层数组
- 容量必定大于等于长度
- 底层数组元素小于1000时,扩容系数为2;大于1000时扩容系数为1.25
PS: 解决在函数中产生扩容后 “新切片指针” 和 "旧切片指针"指向的底层数组不同的问题
// 分配包含 100 万个整型值的切片
slice := make([]int, 1e6)
// 将 slice 传递到函数 foo
slice = foo(slice)
// 函数 foo 接收一个整型切片,并返回这个切片
func foo(slice []int) []int {
...
return slice
}
使用示例
// 声明长度 默认容量
slice := make([]string, 5) // len:5 cap:5
// 声明长度 声明容量
slice := make([]string, 5, 6) // len:5 cap:6
// 初始化 所有值 自动推断长度、容量
slice := []string{"Red", "Blue", "Green", "Yellow", "Pink"} // len:5 cap:5
// 初始化 指定值 自动推断长度、容量
slice := []string{99: ""} // len:100 cap:100
// 指针说明
slice := []int{10, 20, 30, 40, 50}
newSlice := slice[1:3] // 此处指针后移至 索引"1"处; len:2 cap:4
newSlice[1] = 35 // 两个切片指向同一个底层数组,因此修改是同步的
newSlice = append(newSlice, 6) // 当底层容量大于长度时(未扩容) 指向的还是同一个底层数组
fmt.Println(slice, newSlice) // [10 20 35 6 50] [20 35 6]
// 针对 ·当底层容量大于长度时(未扩容) 指向的还是同一个底层数组· 的问题可以通过第三个参数解决
newSlice := slice[1:3:3] // 指定容量 slice[i:x:y] len:x-i cap:y-i
// 扩容后
newSliceAdd := append(slice, 5)
// 此处有两个特别的:
// 1. 由于本身切片属于隐性指针,所以打印内存地址的时候不需要加上 &slice
// 2. 这里的指针指向的是首个元素的内存地址, 所以起始位置不一样的也会不同
// 扩容其实就是创建一个更大的数组,然后把数据复制过去
fmt.Printf("old指针地址:%p new指针地址:%p \n", slice, newSliceAdd) //old指针地址:0xc00000c3c0 new指针地址:0xc00001c1e0
// 切片合并
s1 := []int{1, 2}
s2 := []int{3, 4}
fmt.Println(append(s1, s2...)) // [1 2 3 4]
// 切片迭代
for index, val := range slice {
// index, val 属于值传递 && 有效范围在代码块里面
fmt.Printf("%v = %v\n", index, val)
}
nil切片和空切片
不论nil 切片
还是空切片
,都没有分配任何存储空间。
nil 切片:声明
&不初始化
var slice []int
fmt.Printf("val:%v isnil:%v type:%T len:%d, cap:%d\n", slice, nil == slice, slice, len(slice), cap(slice))
// val:[] isnil:true type:[]int len:0, cap:0
空切片:声明
&初始化空切片
// 两种方式等价
slice := make([]int, 0)
slice := []int{}
fmt.Printf("val:%v isnil:%v type:%T len:%d, cap:%d\n", slice, nil == slice, slice, len(slice), cap(slice))
// val:[] isnil:false type:[]int len:0, cap:0
切片复制
copy返回被复制的元素数量,它会是 len(src) 和 len(dst) 中较小的那个。来源和目标的底层内存可以重叠
- 将元素从来源切片复制到目标切片中,
- 将字节从字符串复制到字节切片中
func copy
func copy(dst, src []Type) int
映射
底层特点
- 映射是一个存储键值对的无序集合. (底层为散列。TODO::书中没有详细备注,暂时理解为hashmap)
- 迭代时是无序的,可能每次的迭代顺序是不一样的
- 迭代时当类型
未定义
时返回对应类型的零值
- nil映射无法直接赋值
函数传参
时 会用很小的代价复制,不涉及底层数组
(TODO::后面详细看看为啥)
使用示例
// 创建一个映射
colors := make(map[string]string)
// 创建一个映射 并初始化
colors := map[string]string{"Red": "#da1337", "Orange": "#e95a22"}
colors["read"] = "#cccc"
fmt.Println(colors)
m := make(map[string]int)
// 添加 & 修改
m["Answer"] = 42
fmt.Println("The value:", m["Answer"])
m["Answer"] = 48
fmt.Println("The value:", m["Answer"])
for index := range m {
fmt.Println("for index:", index)
}
// 删除
delete(m, "Answer")
fmt.Println("The value:", m["Answer"])
// 检测某个键是否存在 -- 不存在 exists 为 false
value, exists := m["Answer"]
fmt.Println("The value:", value, "Present?", exists)
nil映射
nil 映射:声明
&不初始化
。nil映射无法直接赋
var dict map[string]int
fmt.Printf("val:%v isnil:%v type:%T\n", dict, nil == dict, dict)
// val:map[] isnil:true type:map[string]int
dict["aaa"] = 1 // 会报错:assignment to entry in nil map
函数&方法
语法
func name(parameter-list) (result-list) {
body
}
错误处理策略
-
直接返回子程序的错误
resp, err := http.Get(url) if err != nil{ return nill, err // 返回子程序错误 }
-
失败重试 + 超时时间(最大重试次数)
-
输出错误信息并结束程序
if err := WaitForServer(url); err != nil { fmt.Fprintf(os.Stderr, "Site is down: %v\n", err) os.Exit(1) // log.Fatalf("Site is down: %v\n", err) // 和上面相同效果的代码 }
-
只需要输出错误信息就足够了,不需要中断程序的运行
-
我们可以直接忽略掉错误
文件结尾错误(EOF)
in := bufio.NewReader(os.Stdin)
for {
r, _, err := in.ReadRune()
if err == io.EOF {
break // finished reading
}
if err != nil {
return fmt.Errorf("read failed:%v", err)
}
// ...use r…
}
匿名函数
函数字面量的语法和函数声明相似,区别在于func关键字后没有函数名。函数值字面量是一种表达式,它的值被成为匿名函数(anonymous function)。
通过这种方式定义的函数可以访问完整的词法环境(lexical environment),这意味着在函数中定义的内部函数可以引用该函数的变量
。
作用域导致的BUG
for _, i := range []byte("CBA") {
go func() {
fmt.Println(i) // 由于这里引用了块中的变量,所以运行的结果是一样的,!!!而不是预期的值!!!
}()
}
time.Sleep(2 * time.Second)
// 解决办法思路一致:增加副本
// 解决办法 1
for _, i := range []byte("CBA") {
i := i // 循环变量的副本
go func() {
fmt.Println(i)
}()
}
time.Sleep(2 * time.Second)
// 解决办法 2
for _, i := range []byte("CBA") {
go func(i byte) {
fmt.Println(i)
}(i) // 参数传递副本
}
time.Sleep(2 * time.Second)
可变参数
参数列表的最后一个参数
类型之前加上省略符号“…”,这表示该函数会接收任意数量的该类型参数。
func sum(vals ...int) {
fmt.Printf("参数类型:%T is nil:%v 参数值:%v \n", vals, vals == nil, vals)
}
sum() // 参数类型:[]int is nil:true 参数值:[]
sum(1, 2, 3) // 参数类型:[]int is nil:false 参数值:[1 2 3]
sum([]int{5, 6, 7}...) // 参数类型:[]int is nil:false 参数值:[5 6 7]
方法
如果一个函数有接收者,这个函数就被称为方法
接收者类型
它的作用是将函数与接收者的类型绑在一起
。一般情况下会使用其中一种,不会混用
- 值接收者。调用时会使用这个值的一个
副本
来执行 - 指针接收者。调用时能够修改其接收者
实际指向的值
指针重定向
指针接收者
的方法被调用时,接收者( 声明对应类型的值 )既能为值
(隐性转换为 &p)又能为指针
// 普通函数
func manChangeName(man *Man, name string) {
man.Name = name
}
m := Man{}
manChangeName(m, "new") // 编译错误!因为类型不对,函数定义传指针
manChangeName(&m,"new") // OK
// 方法
func (man *Man) changeName(name string) {
man.Name = name
// 对于下方的 "ma", 由于返回的就是指针,所以可以调用
// 对于下方的 "mv", 这里Go隐性转换了访问方式 (&p).Name = name
}
ma := &Man{} // 这里返回的是指针
ma.changeName("new") // OK
mv := Man{} // 这里返回的是值, 但为什么可以调用指针接收者的方法?见上面备注
mv.changeName("new") // OK
值接收者
的方法被调用时,接收者( 声明对应类型的值 )既能为值
又能为指针
(*隐性转换为 p)
// 普通函数
func manGetName(man Man) string {
return man.Name
}
m := Man{Name:"张三"}
manGetName(m ) // OK
manGetName(&m) // 编译错误!因为类型不对,函数定义传指针
// 方法
func (man *Man) getName() string {
return man.Name
// 对于下方的 "ma", 这里Go隐性转换了访问方式 (*p).Name . 指向底层数据
}
ma := &Man{} // 这里返回的是指针,但为什么可以调用值接收者的方法?见上面备注
ma.getName() // OK
mv := Man{} // 这里返回的是值
ma.getName() // OK
PS:所以要防止数据的指针传递时不小心修改到原始数据
m := &Man{Name:"张三"}
n := m
n.changeName("李四")
fmt.Println(n.getName(), m.getName()) // 李四 李四. !!!修改到原来的数据了!!!
fmt.Printf("n内存地址:%p m内存地址:%p \n", &*m, &*n) // 一样的
接口类型
接口是用来定义行为的类型。这些被定义的行为不由接口直接实现,而是通过方法由用户定义的类型实现
空接口
指定了零个方法的接口值被称为 *空接口:( **空接口可保存任何类型的值
*)
interface{}
// 接收 任意类型的 参数
func Decode(v interface{}) error {
// ...
}
底层原理
TODO::itable 什么的还是要最后确认后才可以确定;下面是初略的理解。
实体类型(用户定义的类型)赋值给接口时,接口通过两部分数据来保存对应的信息:
- 内部表 iTable 的地址
- 实体对象 类型. (值类型、指针类型)
- 方法集。内部映射了 接口类型声明的方法 - 实体对象方法地址(TODO::看报错像是这样的)
- 实体对象
值的地址
PS:实体对象 描述不正确,是自己瞎起的。就是 实体类型 初始化后
定义方法
type Man interface {
setName(n string) string
getName() string
online()
}
使用场景
直接赋值;获取实体类型的值和接口需要的方法集
// 假设 管理员角色 包含 人的结构体信息
man Man := Manager{Name:"张三", Online:false}
man.online() // 执行用户上线
man.setGuestAcl(....) // !!!这里要特别注意,此处是不可能调用到的!!!
类型约束;使用函数的时候进行类型约束,是否实现对应的方法集
man := Manager{Name:"张三", Online:false}
func resetDefaultUserName(m Man) {
_ := m.setName(DEFAULT_MAN_NAME)
man.setGuestAcl(....) // !!!这里要特别注意,此处也是不可能调用到的!!!
}
方法集规则
方法集定义了一组关联到实体类型的值或者指针的方法。(明确一点 int 和 *int 不是一个类型
)
实体类型定义方法时使用的接收者的类型决定了这个方法是关联到值,还是关联到指针,还是两个都关联
接收者类型 | 值类型 |
---|---|
指针接收者 (v *T) | 指针类型 |
值接收者(v T) | 指针类型 & 值类型 |
PS:特殊场景
package main
import "fmt"
// 声明一个 命名类型
type duration int
// 指针接收者类型
func (d *duration) pretty() string {
return fmt.Sprintf("Duration: %d", *d)
}
func main() {
// 默认 Go 会尽可能的进行 指针重定向;但已下会报错查找不到
duration(42).pretty()
// 这样却可以,因为隐性的转换了
d := duration(42)
d.pretty()
}
Goroutines和channel
goroutine
Go 语言的并发同步模型来自一个叫作通信顺序进程(Communicating Sequential Processes, CSP)的范型(paradigm)。 CSP 是一种消息传递模型,通过在 goroutine 之间传递数据来传递消息,而不是对数据进行加锁来实现同步访问。用于在 goroutine 之间同步和传递数据的关键数据类型叫作通道(channel)。
当主函数返回时,所有的goroutine都会直接打断,程序退出。
竞争状态
如果两个或者多个 goroutine 在没有互相同步的情况下,访问某个共享的资源,并试图同时读和写这个资源,就处于相互竞争的状态,这种情况被称作竞争状态(race candition)。
通过 go build -race
( -race
使用竞争检测器)来编译
并执行
发现问题
避免竞争状态的方法如下:
- 尽量避免写变量
- 避免从多个goroutine访问变量。通过单一goroutine访问,通过同步通道来设置和输出对应的值
- 允许很多goroutine去访问变量,但是在同一个时刻最多只有一个goroutine在访问
- 通过设置容量为1的buffer channel来限制同一时刻访问的goroutine数量
- 通过互斥锁 sync.Mutex 限制访问
共享资源锁
原子函数
以很底层的加锁机制来同步访问
整型变量和指针 。强制同一时刻只能有一个 goroutine 运行并完成
指定操作。
import "sync/atomic"
// 类型共六种:int32, int64, uint32, uint64, uintptr, unsafe.Pinter
// 操作共五种:增减, 比较并交换, 载入, 存储,交换
// 增减 => AddT => AddInt64
func AddInt32(addr *int32, delta int32) (new int32)
// 载入 => LoadT => LoadInt64
// 存储 => StoreT => StoreInt64
func LoadInt32(addr *int32) (val int32)
func StoreInt32(addr *int32, val int32)
// 交换 => SwapT => SwapInt64
func SwapInt32(addr *int32, new int32) (old int32)
// 比较并交换(就是 CAS) => CompareAndSwapT => CompareAndSwapInt64
func CompareAndSwapInt32(addr *int32, old, new int32) (swapped bool)
互斥锁
互斥锁用于在代码上创建一个临界区,保证同一时间只有一个 goroutine 可以执行这个临界区代码
。
当手动调用runtime.Gosched()
强制将当前 goroutine 退出当前线程后,调度器会再次分配这个 goroutine 继续运
行
import "sync"
var mutex sync.Mutex
mutex.Lock()
// ...临界区内容...
mutex.Unlock()
读写锁
允许多个只读操作并行执行,但写操作会完全互斥
这种锁叫作“多读单写”锁(multiple readers, single writer lock),Go语言提供的这样的锁是sync.RWMutex
RLock只能在临界区共享变量没有任何写入操作时可用
var mu sync.RWMutex
var balance int
func Balance() int {
mu.RLock() // readers lock
defer mu.RUnlock()
return balance
}
RWMutex需要更复杂的内部记录,所以会让它比一般的无竞争锁的mutex慢一些。
初始化懒加载
var loadIconsOnce sync.Once
var icons map[string]image.Image
// Concurrency-safe.
func Icon(name string) image.Image {
loadIconsOnce.Do(loadIcons) // 真正需要调用的时候初始化
return icons[name]
}
func loadIcons() {
icons = make(map[string]image.Image)
icons["spades.png"] = loadIcon("spades.png")
icons["hearts.png"] = loadIcon("hearts.png")
icons["diamonds.png"] = loadIcon("diamonds.png")
icons["clubs.png"] = loadIcon("clubs.png")
}
通道
可通过通道共享的内容:内置类型
,命名类型
,结构类型
,引用类型的值或者指针
读写未初始化的 chan
(nil)都会阻塞
若在关闭Channel后继续从中接收数据,接收者就会收到该Channel返回的零值
对于从无缓冲Channel进行的接收,发生在对该Channel进行的发送完成之前
对于带缓冲的Channel,对于Channel的第K
个接收完成操作发生在第K+C
个发送操作完成之前,其中C
是Channel的缓存大小。
// 无缓冲的通道
unbuffered := make(chan int)
// 有缓冲的通道
buffered := make(chan string, 10)
// 发送
buffered<-true
// 读取
val := <-buffered
val, isclose := <-buffered
// 关闭通道
close(buffered)
// 只写通道 out chan<- int
// 只读通道 in <-chan int
func squarer(out chan<- int, in <-chan int) {
for v := range in {
out <- v * v
}
close(out)
}
无缓冲的通道
- 发送者:阻塞等待
- 接收者:阻塞等待
- 同步方式:同步发送。保证发送、接收的goroutine会在同一时间进行数据交换
goroutines泄漏:
goroutines因为使用同步通道
没有人接收而被永远卡住
和垃圾变量不同,泄漏的goroutines并不会被自动回收,因此确保每个不再需要的goroutine能正常退出是重要的。
无缓存的Channel上的发送操作总在对应的接收操作完成前发生
有缓冲的通道
- 发送者:通道
没有可用缓冲区
容纳被发送的值时,发送动作阻塞
- 接收者: 通道中
没有要接收的值
时,接收动作阻塞
- 同步方式:异步发送。不保证同时交换数据
close
- 当通道
关闭后
, goroutine 依旧可以从通道接收数据
,但是不能再向通道里发送数据
。(保证数据不会丢失) - 从一个
已经关闭
且没有数据
的通道里获取数据,总会立刻返回
,并返回一个通道类型的零值
。(不能通过值判断是否有数据,而应该通过可选标志位获取通道状态) - 这个方法应该只
由发送者调用
, 而不是接收者。却调用的channel应该是双向的或者只写(chan<- Type)
select
语法特点:
- 表达式会被解析求值。求值顺序:自上而下、从左到右
- 多个同时到达则随机选择一个完成(CSP理论);存在default则执行default;否则阻塞等待
遍历方式总结
for-range
通道关闭时自动退出循环
for data := range ch {
// ...
}
for
通道关闭时手动断言关闭退出
for {
in, ok := <-ch
if !ok {
return // 通道关闭
}
// ....
}
for-select
通道关闭时手动置为nil
,避免总是获取关闭通道的分支
for {
select {
case n, ok := <-a1:
if !ok {
a1 = nil // 设置为nil不再进入该分支
} else {
c <- n
}
case n, _ := <-a2:
c <- n // 通道关闭后死循环了,每次都进入该分支
default:
fmt.Println("default")
time.Sleep(time.Second)
}
}
标准库学习
测试 testing
以 _test.go
为后缀名的源文件是go test
的一部分
在_test.go
文件中,有三种测试类型:
- 测试函数(格式:Test*)
- 用于测试程序的一些逻辑行为是否正确
- `go test命令会调用这些测试函数并报告测试结果是PASS或FAIL
- 基准测试函数(格式:Benchmark*)
- 用于衡量一些函数的性能
- go test命令会多次运行基准函数以计算一个平均的执行时间
- 示例函数 (格式:Example*)
测试覆盖率
使用go tool cover -html=cover.out
可以查看当前的测试覆盖率。
红色:未覆盖
绿色:覆盖
测试常用方法
在测试函数和基准测试中,会经常使用到以下的方法
是否失败
查询当前的测试函数是否已标记为失败
func (c *T) Failed() bool
标记失败
标记当前的测试函数测试失败
func (c *T) Fail() // 后续代码继续执行
func (c *T) FailNow() // 停止执行该测试
func (c *T) Failed() bool
错误日志
对于测试来说,格式化文本只会在测试失败或者设置了 -test.v 标志的情况下被打印出来
对于基准测试来说,为了避免 -test.v 标志的值对测试的性能产生影响, 格式化文本总会被打印出来
func (c *T) Log(args ...interface{})
func (c *T) Logf(format string, args ...interface{})
常用操作
// 等价于 Log[f] + Fail()
func (c *B) Error(args ...interface{})
func (c *B) Errorf(format string, args ...interface{})
// 等价于 Log[f] + FailNow()
func (c *B) Fatal(args ...interface{})
func (c *B) Fatalf(format string, args ...interface{})
测试函数 Test
运行指令:go test -v
必须导入 t *testing.T
名字必须以Test开头
, 可选的后缀名必须以大写字母开头
func TestName(t *testing.T) {
// ...
}
基准测试 Benchmark
运行指令:go test -test.bench=.*
必须导入 t *testing.B
名字必须以Benchmark开头
, 可选的后缀名必须以大写字母开头
import "testing"
func BenchmarkIsPalindrome(b *testing.B) {
// 指定执行 N 次
for i := 0; i < b.N; i++ {
IsPalindrome("A man, a plan, a canal: Panama")
}
}
示例函数 Example
名字必须以Example开头
, 可选的后缀名必须以大写字母开头
它的作用如下:
- 文档用例说明(
编译时会检查示例代码
) - 生成在线运行脚本