Golang:Go 语言学习
免责声明
本文转自李文周的博客,转载此文章仅为个人学习查阅,如有侵权,请联系博主进行删除。
原文作者:李文周
原文地址:https://www.liwenzhou.com/
Go基础篇
Go命令行工具
// 当前路径编译
go build
go build -o 新名字.exe
// 编译后可执行文件保留在命令执行的路径
go build 完整项目名
// 运行 go 文件
go run main.go
// 编译后移动可执行文件到 GOPATH/bin 目录
go install
// 查看go环境变量
go env
// 修改环境变量,direct 是一个特殊指示符,用于指示 Go 回到源地址去抓取(比如 GitHub 等)
go env -w GOPROXY=https://goproxy.cn,direct
go env -w GO111MODULE=on
// 初始化项目,完整项目名 通常设置为:域名/作者/项目名
go mod init 完整项目名
// 从网上下载源码到 $GOPATH/src/,-u表示强制更新现有依赖
go get -u gopl.io/ch1/helloworld
// 下载当前目录中go.mod所指定的依赖包
go mod download
跨平台编译
Windows编译可执行文件
cmd
SET CGO_ENABLED=0 // 禁用CGO
SET GOOS=linux // 目标平台是linux,目标平台为 mac 把 GOOS 改为 darwin 即可
SET GOARCH=amd64 // 目标处理器架构是amd64
PowerShell
$ENV:CGO_ENABLED=0
$ENV:GOOS="linux"
$ENV:GOARCH="amd64"
再执行下面的命令,得到能够在Linux平台运行的可执行文件
go build
Mac编译可执行文件
// 目标平台为 Windows 把 GOOS 改为 Windows 即可
CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build
Linux编译可执行文件
// 目标平台为 mac 把 GOOS 改为 darwin 即可
CGO_ENABLED=0 GOOS=windows GOARCH=amd64 go build
VSCode中配置Go
// go
"gopls": {
// 支持打开多个模块
"build.experimentalWorkspaceModule": true
},
"code-runner.executorMap": {
"go": "cd $dir && go run ."
},
语言特点
-
语句末尾不加分号
-
变量声明时会自动赋予默认值
-
整型和浮点型=>
0
-
字符串=>
""
-
布尔型=>
false
-
切片、函数、指针变量=>
nil
-
-
类型推导,在声明变量时可以自动判断变量类型,而不用声明变量类型
-
短变量声明:
:=
,只能用于函数体内部 -
匿名变量声明:
_
,表示占位,用于接收被忽略的值 -
函数体外部的每个语句都必须以关键字开始(var、const、func等)
-
不允许将整型强制转换为布尔型,布尔型无法参与数值运算,也无法与其他类型进行转换
-
字符串只能使用双引号
""
和反引号` `
而不能使用单引号''
-
反引号用于定义多行的字符串,多行字符串中的转义字符将失效
-
字符串拼接使用
+
变量
// 全局变量
var 变量名 变量类型
var 变量名 = 表达式
var 变量名 变量类型 = 表达式
var (
a string
b int
c bool
d float32
)
// 局部变量
// 同时声明多个变量,必须是相同的数据类型
var name, age = "张三", 20
// 短变量声明,必须在函数内部使用,属于局部变量
func main() {
n := 10
}
// 匿名变量声明,匿名变量不占用命名空间,不会分配内存
x, _ := a, b
常量
只需把 var
换成了 const
,常量在定义的时候必须赋值
const pi = 3.1415
// const同时声明多个常量时,如果省略了值则表示和上面一行的值相同
const (
pi = 3.1415
e
)
// 常量计数器,iota 可理解为 const 语句块中的行索引
// 在 const 关键字出现时将被重置为 0
const (
n1 = iota //0
n2 = 100 //100
n3 = iota //2
_
n5 //4
)
const n6 = iota //0
const (
a, b = iota + 1, iota + 2 //1,2
c, d //2,3
e, f //3,4
)
位移运算
<<
表示左移操作,1<<10
表示将 1
的二进制表示向左移10位,也就是由 1
变成了 10000000000
,也就是十进制的 1024
数据类型
int8 int16 int32 int64
uint8 uint16 uint32 uint64
uint int uintptr
float32 float64
complex64 complex128
bool
byte rune
内置函数
fmt.Printf() - 数据类型
len(v) - 获取对象的长度
strings.Split(string,sep) - 分割(需要导包)
strings.contains() - 判断是否包含
strings.HasPrefix() - 前缀判断
strings.HasSuffix() - 后缀判断
strings.Index() - 子串出现的位置
strings.LastIndex() - 子串出现的位置
strings.Join() - join操作
Number literals syntax
v := 0b00101101
, 代表二进制的 101101
,相当于十进制的 45
。 v := 0o377
,代表八进制的 377
,相当于十进制的 255
。 v := 0x1p-2
,代表十六进制的 1
除以 2²
,也就是 0.25
func main(){
var a int = 10
// 二进制
fmt.Printf("%b \n", a) // 1010
// 十进制
fmt.Printf("%d \n", a) // 10
// 八进制 以0开头
var b int = 077
fmt.Printf("%o \n", b) // 77
// 十六进制 以0x开头
var c int = 0xff
fmt.Printf("%x \n", c) // ff
fmt.Printf("%X \n", c) // FF
}
浮点型
func main() {
fmt.Printf("%f\n", math.Pi)
fmt.Printf("%.2f\n", math.Pi)
}
byte和rune类型
-
uint8
类型,或者叫byte
型,代表了ASCII码
的一个字符 -
rune
类型,代表一个UTF-8字符
-
当需要处理中文、日文或者其他复合字符时,则需要用到
rune
类型。 -
rune
类型实际是一个int32
func main() {
s := "hello 世界"
//byte
for i := 0; i < len(s); i++ {
fmt.Printf("%v(%c) ", s[i], s[i])
}
// 104(h) 101(e) 108(l) 108(l) 111(o) 32( ) 228(ä) 184(¸) 150() 231(ç) 149() 140()
//rune
for _, r := range s {
fmt.Printf("%v(%c) ", r, r)
}
// 104(h) 101(e) 108(l) 108(l) 111(o) 32( ) 19990(世) 30028(界)
}
字符串
要修改字符串,需要先将其转换成 []rune
或 []byte
,完成后再转换为 string
。无论哪种转换,都会重新分配内存,并复制字节数组
func main() {
s1 := "big"
// 强制类型转换
byteS1 := []byte(s1)
byteS1[0] = 'p'
fmt.Println(string(byteS1))
s2 := "白萝卜"
runeS2 := []rune(s2)
runeS2[0] = '红'
fmt.Println(string(runeS2))
}
判断结构
func main() {
if score := 65; score >= 90 {
fmt.Println("A")
} else if score > 75 {
fmt.Println("B")
} else {
fmt.Println("C")
}
}
循环结构
func main() {
i := 0
for ; i < 10; i++ {
fmt.Println(i)
}
j := 0
for j < 10 {
fmt.Println(j)
j++
}
for {
fmt.Println("hi")
}
}
for range
循环可以遍历数组、切片、字符串、map 及 通道(channel)
Switch Case
func main() {
switch n := 7; n {
case 1, 3, 5, 7, 9:
fmt.Println("奇数")
case 2, 4, 6, 8:
fmt.Println("偶数")
default:
fmt.Println(n)
}
}
Break
func main() {
forloop1:
for i := 0; i < 10; i++ {
// forloop2:
for j := 0; j < 10; j++ {
if j == 2 {
break forloop1
}
fmt.Printf("%v-%v\n", i, j)
}
}
fmt.Println("...")
}
Continue
func main() {
forloop1:
for i := 0; i < 5; i++ {
// forloop2:
for j := 0; j < 5; j++ {
if i == 2 && j == 2 {
continue forloop1
}
fmt.Printf("%v-%v\n", i, j)
}
}
}
Array
- 定义:
var name [元素数量]Type
- 数组的长度必须是常量,并且长度是数组类型的一部分,长度不同的数组类型也不同,数组一旦定义,长度就不能改变
- 多维数组只有第一层可以使用
...
进行数组长度推导 - 数组是值类型,赋值和传参会复制整个数组,因此改变副本的值,不会改变本身的值
[n]*T
表示指针数组,*[n]T
表示数组指针
初始化数组
一维
func main() {
// 初始化为 int 类型的 [0 0 0]
var array1 [3]int
// 使用指定的初始值 [1 2 0]
var array2 = [3]int{1, 2}
// 自动推断数组的长度 [北京 上海 深圳]
var array3 = [...]string{"北京", "上海", "深圳"}
// 使用指定索引值 [0 1 0 5]
array4 := [...]int{1: 1, 3: 5}
fmt.Println(array1)
fmt.Println(array2)
fmt.Println(array3)
fmt.Println(array4)
}
多维
func main() {
a := [3][2]string{
{"北京", "上海"},
{"广州", "深圳"},
{"成都", "重庆"},
}
fmt.Println(a) // [[北京 上海] [广州 深圳] [成都 重庆]]
fmt.Println(a[2][1]) // 重庆
}
遍历数组
一维
func main() {
var a = [...]string{"北京", "上海", "深圳"}
// 方法1:for循环遍历
for i := 0; i < len(a); i++ {
fmt.Println(a[i])
}
// 方法2:for range遍历
for index, value := range a {
fmt.Println(index, value)
}
}
多维
func main() {
array := [...][2]string{
{"北京", "上海"},
{"广州", "深圳"},
{"成都", "重庆"},
}
for _, v1 := range array {
for _, v2 := range v1 {
fmt.Printf("%s\t", v2)
}
fmt.Println()
}
}
切片
切片是一个引用类型,它的内部结构包含 地址
、长度
和 容量
声明:var name []Type
初始化:var name []Type{}
内置的 len()
函数求切片的长度,内置的 cap()
函数求切片的容量
简单表达式
func main() {
a := []int{1, 2, 3, 4, 5}
s := a[1:3]
fmt.Printf("s:%v len(s):%v cap(s):%v\n", s, len(s), cap(s)) // s:[2 3] len(s):2 cap(s):4
}
完整表达式
表达式为 a[low:high:max]
,最终得到的是一个长度是 high - low
,容量为 max - low
的切片
func main() {
a := []int{1, 2, 3, 4, 5}
t := a[1:3:5]
fmt.Printf("t:%v len(t):%v cap(t):%v\n", t, len(t), cap(t)) // t:[2 3] len(t):2 cap(t):4
}
make()函数
定义:make([]Type, size, cap)
func main() {
a := make([]int, 2, 10)
fmt.Printf("a:%v len(a):%v cap(a):%v\n", a, len(a), cap(a))
}
要检查切片是否为空,请始终使用 len(a) == 0
来判断,而不应该使用 a == nil
来判断
赋值拷贝
切片是引用类型,都指向了同一块内存地址,赋值拷贝后两个变量共享底层数组
func main() {
s1 := make([]int, 3) //[0 0 0]
s2 := s1 //将s1直接赋值给s2,s1和s2共用一个底层数组
s2[0] = 100
fmt.Println(s1) //[100 0 0]
fmt.Println(s2) //[100 0 0]
}
copy()函数
定义:copy(destSlice, srcSlice []Type)
-
srcSlice: 数据来源切片
-
destSlice: 目标切片
func main() {
a := []int{1, 2, 3, 4, 5}
c := make([]int, 5, 5)
copy(c, a)
fmt.Println(a) //[1 2 3 4 5]
fmt.Println(c) //[1 2 3 4 5]
c[0] = 1000
fmt.Println(a) //[1 2 3 4 5]
fmt.Println(c) //[1000 2 3 4 5]
}
遍历切片
func main() {
s := []int{1, 3, 5}
// 方法1:for循环遍历
for i := 0; i < len(s); i++ {
fmt.Println(i, s[i])
}
// 方法2:for range遍历
for index, value := range s {
fmt.Println(index, value)
}
}
append()函数
func main() {
var citySlice []string
citySlice = append(citySlice, "北京")
citySlice = append(citySlice, "上海", "广州", "深圳")
a := []string{"成都", "重庆"}
citySlice = append(citySlice, a...)
fmt.Println(citySlice) //[北京 上海 广州 深圳 成都 重庆]
}
切片扩容策略
newcap := old.cap
doublecap := newcap + newcap
if cap > doublecap {
newcap = cap
} else {
if old.len < 1024 {
newcap = doublecap
} else {
// Check 0 < newcap to detect overflow
// and prevent an infinite loop.
for 0 < newcap && newcap < cap {
newcap += newcap / 4
}
// Set newcap to the requested cap when
// the newcap calculation overflowed.
if newcap <= 0 {
newcap = cap
}
}
}
删除元素
从切片a中删除索引为index
的元素:a = append(a[:index], a[index+1:]...)
func main() {
// 从切片中删除元素
a := []int{30, 31, 32, 33, 34, 35, 36, 37}
// 要删除索引为2的元素
a = append(a[:2], a[3:]...)
fmt.Println(a) //[30 31 33 34 35 36 37]
}
区分数组与切片
var arr [5]int
slice := make([]int,3,5)
func main() {
// 在 [] 操作符中设定数值的叫数组
var arr1 [5]int
arr2 := [5]int{1, 2, 3}
arr3 := [...]int{1, 2, 3}
// arr1 => type:[5]int len:5 cap:5 value:[0 0 0 0 0]
fmt.Printf("arr1 => type:%T len:%v cap:%v value:%v\n", arr1, len(arr1), cap(arr1), arr1)
// arr2 => type:[5]int len:5 cap:5 value:[1 2 3 0 0]
fmt.Printf("arr2 => type:%T len:%v cap:%v value:%v\n", arr2, len(arr2), cap(arr2), arr2)
// arr3 => type:[3]int len:3 cap:3 value:[1 2 3]
fmt.Printf("arr3 => type:%T len:%v cap:%v value:%v\n", arr3, len(arr3), cap(arr3), arr3)
// [] 中未设定数值的叫切片
var slice1 []int // nil 切片
slice2 := []int{} // 空切片
slice3 := []int{1, 2, 3}
slice4 := make([]int, 0) // 空切片
slice5 := make([]int, 3, 5) // 零切片
// slice1 => type:[]int len:0 cap:0 value:[]
fmt.Printf("slice1 => type:%T len:%v cap:%v value:%v\n", slice1, len(slice1), cap(slice1), slice1)
// slice2 => type:[]int len:0 cap:0 value:[]
fmt.Printf("slice2 => type:%T len:%v cap:%v value:%v\n", slice2, len(slice2), cap(slice2), slice2)
// slice3 => type:[]int len:3 cap:3 value:[1 2 3]
fmt.Printf("slice3 => type:%T len:%v cap:%v value:%v\n", slice3, len(slice3), cap(slice3), slice3)
// slice4 => type:[]int len:0 cap:0 value:[]
fmt.Printf("slice4 => type:%T len:%v cap:%v value:%v\n", slice4, len(slice4), cap(slice4), slice4)
// slice5 => type:[]int len:3 cap:5 value:[0 0 0]
fmt.Printf("slice5 => type:%T len:%v cap:%v value:%v\n", slice5, len(slice5), cap(slice5), slice5)
}
map
定义:make(map[KeyType]ValueType, [cap])
func main() {
myMap := make(map[string]string, 8)
myMap["key"] = "value"
fmt.Println(myMap) // map[key:value]
fmt.Println(myMap["key"]) // value
fmt.Printf("type of myMap:%T\n", myMap) // type of myMap:map[string]string
// 判断某个键是否存在
v, ok := myMap["key"]
if ok {
fmt.Printf("value is: %s", v)
} else {
fmt.Print("no value")
}
}
func main() {
userInfo := map[string]string{
"username": "root",
"password": "123456",
}
fmt.Println(userInfo)
}
遍历map
遍历map时的元素顺序与添加键值对的顺序无关
func main() {
scoreMap := make(map[string]int)
scoreMap["张三"] = 70
scoreMap["李四"] = 80
scoreMap["王五"] = 90
for k, v := range scoreMap {
fmt.Println(k, v)
}
for k := range scoreMap {
fmt.Println(k)
}
}
delete()函数
定义:delete(map, key)
func main() {
scoreMap := make(map[string]int)
scoreMap["张三"] = 70
scoreMap["李四"] = 80
scoreMap["王五"] = 90
delete(scoreMap, "李四")
for k, v := range scoreMap {
fmt.Println(k, v)
}
}
指定顺序遍历map
func main() {
rand.Seed(time.Now().UnixNano()) //初始化随机数种子
var scoreMap = make(map[string]int, 200)
for i := 0; i < 100; i++ {
key := fmt.Sprintf("stu%02d", i) //生成stu开头的字符串
value := rand.Intn(100) //生成0~99的随机整数
scoreMap[key] = value
}
//取出map中的所有key存入切片keys
var keys = make([]string, 0, 200)
for key := range scoreMap {
keys = append(keys, key)
}
//对切片进行排序
sort.Strings(keys)
//按照排序后的key遍历map
for _, key := range keys {
fmt.Println(key, scoreMap[key])
}
}
切片中存map
map类型的切片
func main() {
var mapSlice = make([]map[string]string, 3)
for index, value := range mapSlice {
fmt.Printf("index:%d value:%v\n", index, value)
}
fmt.Println("after init")
mapSlice[0] = make(map[string]string, 10)
mapSlice[0]["name"] = "root"
mapSlice[0]["password"] = "123456"
for index, value := range mapSlice {
fmt.Printf("index:%d value:%v\n", index, value)
}
}
map中存切片
切片类型的map
func main() {
var sliceMap = make(map[string][]string, 3)
fmt.Println(sliceMap)
fmt.Println("after init")
key := "address"
value, ok := sliceMap[key]
if !ok {
value = make([]string, 0, 2)
}
value = append(value, "北京", "上海", "广州", "深圳")
sliceMap[key] = value
fmt.Println(sliceMap)
}
函数
定义
func sayHello() {
fmt.Println("Hello")
}
func intSum(x int, y int) int {
return x + y
}
// 类型简写
func intSum(x, y int) int {
return x + y
}
// 可变参数,固定参数搭配可变参数使用时,可变参数要放在固定参数的后面
func intSum(x int, y ...int) int {
fmt.Println(x, y)
sum := x
for _, v := range y {
sum = sum + v
}
return sum
}
// 函数调用
ret := intSum3(100, 10, 20, 30)
fmt.Println(ret) // 160
返回值
// 多值返回
func calc(x, y int) (int, int) {
sum := x + y
sub := x - y
return sum, sub
}
// 返回值命名
func calc(x, y int) (sum, sub int) {
sum = x + y
sub = x - y
return
}
// 返回值补充
func someFunc(x string) []int {
if x == "" {
return nil // 没必要返回[]int{}
}
...
}
变量作用域
全局变量
// 定义全局变量num
var num int64 = 10
func testGlobalVar() {
fmt.Printf("num=%d\n", num) // 函数中可以访问全局变量num
}
func main() {
testGlobalVar() // num=10
}
局部变量
func testLocalVar() {
// 定义一个函数局部变量x,仅在该函数内生效
var x int64 = 100
fmt.Printf("x=%d\n", x)
}
func main() {
testLocalVar()
fmt.Println(x) // 此时无法使用变量x
}
变量优先级
如果局部变量和全局变量重名,优先访问局部变量
//定义全局变量num
var num int64 = 10
func testNum() {
num := 100
fmt.Printf("num=%d\n", num) // 函数中优先使用局部变量
}
func main() {
testNum() // num=100
}
函数类型与变量
type calculation func(int, int) int
// 满足条件的函数都是 calculation 类型的函数
func add(x, y int) int {
return x + y
}
func sub(x, y int) int {
return x - y
}
func main() {
// add 和 sub 都能赋值给calculation类型的变量
var c calculation
c = add
fmt.Println(c(1, 2))
d := sub
fmt.Println(d(2, 1))
}
高阶函数
函数作为参数
func add(x, y int) int {
return x + y
}
func calc(x, y int, op func(int, int) int) int {
return op(x, y)
}
func main() {
ret := calc(10, 20, add)
fmt.Println(ret) //30
}
函数作为返回值
func add(x, y int) int {
return x + y
}
func sub(x, y int) int {
return x - y
}
func do(s string) (func(int, int) int, error) {
switch s {
case "+":
return add, nil
case "-":
return sub, nil
default:
err := errors.New("无法识别的操作符")
return nil, err
}
}
匿名函数
匿名函数需要保存到某个变量或者作为立即执行函数,多用于实现回调函数和闭包
func main() {
// 将匿名函数保存到变量
add := func(x, y int) {
fmt.Println(x + y)
}
add(10, 20) // 通过变量调用匿名函数
//自执行函数:匿名函数定义完加()直接执行
func(x, y int) {
fmt.Println(x + y)
}(10, 20)
}
闭包
闭包=函数+引用环境
func adder() func(int) int {
var x int
return func(y int) int {
x += y
return x
}
}
func main() {
var f = adder()
fmt.Println(f(10)) //10
fmt.Println(f(20)) //30
fmt.Println(f(30)) //60
f1 := adder()
fmt.Println(f1(40)) //40
fmt.Println(f1(50)) //90
}
func adder2(x int) func(int) int {
return func(y int) int {
x += y
return x
}
}
func main() {
var f = adder2(10)
fmt.Println(f(10)) //20
fmt.Println(f(20)) //40
fmt.Println(f(30)) //70
f1 := adder2(20)
fmt.Println(f1(40)) //60
fmt.Println(f1(50)) //110
}
func makeSuffixFunc(suffix string) func(string) string {
return func(name string) string {
if !strings.HasSuffix(name, suffix) {
return name + suffix
}
return name
}
}
func main() {
jpgFunc := makeSuffixFunc(".jpg")
txtFunc := makeSuffixFunc(".txt")
fmt.Println(jpgFunc("test")) //test.jpg
fmt.Println(txtFunc("test")) //test.txt
}
func calc(base int) (func(int) int, func(int) int) {
add := func(i int) int {
base += i
return base
}
sub := func(i int) int {
base -= i
return base
}
return add, sub
}
func main() {
f1, f2 := calc(10)
fmt.Println(f1(1), f2(2)) //11 9
fmt.Println(f1(3), f2(4)) //12 8
fmt.Println(f1(5), f2(6)) //13 7
}
defer语句
将 defer
后面跟随的语句进行延迟处理,先被 defer
的语句最后被执行,最后被 defer
的语句,最先被执行。由于 defer
语句延迟调用的特性,所以 defer
语句能非常方便的处理资源释放问题。比如:资源清理、文件关闭、解锁及记录时间等。
func main() {
fmt.Println("start")
defer fmt.Println(1)
defer fmt.Println(2)
defer fmt.Println(3)
fmt.Println("end")
}
输出:
start
end
3
2
1
defer执行时机
在Go语言的函数中 return
语句在底层并不是原子操作,它分为给返回值赋值和RET指令两步。而 defer
语句执行的时机就在返回值赋值操作后,RET指令执行前。具体如下图所示:
defer经典案例
func f1() int {
x := 5
defer func() {
x++
}()
return x
}
func f2() (x int) {
defer func() {
x++
}()
return 5
}
func f3() (y int) {
x := 5
defer func() {
x++
}()
return x
}
func f4() (x int) {
defer func(x int) {
x++
}(x)
return 5
}
func main() {
fmt.Println(f1())
fmt.Println(f2())
fmt.Println(f3())
fmt.Println(f4())
}
defer面试题
defer注册要延迟执行的函数时,该函数所有的参数都需要确定其值
func calc(index string, a, b int) int {
ret := a + b
fmt.Println(index, a, b, ret)
return ret
}
func main() {
x := 1
y := 2
defer calc("AA", x, calc("A", x, y))
x = 10
defer calc("BB", x, calc("B", x, y))
y = 20
}
内置函数
内置函数 | 介绍 |
---|---|
close | 主要用来关闭channel |
len | 用来求长度,比如string、array、slice、map、channel |
new | 用来分配内存,主要用来分配值类型,比如int、struct。返回的是指针 |
make | 用来分配内存,主要用来分配引用类型,比如chan、map、slice |
append | 用来追加元素到数组、slice中 |
panic和recover | 用来做错误处理 |
异常处理
panic
可以在任何地方引发,但 recover
只有在 defer
调用的函数中有效
func funcA() {
fmt.Println("func A")
}
func funcB() {
panic("panic in B")
}
func funcC() {
fmt.Println("func C")
}
func main() {
funcA()
funcB()
funcC()
}
程序运行期间 funcB
中引发了 panic
导致程序崩溃,异常退出了。这个时候我们就可以通过 recover
将程序恢复回来,继续往后执行
func funcA() {
fmt.Println("func A")
}
func funcB() {
defer func() {
err := recover()
//如果程序出出现了panic错误,可以通过recover恢复过来
if err != nil {
fmt.Println("recover in B")
}
}()
panic("panic in B")
}
func funcC() {
fmt.Println("func C")
}
func main() {
funcA()
funcB()
funcC()
}
注意:
recover()
必须搭配defer
使用。defer
一定要在可能引发panic
的语句之前定义。
指针
func main() {
a := 10
b := &a
fmt.Printf("a:%d ptr:%p\n", a, &a) // a:10 ptr:0xc00001a078
fmt.Printf("b:%p type:%T\n", b, b) // b:0xc00001a078 type:*int
fmt.Println(&b) // 0xc00000e018
}
指针取值
func main() {
a := 10
b := &a // 取变量a的地址,将指针保存到b中
fmt.Printf("type of b:%T\n", b)
c := *b // 指针取值(根据指针去内存取值)
fmt.Printf("type of c:%T\n", c)
fmt.Printf("value of c:%v\n", c)
}
输出
type of b:*int
type of c:int
value of c:10
总结:取地址操作符&
和取值操作符*
是一对互补操作符,&
取出地址,*
根据地址取出地址指向的值。
变量、指针地址、指针变量、取地址、取值的相互关系和特性如下:
- 对变量进行取地址(&)操作,可以获得这个变量的指针变量。
- 指针变量的值是指针地址。
- 对指针变量进行取值(*)操作,可以获得指针变量指向的原变量的值。
指针传值
func modify1(x int) {
x = 100
}
func modify2(x *int) {
*x = 100
}
func main() {
a := 10
modify1(a)
fmt.Println(a) // 10
modify2(&a)
fmt.Println(a) // 100
}
内存分配
func main() {
var a *int
*a = 100
fmt.Println(*a)
var b map[string]int
b["Go"] = 100
fmt.Println(b)
}
执行上面的代码会引发panic,为什么呢? 在Go语言中对于引用类型的变量,我们在使用的时候不仅要声明它,还要为它分配内存空间,否则我们的值就没办法存储。而对于值类型的声明不需要分配内存空间,是因为它们在声明的时候已经默认分配好了内存空间。Go语言中new和make是内建的两个函数,主要用来分配内存。
new
new是一个内置的函数,它的函数签名如下:
func new(Type) *Type
- Type表示类型,new函数只接受一个参数,这个参数是一个类型
- *Type表示类型指针,new函数返回一个指向该类型内存地址的指针
new函数不太常用,使用new函数得到的是一个类型的指针,并且该指针对应的值为该类型的零值
func main() {
a := new(int)
b := new(bool)
fmt.Printf("%T\n", a) // *int
fmt.Printf("%T\n", b) // *bool
fmt.Println(*a) // 0
fmt.Println(*b) // false
}
开始的示例代码中 var a *int
只是声明了一个指针变量a但是没有初始化,指针作为引用类型需要初始化后才会拥有内存空间,才可以给它赋值。应该按照如下方式使用内置的new函数对a进行初始化之后就可以正常对其赋值
func main() {
var a *int
a = new(int)
*a = 10
fmt.Println(*a)
}
make
make也是用于内存分配的,区别于new,它只用于slice、map以及chan的内存创建,而且它返回的类型就是这三个类型本身,而不是他们的指针类型,因为这三种类型就是引用类型,所以就没有必要返回他们的指针了。make函数的函数签名如下:
func make(t Type, size ...IntegerType) Type
make函数是无可替代的,我们在使用slice、map以及channel的时候,都需要使用make进行初始化,然后才可以对它们进行操作
开始的示例中var b map[string]int
只是声明变量b是一个map类型的变量,需要像下面的示例代码一样使用make函数进行初始化操作之后,才能对其进行键值对赋值
func main() {
var b map[string]int
b = make(map[string]int, 10)
b["Go"] = 100
fmt.Println(b)
}
new与make的区别
- 二者都是用来做内存分配的
- make只用于slice、map以及channel的初始化,返回的还是这三个引用类型本身
- 而new用于类型的内存分配,并且内存对应的值为类型零值,返回的是指向类型的指针
type
自定义类型
type NewInt int
类型别名
type MyInt = int
type byte = uint8
type rune = int32
区别
// 自定义类型
type NewInt int
// 类型别名
type MyInt = int
func main() {
var a NewInt
var b MyInt
fmt.Printf("type of a:%T\n", a) //type of a:main.NewInt
fmt.Printf("type of b:%T\n", b) //type of b:int
}
结果显示
a(自定义)类型是 main.NewInt
,表示main包下定义的 NewInt
类型。
b(别名)类型是 int
,MyInt
类型只会在代码中存在,编译完成时并不会有 MyInt
类型。
结构体
Go语言中通过 struct
来实现面向对象
type 类型名 struct {
字段名 字段类型
字段名 字段类型
…
}
type person struct {
name string
city string
age int8
}
type person1 struct {
name, city string
age int8
}
- 类型名:标识自定义结构体的名称,在同一个包内不能重复。
- 字段名:表示结构体字段名。结构体中的字段名必须唯一。
- 字段类型:表示结构体字段的具体类型。
结构体实例化
func main() {
var p person
p.name = "张三"
p.city = "深圳"
p.age = 18
fmt.Printf("p=%v\n", p) // p1={张三 深圳 18}
fmt.Printf("p=%#v\n", p) // p1=main.person{name:"张三", city:"深圳", age:18}
}
注:
%v // 只输出所有的值
%+v // 先输出字段名字,再输出该字段的值
%#v // 先输出结构体名,再输出结构体值(字段名字+字段的值)
匿名结构体
func main() {
var user struct {
name string
age int
}
user.name = "张三"
user.age = 18
fmt.Printf("%#v\n", user) // struct { name string; age int }{name:"张三", age:18}
}
指针结构体
Go语言中支持对结构体指针直接使用 .
来访问结构体的成员
func main() {
var p = new(person)
p.name = "张三"
p.age = 18
p.city = "深圳"
fmt.Printf("%T\n", p) // *main.person
fmt.Printf("p=%#v\n", p) // p=&main.person{name:"张三", city:"深圳", age:18}
}
取结构体的地址实例化
使用 &
对结构体进行取地址操作,相当于对该结构体类型进行了一次 new
实例化操作
func main() {
p := &person{}
fmt.Printf("%T\n", p) // *main.person
fmt.Printf("p=%#v\n", p) // p=&main.person{name:"", city:"", age:0}
p.name = "张三"
p.age = 18
p.city = "深圳"
fmt.Printf("p=%#v\n", p) // p=&main.person{name:"张三", city:"深圳", age:18}
}
p.name = "张三"
其实在底层是 (*p).name = "张三"
,这是Go语言帮我们实现的语法糖
结构体初始化
没有初始化的结构体,其成员变量都是对应其类型的零值
func main() {
var p person
fmt.Printf("p=%#v\n", p) // p=main.person{name:"", city:"", age:0}
}
func main() {
// 使用键值对初始化
p := person{
name: "张三",
city: "深圳",
age: 18,
}
fmt.Printf("p=%#v\n", p) // p=main.person{name:"张三", city:"深圳", age:18}
}
func main() {
// 对结构体指针进行键值对初始化
p := &person{
city: "深圳",
age: 18,
}
fmt.Printf("p=%#v\n", p) // p=&main.person{name:"", city:"深圳", age:18}
}
func main() {
// 使用值的列表初始化
p := &person{
"张三",
"深圳",
18,
}
fmt.Printf("p=%#v\n", p) // p=&main.person{name:"张三", city:"深圳", age:18}
}
结构体内存布局
结构体占用一块连续的内存
func main() {
type test struct {
a int8
b int8
c int8
d int8
}
n := test{
1, 2, 3, 4,
}
fmt.Printf("n.a %p\n", &n.a) // n.a 0xc00012a058
fmt.Printf("n.b %p\n", &n.b) // n.b 0xc00012a059
fmt.Printf("n.c %p\n", &n.c) // n.c 0xc00012a05a
fmt.Printf("n.d %p\n", &n.d) // n.d 0xc00012a05b
}
空结构体
空结构体是不占用空间的
func main() {
var v struct{}
fmt.Println(unsafe.Sizeof(v)) // 0
}
面试题
type student struct {
name string
age int
}
func main() {
m := make(map[string]*student)
stus := []student{
{name: "张三", age: 18},
{name: "李四", age: 23},
{name: "王五", age: 9000},
}
for _, stu := range stus {
m[stu.name] = &stu
}
for k, v := range m {
fmt.Println(k, "=>", v.name)
}
}
构造函数
Go语言的结构体没有构造函数,可以自己实现。 例如,下方的代码就实现了一个 person
的构造函数。 因为 struct
是值类型,如果结构体比较复杂的话,值拷贝性能开销会比较大,所以该构造函数返回的是结构体指针类型。
type person struct {
name string
city string
age int8
}
func newPerson(name, city string, age int8) *person {
return &person{
name: name,
city: city,
age: age,
}
}
func main() {
p := newPerson("张三", "深圳", 18)
fmt.Printf("%#v\n", p) // &main.person{name:"张三", city:"深圳", age:18}
}
方法和接收者
Go语言中的 方法(Method)
是一种作用于特定类型变量的函数。这种特定类型变量叫做 接收者(Receiver)
。接收者的概念就类似于其他语言中的 this
或者 self
方法的定义格式如下:
func (接收者变量 接收者类型) 方法名(参数列表) (返回参数) {
函数体
}
其中
- 接收者变量:接收者中的参数变量名在命名时,官方建议使用接收者类型名称的首字母小写,而不是
self
、this
之类的命名。例如,Person
类型的接收者变量应该命名为p
,Connector
类型的接收者变量应该命名为c
等 - 接收者类型:接收者类型和参数类似,可以是指针类型和非指针类型
- 方法名、参数列表、返回参数:具体格式与函数定义相同
// Person 结构体
type Person struct {
name string
age int8
}
// NewPerson 构造函数
func NewPerson(name string, age int8) *Person {
return &Person{
name: name,
age: age,
}
}
// Dream Person 做梦的方法
func (p Person) Dream() {
fmt.Printf("%s的梦想是学好Go语言!\n", p.name)
}
func main() {
p := NewPerson("张三", 18)
p.Dream()
}
方法与函数的区别是,函数不属于任何类型,方法属于特定的类型
指针类型的接收者
指针类型的接收者由一个结构体的指针组成,由于指针的特性,调用方法时修改接收者指针的任意成员变量,在方法结束后,修改都是有效的。这种方式就十分接近于其他语言中面向对象中的this
或者self
。 例如我们为Person
添加一个SetAge
方法,来修改实例变量的年龄
// Person 结构体
type Person struct {
name string
age int8
}
// NewPerson 构造函数
func NewPerson(name string, age int8) *Person {
return &Person{
name: name,
age: age,
}
}
// SetAge 设置p的年龄
// 使用指针接收者
func (p *Person) SetAge(newAge int8) {
p.age = newAge
}
func main() {
p := NewPerson("张三", 18)
fmt.Println(p.age) // 18
p.SetAge(15)
fmt.Println(p.age) // 15
}
值类型的接收者
当方法作用于值类型接收者时,Go语言会在代码运行时将接收者的值复制一份。在值类型接收者的方法中可以获取接收者的成员值,但修改操作只是针对副本,无法修改接收者变量本身
// Person 结构体
type Person struct {
name string
age int8
}
// NewPerson 构造函数
func NewPerson(name string, age int8) *Person {
return &Person{
name: name,
age: age,
}
}
// SetAge2 设置p的年龄
// 使用值接收者
func (p Person) SetAge(newAge int8) {
p.age = newAge
}
func main() {
p := NewPerson("张三", 18)
fmt.Println(p.age) // 18
p.SetAge(15) // (*p).SetAge(15)
fmt.Println(p.age) // 18
}
什么时候应该使用指针类型接收者
- 需要修改接收者中的值
- 接收者是拷贝代价比较大的大对象
- 保证一致性,如果有某个方法使用了指针接收者,那么其他的方法也应该使用指针接收者
任意类型添加方法
在Go语言中,接收者的类型可以是任何类型,不仅仅是结构体,任何类型都可以拥有方法。 举个例子,我们基于内置的int
类型使用type关键字可以定义新的自定义类型,然后为我们的自定义类型添加方法
// MyInt将int定义为自定义MyInt类型
type MyInt int
// SayHello 为MyInt添加一个SayHello的方法
func (m MyInt) SayHello() {
fmt.Println("Hello, 我是一个int")
}
func main() {
var m MyInt
m.SayHello() // Hello, 我是一个int。
m = 100
fmt.Printf("%#v %T\n", m, m) // 100 main.MyInt
}
注意:非本地类型不能定义方法,也就是说我们不能给别的包的类型定义方法
结构体的匿名字段
结构体允许其成员字段在声明时没有字段名而只有类型,这种没有名字的字段就称为匿名字段
//Person 结构体Person类型
type Person struct {
string
int
}
func main() {
p1 := Person{
"张三",
18,
}
fmt.Printf("%#v\n", p1) // main.Person{string:"张三", int:18}
fmt.Println(p1.string, p1.int) // 张三 18
}
注意:这里匿名字段的说法并不代表没有字段名,而是默认会采用类型名作为字段名,结构体要求字段名称必须唯一,因此一个结构体中同种类型的匿名字段只能有一个
嵌套结构体
一个结构体中可以嵌套包含另一个结构体或结构体指针
// Address 地址结构体
type Address struct {
Province string
City string
}
// User 用户结构体
type User struct {
Name string
Gender string
Address Address
}
func main() {
user := User{
Name: "张三",
Gender: "男",
Address: Address{
Province: "广东",
City: "深圳",
},
}
fmt.Printf("user=%#v\n", user) // user=main.User{Name:"张三", Gender:"男", Address:main.Address{Province:"广东", City:"深圳"}}
}
嵌套匿名字段
上面user结构体中嵌套的 Address
结构体也可以采用匿名字段的方式
// Address 地址结构体
type Address struct {
Province string
City string
}
// User 用户结构体
type User struct {
Name string
Gender string
Address // 匿名字段
}
func main() {
var user User
user.Name = "张三"
user.Gender = "男"
user.Address.Province = "广东" // 匿名字段默认使用类型名作为字段名
user.City = "深圳" // 匿名字段可以省略
fmt.Printf("user=%#v\n", user) // user=main.User{Name:"张三", Gender:"男", Address:main.Address{Province:"广东", City:"深圳"}}
}
当访问结构体成员时会先在结构体中查找该字段,找不到再去嵌套的匿名字段中查找
嵌套结构体的字段名冲突
嵌套结构体内部可能存在相同的字段名,在这种情况下为了避免歧义需要通过指定具体的内嵌结构体字段名
// Address 地址结构体
type Address struct {
Province string
City string
CreateTime string
}
// Email 邮箱结构体
type Email struct {
Account string
CreateTime string
}
// User 用户结构体
type User struct {
Name string
Gender string
Address
Email
}
func main() {
var user User
user.Name = "张三"
user.Gender = "男"
user.Address.CreateTime = "2022" // 指定Address结构体中的CreateTime
user.Email.CreateTime = "2022" // 指定Email结构体中的CreateTime
fmt.Printf("user=%#v\n", user) // user=main.User{Name:"张三", Gender:"男", Address:main.Address{Province:"", City:"", CreateTime:"2022"}, Email:main.Email{Account:"", CreateTime:"2022"}}
}
结构体的“继承”
Go语言中使用结构体也可以实现其他编程语言中面向对象的继承
// Animal 动物
type Animal struct {
name string
}
func (a *Animal) move() {
fmt.Printf("%s会动!\n", a.name)
}
// Dog 狗
type Dog struct {
Feet int8
*Animal // 通过嵌套匿名结构体实现继承
}
func (d *Dog) wang() {
fmt.Printf("%s会汪汪汪~\n", d.name)
}
func main() {
d := &Dog{
Feet: 4,
Animal: &Animal{ // 注意嵌套的是结构体指针
name: "乐乐",
},
}
d.wang() // 乐乐会汪汪汪~
d.move() // 乐乐会动!
}
结构体中字段大写开头表示可公开访问,小写表示私有(仅在定义当前结构体的包中可访问)
结构体与JSON序列化
// Student 学生
type Student struct {
ID int
Gender string
Name string
}
// Class 班级
type Class struct {
Title string
Students []*Student
}
func main() {
c := &Class{
Title: "101",
Students: make([]*Student, 0, 200),
}
for i := 0; i < 10; i++ {
stu := &Student{
Name: fmt.Sprintf("stu%02d", i),
Gender: "男",
ID: i,
}
c.Students = append(c.Students, stu)
}
// JSON序列化:结构体=>JSON格式的字符串
data, err := json.Marshal(c)
if err != nil {
fmt.Println("json marshal failed")
return
}
fmt.Printf("json:%s\n", data)
// JSON反序列化:JSON格式的字符串=>结构体
str := `{"Title":"101","Students":[{"ID":0,"Gender":"男","Name":"stu00"},{"ID":1,"Gender":"男","Name":"stu01"},{"ID":2,"Gender":"男","Name":"stu02"},{"ID":3,"Gender":"男","Name":"stu03"},{"ID":4,"Gender":"男","Name":"stu04"},{"ID":5,"Gender":"男","Name":"stu05"},{"ID":6,"Gender":"男","Name":"stu06"},{"ID":7,"Gender":"男","Name":"stu07"},{"ID":8,"Gender":"男","Name":"stu08"},{"ID":9,"Gender":"男","Name":"stu09"}]}`
c1 := &Class{}
err = json.Unmarshal([]byte(str), c1)
if err != nil {
fmt.Println("json unmarshal failed!")
return
}
fmt.Printf("%#v\n", c1)
}
结构体标签(Tag)
Tag
是结构体的元信息,可以在运行的时候通过反射的机制读取出来。Tag
在结构体字段的后方定义,由一对反引号包裹起来
`key1:"value1" key2:"value2"`
结构体tag由一个或多个键值对组成。键与值使用冒号分隔,值用双引号括起来。同一个结构体字段可以设置多个键值对tag,不同的键值对之间使用空格分隔。
注意:为结构体编写 Tag
时,必须严格遵守键值对的规则。结构体标签的解析代码的容错能力很差,一旦格式写错,编译和运行时都不会提示任何错误,通过反射也无法正确取值。例如不要在key和value之间添加空格。
//Student 学生
type Student struct {
ID int `json:"id"` // 通过指定tag实现json序列化该字段时的key
Gender string // json序列化是默认使用字段名Gender作为key
name string // 私有不能被json包访问
}
func main() {
s1 := Student{
ID: 1,
Gender: "男",
name: "张三",
}
data, err := json.Marshal(s1)
if err != nil {
fmt.Println("json marshal failed!")
return
}
fmt.Printf("json str:%s\n", data) // json str:{"id":1,"Gender":"男"}
}
结构体和方法
因为slice和map这两种数据类型都包含了指向底层数据的指针,因此我们在需要复制它们时要特别注意
type Person struct {
name string
age int8
dreams []string
}
func (p *Person) SetDreams(dreams []string) {
p.dreams = dreams
}
func main() {
p := Person{name: "张三", age: 18}
data := []string{"吃饭", "睡觉", "打豆豆"}
p.SetDreams(data)
data[1] = "不睡觉"
fmt.Println(p.dreams) // [吃饭 不睡觉 打豆豆]
}
正确的做法是在方法中使用传入的slice的拷贝进行结构体赋值
type Person struct {
name string
age int8
dreams []string
}
func (p *Person) SetDreams(dreams []string) {
p.dreams = make([]string, len(dreams))
copy(p.dreams, dreams)
}
func main() {
p := Person{name: "张三", age: 18}
data := []string{"吃饭", "睡觉", "打豆豆"}
p.SetDreams(data)
data[1] = "不睡觉"
fmt.Println(p.dreams) // [吃饭 睡觉 打豆豆]
}
同样的问题也存在于返回值slice和map的情况,在实际编码过程中一定要注意这个问题
包
创建包
在Go语言中通过标识符的首字母 大/小写
来控制标识符的对外 可见(public)/不可见(private)
// 创建第一个包,路径为 /demo/mypackage.go
package demo
import "fmt"
// 首字母小写,对外不可见(只能在当前包内使用)
var num = 100
// 首字母大写,对外可见(可在其它包中使用)
const Mode = 1
// 首字母小写,对外不可见(只能在当前包内使用)
type person struct {
name string // 仅限包内访问的字段
Age int // 可在包外访问的字段
}
// 首字母大写,对外可见(可在其它包中使用)
type Student struct {
Name string // 可在包外访问的字段
class string // 仅限包内访问的字段
}
// 首字母小写,对外不可见(只能在当前包内使用)
func sayHi() {
var myName = "张三"
fmt.Println(myName)
}
// 首字母大写,对外可见(可在其它包中使用)
func Add(x, y int) int {
return x + y
}
// 创建第二个包,路径为 /demo/init.go
package demo
import "fmt"
func init() {
fmt.Println("init")
}
引入包
如果引入一个包的时候为其设置了一个特殊 _
作为包名,那么这个包的引入方式就称为匿名引入。一个包被匿名引入的目的主要是为了加载这个包,从而使得这个包中的资源得以初始化。被匿名引入的包中的 init
函数将被执行并且仅执行一遍。匿名引入的包与其他方式导入的包一样都会被编译到可执行文件中。
package main
import (
f "fmt" // 包取别名
_ "github.com/toki-plus/study/03-package/demo" // 匿名引入
mypackage "github.com/toki-plus/study/03-package/demo" // 指定包引入
)
func main() {
sum := mypackage.Add(1, 2)
f.Println(sum)
}
init()函数
init()函数不接收任何参数也没有任何返回值,我们也不能在代码中主动调用它。当程序启动的时候,init函数会按照它们声明的顺序自动执行。一个包的初始化过程是按照代码中引入的顺序来进行的,所有在该包中声明的 init
函数都将被串行调用并且仅调用执行一次。每一个包初始化的时候都是先执行依赖的包中声明的 init
函数再执行当前包中声明的 init
函数。确保在程序的 main
函数开始执行时所有的依赖包都已初始化完成。
每一个包的初始化是先从初始化包级别变量开始的。例如从下面的示例中我们就可以看出包级别变量的初始化会先于 init
初始化函数。
package main
import "fmt"
var x int8 = 10
const pi = 3.14
func init() {
fmt.Println("x:", x)
fmt.Println("pi:", pi)
sayHi()
}
func sayHi() {
fmt.Println("Hello World!")
}
func main() {
fmt.Println("你好,世界!")
}
x: 10
pi: 3.14
Hello World!
你好,世界!
go module
相关命令
命令 | 介绍 |
---|---|
go mod init | 初始化项目依赖,生成go.mod文件 |
go mod download | 根据go.mod文件下载依赖 |
go mod tidy | 比对项目文件中引入的依赖与go.mod进行比对 |
go mod graph | 输出依赖关系图 |
go mod edit | 编辑go.mod文件 |
go mod vendor | 将项目的所有依赖导出至vendor目录 |
go mod verify | 检验一个依赖包是否被篡改过 |
go mod why | 解释为什么需要某个依赖 |
GOPRIVATE
设置了GOPROXY 之后,go 命令就会从配置的代理地址拉取和校验依赖包。当我们在项目中引入了非公开的包(公司内部git仓库或 github 私有仓库等),此时便无法正常从代理拉取到这些非公开的依赖包,这个时候就需要配置 GOPRIVATE 环境变量。GOPRIVATE用来告诉 go 命令哪些仓库属于私有仓库,不必通过代理服务器拉取和校验。GOPRIVATE 的值也可以设置多个,多个地址之间使用英文逗号 “,” 分隔。我们通常会把自己公司内部的代码仓库设置到 GOPRIVATE 中,例如:
go env -w GOPRIVATE="git.mycompany.com"
这样在拉取以 git.mycompany.com
为路径前缀的依赖包时就能正常拉取了。此外,如果公司内部自建了 GOPROXY 服务,那么我们可以通过设置 GONOPROXY=none
,允许通内部代理拉取私有仓库的包。
go.mod文件
go.mod
文件中记录了当前项目中所有依赖包的相关信息,声明依赖的格式如下:
require module/path v1.2.3
其中:
- require:声明依赖的关键字
- module/path:依赖包的引入路径
- v1.2.3:依赖包的版本号。支持以下几种格式:
- latest:最新版本
- v1.0.0:详细版本号
- commit hash:指定某次commit hash
引入某些没有发布过 tag
版本标识的依赖包时,go.mod
中记录的依赖版本信息就会出现类似 v0.0.0-20210218074646-139b0bcd549d
的格式,由版本号、commit时间和commit的hash值组成。
go.sum文件
使用go module下载了依赖后,项目目录下还会生成一个go.sum
文件,这个文件中详细记录了当前项目中引入的依赖包的信息及其hash 值。go.sum
文件内容通常是以类似下面的格式出现。
<module> <version>/go.mod <hash>
或者
<module> <version> <hash>
<module> <version>/go.mod <hash>
不同于其他语言提供的基于中心的包管理机制,例如 npm 和 pypi 等,Go并没有提供一个中央仓库来管理所有依赖包,而是采用分布式的方式来管理包。为了防止依赖包被非法篡改,Go module 引入了 go.sum
机制来对依赖包进行校验。
依赖保存位置
Go module 会把下载到本地的依赖包以类似下面的形式保存在 $GOPATH/pkg/mod
目录下,每个依赖包都会带有版本号进行区分,这样就允许在本地存在同一个包的多个不同版本。
mod
├── cache
├── cloud.google.com
├── github.com
└──q1mi
├── hello@v0.0.0-20210218074646-139b0bcd549d
├── hello@v0.1.1
└── hello@v0.1.0
...
如果想清除所有本地已缓存的依赖包数据,可以执行 go clean -modcache
命令。
发布包
在github创建一个名为hello的项目
$ git clone git@github.com:toki-plus/hello.git
$ cd hello
$ go mod init github.com/toki-plus/hello
创建一个 hello.go
文件
package hello
import "fmt"
func SayHi() {
fmt.Println("Hi, I'm Toki!")
}
$ git add .
$ git commit -m "first commit"
$ git push
打好 tag 推送到远程仓库
$ git tag -a v1.0.0 -m "release version v0.1.0"
$ git push origin v1.0.0
经过上面的操作我们就发布了一个版本号为 v0.1.0
的版本。
Go modules中建议使用语义化版本控制,其建议的版本号格式如下:
其中:
- 主版本号:发布了不兼容的版本迭代时递增(breaking changes)。
- 次版本号:发布了功能性更新时递增。
- 修订号:发布了bug修复类更新时递增。
发布新的主版本
现在我们的 hello
项目要进行与之前版本存在不兼容的更新,我们计划让 SayHi
函数支持向指定人发出问候。更新后的 SayHi
函数内容如下:
package hello
import "fmt"
// SayHi 向指定人打招呼的函数
func SayHi(name string) {
fmt.Printf("Hello %s, I'm Toki, nice to meet you!\n", name)
}
由于改动巨大(修改了函数之前的调用规则),对之前使用该包作为依赖的用户影响巨大,需要发布一个主版本号递增的 v2
版本
// 在 hello/go.mod 文件中添加 v2 版本
module github.com/toki-plus/hello/v2
go 1.18
把修改后的代码提交
$ git add .
$ git commit -m "second commit"
$ git push
打好 tag 推送到远程仓库
$ git tag -a v2.0.0 -m "release version v2.0.0"
$ git push origin v2.0.0
这样在不影响使用旧版本的用户的前提下,我们新的版本也发布出去了。想要使用 v2
版本的代码包的用户只需按修改后的引入路径下载即可
go get github.com/toki-plus/hello/v2@v2.0.0
在代码中使用的过程与之前类似,只是需要注意引入路径要添加 v2 版本后缀。
package main
import (
"fmt"
"github.com/toki-plus/hello/v2" // 引入v2版本
)
func main() {
hello.SayHi("张三") // v2版本的SayHi函数需要传入字符串参数
}
废弃已发布版本
如果某个发布的版本存在致命缺陷不再想让用户使用时,我们可以使用 retract
声明废弃的版本。例如我们在 hello/go.mod
文件中按如下方式声明即可对外废弃 v0.1.2
版本
module github.com/toki-plus/hello
go 1.18
retract v0.1.2
用户使用go get下载 v0.1.2
版本时就会收到提示,催促其升级到其他版本
Go进阶篇
接口
接口类型
接口是一种由程序员来定义的类型
,一个接口类型就是一组方法的集合,它规定了需要实现的所有方法。
相较于使用结构体类型,当我们使用接口类型说明相比于它是什么
更关心它能做什么
。
接口的定义
每个接口类型由任意个方法签名组成,接口的定义格式如下:
type 接口类型名 interface{
方法名1( 参数列表1 ) 返回值列表1
方法名2( 参数列表2 ) 返回值列表2
…
}
其中:
接口类型名
:Go语言的接口在命名时,一般会在单词后面添加er
,如有写操作的接口叫Writer
,有关闭操作的接口叫closer
等。接口名最好要能突出该接口的类型含义。方法名
:当方法名
首字母是大写且这个接口类型名
首字母也是大写时,这个方法可以被接口所在的包(package)之外的代码访问。参数列表
、返回值列表
:参数列表和返回值列表中的参数变量名
可以省略。
举个例子,定义一个包含Write
方法的Writer
接口。
type Writer interface{
Write([]byte) error
}
当你看到一个Writer
接口类型的值时,你不知道它是什么,唯一知道的就是可以通过调用它的Write
方法来做一些事情。
实现接口的条件
接口就是规定了一个需要实现的方法列表
,在 Go 语言中一个类型只要实现了接口中规定的所有方法,那么我们就称它实现了这个接口。
我们定义的Singer
接口类型,它包含一个Sing
方法
// Singer 接口
type Singer interface {
Sing()
}
我们有一个Bird
结构体类型如下:
type Bird struct {}
因为Singer
接口只包含一个Sing
方法,所以只需要给Bird
结构体添加一个Sing
方法就可以满足Singer
接口的要求
// Sing Bird类型的Sing方法
func (b Bird) Sing() {
fmt.Println("汪汪汪")
}
这样就称为Bird
实现了Singer
接口
为什么要使用接口?
现在假设我们的代码世界里有很多小动物,下面的代码片段定义了猫和狗,它们饿了都会叫。
package main
import "fmt"
type Cat struct{}
func (c Cat) Say() {
fmt.Println("喵喵喵~")
}
type Dog struct{}
func (d Dog) Say() {
fmt.Println("汪汪汪~")
}
func main() {
c := Cat{}
c.Say()
d := Dog{}
d.Say()
}
这个时候又跑来了一只羊,羊饿了也会发出叫声。
type Sheep struct{}
func (s Sheep) Say() {
fmt.Println("咩咩咩~")
}
我们接下来定义一个饿肚子的场景。
// MakeCatHungry 猫饿了会喵喵喵~
func MakeCatHungry(c Cat) {
c.Say()
}
// MakeSheepHungry 羊饿了会咩咩咩~
func MakeSheepHungry(s Sheep) {
s.Say()
}
接下来会有越来越多的小动物跑过来,我们的代码世界该怎么拓展呢?
在饿肚子这个场景下,我们可不可以把所有动物都当成一个“会叫的类型”来处理呢?当然可以!使用接口类型就可以实现这个目标。 我们的代码其实并不关心究竟是什么动物在叫,我们只是在代码中调用它的Say()
方法,这就足够了。
我们可以约定一个Sayer
类型,它必须实现一个Say()
方法,只要饿肚子了,我们就调用Say()
方法。
type Sayer interface {
Say()
}
然后我们定义一个通用的MakeHungry
函数,接收Sayer
类型的参数。
// MakeHungry 饿肚子了...
func MakeHungry(s Sayer) {
s.Say()
}
我们通过使用接口类型,把所有会叫的动物当成Sayer
类型来处理,只要实现了Say()
方法都能当成Sayer
类型的变量来处理。
var c cat
MakeHungry(c)
var d dog
MakeHungry(d)
在电商系统中我们允许用户使用多种支付方式(支付宝支付、微信支付、银联支付等),我们的交易流程中可能不太在乎用户究竟使用什么支付方式,只要它能提供一个实现支付功能的Pay
方法让调用方调用就可以了。
再比如我们需要在某个程序中添加一个将某些指标数据向外输出的功能,根据不同的需求可能要将数据输出到终端、写入到文件或者通过网络连接发送出去。在这个场景下我们可以不关注最终输出的目的地是什么,只需要它能提供一个Write
方法让我们把内容写入就可以了。
Go语言中为了解决类似上面的问题引入了接口的概念,接口类型区别于我们之前章节中介绍的那些具体类型,让我们专注于该类型提供的方法,而不是类型本身。使用接口类型通常能够让我们写出更加通用和灵活的代码。
面向接口编程
PHP、Java等语言中也有接口的概念,不过在PHP和Java语言中需要显式声明一个类实现了哪些接口,在Go语言中使用隐式声明的方式实现接口。只要一个类型实现了接口中规定的所有方法,那么它就实现了这个接口。
Go语言中的这种设计符合程序开发中抽象的一般规律,例如在下面的代码示例中,我们的电商系统最开始只设计了支付宝一种支付方式:
type ZhiFuBao struct {
// 支付宝
}
// Pay 支付宝的支付方法
func (z *ZhiFuBao) Pay(amount int64) {
fmt.Printf("使用支付宝付款:%.2f元。\n", float64(amount/100))
}
// Checkout 结账
func Checkout(obj *ZhiFuBao) {
// 支付100元
obj.Pay(100)
}
func main() {
Checkout(&ZhiFuBao{})
}
随着业务的发展,根据用户需求添加支持微信支付。
type WeChat struct {
// 微信
}
// Pay 微信的支付方法
func (w *WeChat) Pay(amount int64) {
fmt.Printf("使用微信付款:%.2f元。\n", float64(amount/100))
}
在实际的交易流程中,我们可以根据用户选择的支付方式来决定最终调用支付宝的Pay方法还是微信支付的Pay方法。
// Checkout 支付宝结账
func CheckoutWithZFB(obj *ZhiFuBao) {
// 支付100元
obj.Pay(100)
}
// Checkout 微信支付结账
func CheckoutWithWX(obj *WeChat) {
// 支付100元
obj.Pay(100)
}
实际上,从上面的代码示例中我们可以看出,我们其实并不怎么关心用户选择的是什么支付方式,我们只关心调用Pay方法时能否正常运行。这就是典型的“不关心它是什么,只关心它能做什么”的场景。
在这种场景下我们可以将具体的支付方式抽象为一个名为Payer
的接口类型,即任何实现了Pay
方法的结构体类型都可以称为Payer
类型。
// Payer 包含支付方法的接口类型
type Payer interface {
Pay(int64)
}
此时只需要修改下原始的Checkout
函数,它接收一个Payer
类型的参数。这样就能够在不修改既有函数调用的基础上,支持新的支付方式。
// Checkout 结账
func Checkout(obj Payer) {
// 支付100元
obj.Pay(100)
}
func main() {
Checkout(&ZhiFuBao{}) // 之前调用支付宝支付
Checkout(&WeChat{}) // 现在支持使用微信支付
}
像类似的例子在我们编程过程中会经常遇到:
- 比如一个网上商城可能使用支付宝、微信、银联等方式去在线支付,我们能不能把它们当成“支付方式”来处理呢?
- 比如三角形,四边形,圆形都能计算周长和面积,我们能不能把它们当成“图形”来处理呢?
- 比如满减券、立减券、打折券都属于电商场景下常见的优惠方式,我们能不能把它们当成“优惠券”来处理呢?
接口类型是Go语言提供的一种工具,在实际的编码过程中是否使用它由你自己决定,但是通常使用接口类型可以使代码更清晰易读。
接口类型变量
那实现了接口又有什么用呢?一个接口类型的变量能够存储所有实现了该接口的类型变量。
例如在上面的示例中,Dog
和Cat
类型均实现了Sayer
接口,此时一个Sayer
类型的变量就能够接收Cat
和Dog
类型的变量。
var x Sayer // 声明一个Sayer类型的变量x
a := Cat{} // 声明一个Cat类型变量a
b := Dog{} // 声明一个Dog类型变量b
x = a // 可以把Cat类型变量直接赋值给x
x.Say() // 喵喵喵
x = b // 可以把Dog类型变量直接赋值给x
x.Say() // 汪汪汪
值接收者和指针接收者
我们定义一个Mover
接口,它包含一个Move
方法。
// Mover 定义一个接口类型
type Mover interface {
Move()
}
值接收者实现接口
我们定义一个Dog
结构体类型,并使用值接收者为其定义一个Move
方法。
// Dog 狗结构体类型
type Dog struct{}
// Move 使用值接收者定义Move方法实现Mover接口
func (d Dog) Move() {
fmt.Println("狗会动")
}
此时实现Mover
接口的是Dog
类型。
var x Mover // 声明一个Mover类型的变量x
var d1 = Dog{} // d1是Dog类型
x = d1 // 可以将d1赋值给变量x
x.Move()
var d2 = &Dog{} // d2是Dog指针类型
x = d2 // 也可以将d2赋值给变量x
x.Move()
从上面的代码中我们可以发现,使用值接收者实现接口之后,不管是结构体类型还是对应的指针类型的变量都可以赋值给该接口变量。
指针接收者实现接口
我们再来测试一下使用指针接收者实现接口有什么区别。
// Cat 猫结构体类型
type Cat struct{}
// Move 使用指针接收者定义Move方法实现Mover接口
func (c *Cat) Move() {
fmt.Println("猫会动")
}
此时实现Mover
接口的是*Cat
类型,我们可以将*Cat
类型的变量直接赋值给Mover
接口类型的变量x
。
var x Mover // 声明一个Mover类型的变量x
var c1 = &Cat{} // c1是*Cat类型
x = c1 // 可以将c1当成Mover类型
x.Move()
但是不能给将Cat
类型的变量赋值给Mover
接口类型的变量x
。
// 下面的代码无法通过编译
var c2 = Cat{} // c2是Cat类型
x = c2 // 不能将c2当成Mover类型
由于Go语言中有对指针求值的语法糖,对于值接收者实现的接口,无论使用值类型还是指针类型都没有问题。但是我们并不总是能对一个值求址,所以对于指针接收者实现的接口要额外注意。
类型与接口的关系
一个类型实现多个接口
一个类型可以同时实现多个接口,而接口间彼此独立,不知道对方的实现。例如狗不仅可以叫,还可以动。我们完全可以分别定义Sayer
接口和Mover
接口,具体代码示例如下。
// Sayer 接口
type Sayer interface {
Say()
}
// Mover 接口
type Mover interface {
Move()
}
Dog
既可以实现Sayer
接口,也可以实现Mover
接口。
type Dog struct {
Name string
}
// 实现Sayer接口
func (d Dog) Say() {
fmt.Printf("%s会叫汪汪汪\n", d.Name)
}
// 实现Mover接口
func (d Dog) Move() {
fmt.Printf("%s会动\n", d.Name)
}
同一个类型实现不同的接口互相不影响使用。
var d = Dog{Name: "旺财"}
var s Sayer = d
var m Mover = d
s.Say() // 对Sayer类型调用Say方法
m.Move() // 对Mover类型调用Move方法
多种类型实现同一接口
Go语言中不同的类型还可以实现同一接口。例如在我们的代码世界中不仅狗可以动,汽车也可以动。我们可以使用如下代码体现这个关系。
// 实现Mover接口
func (d Dog) Move() {
fmt.Printf("%s会动\n", d.Name)
}
// Car 汽车结构体类型
type Car struct {
Brand string
}
// Move Car类型实现Mover接口
func (c Car) Move() {
fmt.Printf("%s速度70迈\n", c.Brand)
}
这样我们在代码中就可以把狗和汽车当成一个会动的类型来处理,不必关注它们具体是什么,只需要调用它们的Move
方法就可以了。
var obj Mover
obj = Dog{Name: "旺财"}
obj.Move()
obj = Car{Brand: "宝马"}
obj.Move()
上面的代码执行结果如下:
旺财会跑
宝马速度70迈
一个接口的所有方法,不一定需要由一个类型完全实现,接口的方法可以通过在类型中嵌入其他类型或者结构体来实现。
// WashingMachine 洗衣机
type WashingMachine interface {
wash()
dry()
}
// 甩干器
type dryer struct{}
// 实现WashingMachine接口的dry()方法
func (d dryer) dry() {
fmt.Println("甩一甩")
}
// 海尔洗衣机
type haier struct {
dryer //嵌入甩干器
}
// 实现WashingMachine接口的wash()方法
func (h haier) wash() {
fmt.Println("洗刷刷")
}
接口组合
接口与接口之间可以通过互相嵌套形成新的接口类型,例如Go标准库io
源码中就有很多接口之间互相组合的示例。
// src/io/io.go
type Reader interface {
Read(p []byte) (n int, err error)
}
type Writer interface {
Write(p []byte) (n int, err error)
}
type Closer interface {
Close() error
}
// ReadWriter 是组合Reader接口和Writer接口形成的新接口类型
type ReadWriter interface {
Reader
Writer
}
// ReadCloser 是组合Reader接口和Closer接口形成的新接口类型
type ReadCloser interface {
Reader
Closer
}
// WriteCloser 是组合Writer接口和Closer接口形成的新接口类型
type WriteCloser interface {
Writer
Closer
}
对于这种由多个接口类型组合形成的新接口类型,同样只需要实现新接口类型中规定的所有方法就算实现了该接口类型。
接口也可以作为结构体的一个字段,我们来看一段Go标准库sort
源码中的示例。
// src/sort/sort.go
// Interface 定义通过索引对元素排序的接口类型
type Interface interface {
Len() int
Less(i, j int) bool
Swap(i, j int)
}
// reverse 结构体中嵌入了Interface接口
type reverse struct {
Interface
}
通过在结构体中嵌入一个接口类型,从而让该结构体类型实现了该接口类型,并且还可以改写该接口的方法。
// Less 为 reverse 类型添加 Less 方法,重写原 Interface 接口类型的 Less 方法
func (r reverse) Less(i, j int) bool {
return r.Interface.Less(j, i)
}
Interface
类型原本的Less
方法签名为Less(i, j int) bool
,此处重写为r.Interface.Less(j, i)
,即通过将索引参数交换位置实现反转。
在这个示例中还有一个需要注意的地方是reverse
结构体本身是不可导出的(结构体类型名称首字母小写),sort.go
中通过定义一个可导出的Reverse
函数来让使用者创建reverse
结构体实例。
func Reverse(data Interface) Interface {
return &reverse{data}
}
这样做的目的是保证得到的reverse
结构体中的Interface
属性一定不为nil
,否者r.Interface.Less(j, i)
就会出现空指针panic。
此外在Go内置标准库database/sql
中也有很多类似的结构体内嵌接口类型的使用示例,各位读者可自行查阅。
空接口
空接口的定义
空接口是指没有定义任何方法的接口类型。因此任何类型都可以视为实现了空接口。也正是因为空接口类型的这个特性,空接口类型的变量可以存储任意类型的值。
package main
import "fmt"
// 空接口
// Any 不包含任何方法的空接口类型
type Any interface{}
// Dog 狗结构体
type Dog struct{}
func main() {
var x Any
x = "你好" // 字符串型
fmt.Printf("type:%T value:%v\n", x, x)
x = 100 // int型
fmt.Printf("type:%T value:%v\n", x, x)
x = true // 布尔型
fmt.Printf("type:%T value:%v\n", x, x)
x = Dog{} // 结构体类型
fmt.Printf("type:%T value:%v\n", x, x)
}
通常我们在使用空接口类型时不必使用type
关键字声明,可以像下面的代码一样直接使用interface{}
。
var x interface{} // 声明一个空接口类型变量x
空接口的应用
空接口作为函数的参数
使用空接口实现可以接收任意类型的函数参数。
// 空接口作为函数参数
func show(a interface{}) {
fmt.Printf("type:%T value:%v\n", a, a)
}
空接口作为map的值
使用空接口实现可以保存任意值的字典。
// 空接口作为map值
var studentInfo = make(map[string]interface{})
studentInfo["name"] = "张三"
studentInfo["age"] = 18
studentInfo["married"] = false
fmt.Println(studentInfo)
接口值
由于接口类型的值可以是任意一个实现了该接口的类型值,所以接口值除了需要记录具体值
之外,还需要记录这个值属于的类型
。也就是说接口值由“类型”和“值”组成,鉴于这两部分会根据存入值的不同而发生变化,我们称之为接口的动态类型
和动态值
。
我们接下来通过一个示例来加深对接口值的理解。
下面的示例代码中,定义了一个Mover
接口类型和两个实现了该接口的Dog
和Car
结构体类型。
type Mover interface {
Move()
}
type Dog struct {
Name string
}
func (d *Dog) Move() {
fmt.Println("狗在跑~")
}
type Car struct {
Brand string
}
func (c *Car) Move() {
fmt.Println("汽车在跑~")
}
首先,我们创建一个Mover
接口类型的变量m
。
var m Mover
此时,接口变量m
是接口类型的零值,也就是它的类型和值部分都是nil
,就如下图所示。
我们可以使用m == nil
来判断此时的接口值是否为空。
fmt.Println(m == nil) // true
注意:我们不能对一个空接口值调用任何方法,否则会产生panic。
m.Move() // panic: runtime error: invalid memory address or nil pointer dereference
接下来,我们将一个*Dog
结构体指针赋值给变量m
。
m = &Dog{Name: "旺财"}
此时,接口值m
的动态类型会被设置为*Dog
,动态值为结构体变量的拷贝。
然后,我们给接口变量m
赋值为一个*Car
类型的值。
m = new(Car)
这一次,接口值的动态类型为*Car
,动态值为nil
。
注意:此时接口变量m
与nil
并不相等,因为它只是动态值的部分为nil
,而动态类型部分保存着对应值的类型。
fmt.Println(m == nil) // false
接口值是支持相互比较的,当且仅当接口值的动态类型
和动态值
都相等时才相等。
var (
x Mover = new(Dog)
y Mover = new(Car)
)
fmt.Println(x == y) // false
但是有一种特殊情况需要特别注意,如果接口值的保存的动态类型相同,但是这个动态类型不支持互相比较(比如切片),那么对它们相互比较时就会引发panic。
var z interface{} = []int{1, 2, 3}
fmt.Println(z == z) // panic: runtime error: comparing uncomparable type []int
类型断言
接口值可能赋值为任意类型的值,那我们如何从接口值获取其存储的具体数据呢?
我们可以借助标准库fmt
包的格式化打印获取到接口值的动态类型。
var m Mover
m = &Dog{Name: "旺财"}
fmt.Printf("%T\n", m) // *main.Dog
m = new(Car)
fmt.Printf("%T\n", m) // *main.Car
而fmt
包内部其实是使用反射的机制在程序运行时获取到动态类型的名称。关于反射的内容我们会在后续章节详细介绍。
而想要从接口值中获取到对应的实际值需要使用类型断言,其语法格式如下。
x.(T)
其中:
- x:表示接口类型的变量
- T:表示断言
x
可能是的类型。
该语法返回两个参数,第一个参数是x
转化为T
类型后的变量,第二个值是一个布尔值,若为true
则表示断言成功,为false
则表示断言失败。
举个例子:
var n Mover = &Dog{Name: "旺财"}
v, ok := n.(*Dog)
if ok {
fmt.Println("类型断言成功")
v.Name = "富贵" // 变量v是*Dog类型
} else {
fmt.Println("类型断言失败")
}
如果对一个接口值有多个实际类型需要判断,推荐使用switch
语句来实现。
// justifyType 对传入的空接口类型变量x进行类型断言
func justifyType(x interface{}) {
switch v := x.(type) {
case string:
fmt.Printf("x is a string,value is %v\n", v)
case int:
fmt.Printf("x is a int is %v\n", v)
case bool:
fmt.Printf("x is a bool is %v\n", v)
default:
fmt.Println("unsupport type!")
}
}
由于接口类型变量能够动态存储不同类型值的特点,所以很多初学者会滥用接口类型(特别是空接口)来实现编码过程中的便捷。只有当有两个或两个以上的具体类型必须以相同的方式进行处理时才需要定义接口。切记不要为了使用接口类型而增加不必要的抽象,导致不必要的运行时损耗。
在 Go 语言中接口是一个非常重要的概念和特性,使用接口类型能够实现代码的抽象和解耦,也可以隐藏某个功能的内部实现,但是缺点就是在查看源码的时候,不太方便查找到具体实现接口的类型。
相信很多读者在刚接触到接口类型时都会有很多疑惑,请牢记接口是一种类型,一种抽象的类型。区别于我们在之前章节提到的那些具体类型(整型、数组、结构体类型等),它是一个只要求实现特定方法的抽象类型。
小技巧:下面的代码可以在程序编译阶段验证某一结构体是否满足特定的接口类型。
// 摘自gin框架routergroup.go
type IRouter interface{ ... }
type RouterGroup struct { ... }
var _ IRouter = &RouterGroup{} // 确保RouterGroup实现了接口IRouter
上面的代码中也可以使用var _ IRouter = (*RouterGroup)(nil)
进行验证。
错误处理
Go 语言中的错误处理与其他语言不太一样,它把错误当成一种值来处理,更强调判断错误、处理错误,而不是一股脑的 catch 捕获异常。
error接口
Go 语言中把错误当成一种特殊的值来处理,不支持其他语言中使用try/catch
捕获异常的方式。
Go 语言中使用一个名为 error
的接口来表示错误类型。
type error interface {
Error() string
}
error
接口只包含一个方法Error()
,这个函数需要返回一个描述错误信息的字符串。
当一个函数或方法需要返回错误时,我们通常是把错误作为最后一个返回值。例如下面标准库 os 中打开文件的函数。
func Open(name string) (*File, error) {
return OpenFile(name, O_RDONLY, 0)
}
由于 error 是一个接口类型,默认零值为nil
。所以我们通常将调用函数返回的错误与nil
进行比较,以此来判断函数是否返回错误。例如你会经常看到类似下面的错误判断代码。
file, err := os.Open("./xx.go")
if err != nil {
fmt.Println("打开文件失败,err:", err)
return
}
注意:当我们使用fmt
包打印错误时会自动调用 error 类型的 Error 方法,也就是会打印出错误的描述信息。
创建error
我们可以根据需求自定义 error,最简单的方式是使用errors
包提供的New
函数创建一个错误。
errors.New
函数签名如下:
func New(text string) error
它接收一个字符串参数返回包含该字符串的错误。我们可以在函数返回时快速创建一个错误。
func queryById(id int64) (*Info, error) {
if id <= 0 {
return nil, errors.New("无效的id")
}
// ...
}
或者用来定义一个错误变量,例如标准库io.EOF
错误定义如下。
var EOF = errors.New("EOF")
fmt.Errorf
当我们需要传入格式化的错误描述信息时,使用fmt.Errorf
是个更好的选择。
fmt.Errorf("查询数据库失败,err:%v", err)
但是上面的方式会丢失原有的错误类型,只拿到错误描述的文本信息。
为了不丢失函数调用的错误链,使用fmt.Errorf
时搭配使用特殊的格式化动词%w
,可以实现基于已有的错误再包装得到一个新的错误。
fmt.Errorf("查询数据库失败,err:%w", err)
对于这种二次包装的错误,errors
包中提供了以下三个方法。
func Unwrap(err error) error // 获得err包含下一层错误
func Is(err, target error) bool // 判断err是否包含target
func As(err error, target interface{}) bool // 判断err是否为target类型
错误结构体类型
此外我们还可以自己定义结构体类型,实现error
接口。
// OpError 自定义结构体类型
type OpError struct {
Op string
}
// Error OpError 类型实现error接口
func (e *OpError) Error() string {
return fmt.Sprintf("无权执行%s操作", e.Op)
}
反射
反射是指在程序运行期间对程序本身进行访问和修改的能力。程序在编译时,变量被转换为内存地址,变量名不会被编译器写入到可执行部分。在运行程序时,程序无法获取自身的信息。
支持反射的语言可以在程序编译期将变量的反射信息,如字段名称、类型信息、结构体信息等整合到可执行文件中,并给程序提供接口访问反射信息,这样就可以在程序运行期获取类型的反射信息,并且有能力修改它们。
Go程序在运行期使用reflect包访问程序的反射信息。
在上一篇博客中我们介绍了空接口。空接口可以存储任意类型的变量,那我们如何知道这个空接口保存的数据是什么呢?反射就是在运行时动态的获取一个变量的类型信息和值信息。
在Go语言的反射机制中,任何接口值都由是一个具体类型
和具体类型的值
两部分组成的。 在Go语言中反射的相关功能由内置的reflect包提供,任意接口值在反射中都可以理解为由reflect.Type
和reflect.Value
两部分组成,并且reflect包提供了reflect.TypeOf
和reflect.ValueOf
两个函数来获取任意对象的Value和Type。
TypeOf
在Go语言中,使用reflect.TypeOf()
函数可以获得任意值的类型对象(reflect.Type),程序通过类型对象可以访问任意值的类型信息。
package main
import (
"fmt"
"reflect"
)
func reflectType(x interface{}) {
v := reflect.TypeOf(x)
fmt.Printf("type:%v\n", v)
}
func main() {
var a float32 = 3.14
reflectType(a) // type:float32
var b int64 = 100
reflectType(b) // type:int64
}
type name 和 type kind
在反射中关于类型还划分为两种:类型(Type)
和种类(Kind)
。因为在Go语言中我们可以使用type关键字构造很多自定义类型,而种类(Kind)
就是指底层的类型,但在反射中,当需要区分指针、结构体等大品种的类型时,就会用到种类(Kind)
。举个例子,我们定义了两个指针类型和两个结构体类型,通过反射查看它们的类型和种类。
package main
import (
"fmt"
"reflect"
)
type myInt int64
func reflectType(x interface{}) {
t := reflect.TypeOf(x)
fmt.Printf("type:%v kind:%v\n", t.Name(), t.Kind())
}
func main() {
var a *float32 // 指针
var b myInt // 自定义类型
var c rune // 类型别名
reflectType(a) // type: kind:ptr
reflectType(b) // type:myInt kind:int64
reflectType(c) // type:int32 kind:int32
type person struct {
name string
age int
}
type book struct{ title string }
var d = person{
name: "张三",
age: 18,
}
var e = book{title: "《Go语言》"}
reflectType(d) // type:person kind:struct
reflectType(e) // type:book kind:struct
}
Go语言的反射中像数组、切片、Map、指针等类型的变量,它们的.Name()
都是返回空
。
在reflect
包中定义的Kind类型如下:
type Kind uint
const (
Invalid Kind = iota // 非法类型
Bool // 布尔型
Int // 有符号整型
Int8 // 有符号8位整型
Int16 // 有符号16位整型
Int32 // 有符号32位整型
Int64 // 有符号64位整型
Uint // 无符号整型
Uint8 // 无符号8位整型
Uint16 // 无符号16位整型
Uint32 // 无符号32位整型
Uint64 // 无符号64位整型
Uintptr // 指针
Float32 // 单精度浮点数
Float64 // 双精度浮点数
Complex64 // 64位复数类型
Complex128 // 128位复数类型
Array // 数组
Chan // 通道
Func // 函数
Interface // 接口
Map // 映射
Ptr // 指针
Slice // 切片
String // 字符串
Struct // 结构体
UnsafePointer // 底层指针
)
ValueOf
reflect.ValueOf()
返回的是reflect.Value
类型,其中包含了原始值的值信息。reflect.Value
与原始值之间可以互相转换。
reflect.Value
类型提供的获取原始值的方法如下:
方法 | 说明 |
---|---|
Interface() interface {} | 将值以 interface{} 类型返回,可以通过类型断言转换为指定类型 |
Int() int64 | 将值以 int 类型返回,所有有符号整型均可以此方式返回 |
Uint() uint64 | 将值以 uint 类型返回,所有无符号整型均可以此方式返回 |
Float() float64 | 将值以双精度(float64)类型返回,所有浮点数(float32、float64)均可以此方式返回 |
Bool() bool | 将值以 bool 类型返回 |
Bytes() []bytes | 将值以字节数组 []bytes 类型返回 |
String() string | 将值以字符串类型返回 |
通过反射获取值
func reflectValue(x interface{}) {
v := reflect.ValueOf(x)
k := v.Kind()
switch k {
case reflect.Int64:
// v.Int()从反射中获取整型的原始值,然后通过int64()强制类型转换
fmt.Printf("type is int64, value is %d\n", int64(v.Int()))
case reflect.Float32:
// v.Float()从反射中获取浮点型的原始值,然后通过float32()强制类型转换
fmt.Printf("type is float32, value is %f\n", float32(v.Float()))
case reflect.Float64:
// v.Float()从反射中获取浮点型的原始值,然后通过float64()强制类型转换
fmt.Printf("type is float64, value is %f\n", float64(v.Float()))
}
}
func main() {
var a float32 = 3.14
var b int64 = 100
reflectValue(a) // type is float32, value is 3.140000
reflectValue(b) // type is int64, value is 100
// 将int类型的原始值转换为reflect.Value类型
c := reflect.ValueOf(10)
fmt.Printf("type c :%T\n", c) // type c :reflect.Value
}
通过反射设置变量的值
想要在函数中通过反射修改变量的值,需要注意函数参数传递的是值拷贝,必须传递变量地址才能修改变量值。而反射中使用专有的Elem()
方法来获取指针对应的值。
package main
import (
"fmt"
"reflect"
)
func reflectSetValue1(x interface{}) {
v := reflect.ValueOf(x)
if v.Kind() == reflect.Int64 {
v.SetInt(200) //修改的是副本,reflect包会引发panic
}
}
func reflectSetValue2(x interface{}) {
v := reflect.ValueOf(x)
// 反射中使用 Elem()方法获取指针对应的值
if v.Elem().Kind() == reflect.Int64 {
v.Elem().SetInt(200)
}
}
func main() {
var a int64 = 100
// reflectSetValue1(a) //panic: reflect: reflect.Value.SetInt using unaddressable value
reflectSetValue2(&a)
fmt.Println(a)
}
isNil()和isValid()
isNil()
func (v Value) IsNil() bool
IsNil()
报告v持有的值是否为nil。v持有的值的分类必须是通道、函数、接口、映射、指针、切片之一;否则IsNil函数会导致panic。
isValid()
func (v Value) IsValid() bool
IsValid()
返回v是否持有一个值。如果v是Value零值会返回假,此时v除了IsValid、String、Kind之外的方法都会导致panic。
举个例子
IsNil()
常被用于判断指针是否为空;IsValid()
常被用于判定返回值是否有效。
func main() {
// *int类型空指针
var a *int
fmt.Println("var a *int IsNil:", reflect.ValueOf(a).IsNil())
// nil值
fmt.Println("nil IsValid:", reflect.ValueOf(nil).IsValid())
// 实例化一个匿名结构体
b := struct{}{}
// 尝试从结构体中查找"abc"字段
fmt.Println("不存在的结构体成员:", reflect.ValueOf(b).FieldByName("abc").IsValid())
// 尝试从结构体中查找"abc"方法
fmt.Println("不存在的结构体方法:", reflect.ValueOf(b).MethodByName("abc").IsValid())
// map
c := map[string]int{}
// 尝试从map中查找一个不存在的键
fmt.Println("map中不存在的键:", reflect.ValueOf(c).MapIndex(reflect.ValueOf("张三")).IsValid())
}
结构体反射
与结构体相关的方法
任意值通过reflect.TypeOf()
获得反射对象信息后,如果它的类型是结构体,可以通过反射值对象(reflect.Type
)的NumField()
和Field()
方法获得结构体成员的详细信息。
reflect.Type
中与获取结构体成员相关的的方法如下表所示。
方法 | 说明 |
---|---|
Field(i int) StructField | 根据索引,返回索引对应的结构体字段的信息。 |
NumField() int | 返回结构体成员字段数量。 |
FieldByName(name string) (StructField, bool) | 根据给定字符串返回字符串对应的结构体字段的信息。 |
FieldByIndex(index []int) StructField | 多层成员访问时,根据 []int 提供的每个结构体的字段索引,返回字段的信息。 |
FieldByNameFunc(match func(string) bool) (StructField,bool) | 根据传入的匹配函数匹配需要的字段。 |
NumMethod() int | 返回该类型的方法集中方法的数目 |
Method(int) Method | 返回该类型方法集中的第i个方法 |
MethodByName(string)(Method, bool) | 根据方法名返回该类型方法集中的方法 |
StructField类型
StructField
类型用来描述结构体中的一个字段的信息。
StructField
的定义如下:
type StructField struct {
// Name是字段的名字。PkgPath是非导出字段的包路径,对导出字段该字段为""。
// 参见http://golang.org/ref/spec#Uniqueness_of_identifiers
Name string
PkgPath string
Type Type // 字段的类型
Tag StructTag // 字段的标签
Offset uintptr // 字段在结构体中的字节偏移量
Index []int // 用于Type.FieldByIndex时的索引切片
Anonymous bool // 是否匿名字段
}
结构体反射示例
当我们使用反射得到一个结构体数据之后可以通过索引依次获取其字段信息,也可以通过字段名去获取指定的字段信息。
type student struct {
Name string `json:"name"`
Score int `json:"score"`
}
func main() {
stu1 := student{
Name: "张三",
Score: 90,
}
t := reflect.TypeOf(stu1)
fmt.Println(t.Name(), t.Kind()) // student struct
// 通过for循环遍历结构体的所有字段信息
for i := 0; i < t.NumField(); i++ {
field := t.Field(i)
fmt.Printf("name:%s index:%d type:%v json tag:%v\n", field.Name, field.Index, field.Type, field.Tag.Get("json"))
}
// 通过字段名获取指定结构体字段信息
if scoreField, ok := t.FieldByName("Score"); ok {
fmt.Printf("name:%s index:%d type:%v json tag:%v\n", scoreField.Name, scoreField.Index, scoreField.Type, scoreField.Tag.Get("json"))
}
}
接下来编写一个函数printMethod(s interface{})
来遍历打印s包含的方法。
// 给student添加两个方法 Study和Sleep(注意首字母大写)
func (s student) Study() string {
msg := "好好学习,天天向上。"
fmt.Println(msg)
return msg
}
func (s student) Sleep() string {
msg := "好好睡觉,快快长大。"
fmt.Println(msg)
return msg
}
func printMethod(x interface{}) {
t := reflect.TypeOf(x)
v := reflect.ValueOf(x)
fmt.Println(t.NumMethod())
for i := 0; i < v.NumMethod(); i++ {
methodType := v.Method(i).Type()
fmt.Printf("method name:%s\n", t.Method(i).Name)
fmt.Printf("method:%s\n", methodType)
// 通过反射调用方法传递的参数必须是 []reflect.Value 类型
var args = []reflect.Value{}
v.Method(i).Call(args)
}
}
反射是把双刃剑
反射是一个强大并富有表现力的工具,能让我们写出更灵活的代码。但是反射不应该被滥用,原因有以下三个。
- 基于反射的代码是极其脆弱的,反射中的类型错误会在真正运行的时候才会引发panic,那很可能是在代码写完的很长时间之后。
- 大量使用反射的代码通常难以理解。
- 反射的性能低下,基于反射实现的代码通常比正常代码运行速度慢一到两个数量级。
Go高级篇
并发
串行、并发与并行
串行:我们都是先读小学,小学毕业后再读初中,读完初中再读高中。
并行:同一时刻执行多个任务(你和你朋友都在用微信和女朋友聊天)。
并发:同一时间段内执行多个任务(你在用微信和两个女朋友聊天)。
进程、线程和协程
进程(process):程序在操作系统中的一次执行过程,系统进行资源分配和调度的一个独立单位。
线程(thread):操作系统基于进程开启的轻量级进程,是操作系统调度执行的最小单位。
协程(coroutine):非操作系统提供而是由用户自行创建和控制的用户态‘线程’,比线程更轻量级。
goroutine
启动 goroutine 只需要在调用函数(普通函数和匿名函数)前加上一个 go
关键字
func hello() {
fmt.Println("hello")
}
func main() {
go hello()
fmt.Println("你好")
time.Sleep(time.Second) // 表示延时1秒
}
输出:
你好
hello
在 Go 程序启动时,Go 程序就会为 main 函数创建一个默认的 goroutine。在上面的代码中我们在 main 函数中使用 go 关键字创建了另外一个 goroutine 去执行 hello 函数,在程序中创建 goroutine 执行函数需要一定的开销,而此时 main goroutine 还在继续往下执行,我们的程序中此时存在两个并发执行的 goroutine。当 main 函数结束时整个程序也就结束了,同时 main goroutine 也结束了,所有由 main goroutine 创建的 goroutine 也会一同退出。所有我们需要对 main goroutine 进行一定的延时,等待其他 goroutine 执行完毕后再退出。
在上面的程序中使用 time.Sleep
让 main goroutine 等待 hello goroutine 执行结束是不优雅的,当然也是不准确的。
当你并不关心并发操作的结果或者有其它方式收集并发操作的结果时,WaitGroup
是实现等待一组并发操作完成的好方法。
下面的示例代码中我们在 main goroutine 中使用 sync.WaitGroup
来等待 hello goroutine 完成后再退出。
sync.WaitGroup
在代码中生硬的使用 time.Sleep
肯定是不合适的,Go语言中可以使用 sync.WaitGroup
来实现并发任务的同步。 sync.WaitGroup
有以下几个方法:
方法名 | 功能 |
---|---|
func (wg * WaitGroup) Add(delta int) | 计数器+delta |
(wg *WaitGroup) Done() | 计数器-1 |
(wg *WaitGroup) Wait() | 阻塞直到计数器变为0 |
sync.WaitGroup
内部维护着一个计数器,计数器的值可以增加和减少。例如当我们启动了 N 个并发任务时,就将计数器值增加N。每个任务完成时通过调用 Done 方法将计数器减1。通过调用 Wait 来等待并发任务执行完,当计数器值为 0 时,表示所有并发任务已经完成。
需要注意sync.WaitGroup
是一个结构体,进行参数传递的时候要传递指针。
// 声明全局等待组变量
var wg sync.WaitGroup
func hello() {
defer wg.Done() // 告知当前goroutine完成
fmt.Println("hello")
}
func main() {
wg.Add(1) // 登记1个goroutine
go hello() // 启动另外一个goroutine去执行hello函数
fmt.Println("你好")
wg.Wait() // 阻塞等待登记的goroutine完成
}
将代码编译后再执行,得到的输出结果和之前一致,但是这一次程序不再会有多余的停顿,hello goroutine 执行完毕后程序直接退出。
启动多个goroutine
var wg sync.WaitGroup
func hello(i int) {
defer wg.Done() // goroutine结束就登记-1
fmt.Println("hello", i)
}
func main() {
for i := 0; i < 10; i++ {
wg.Add(1) // 启动一个goroutine就登记+1
go hello(i)
}
wg.Wait() // 等待所有登记的goroutine都结束
}
多次执行上面的代码会发现每次终端上打印数字的顺序都不一致。这是因为10个 goroutine 是并发执行的,而 goroutine 的调度是随机的。
动态栈
操作系统的线程一般都有固定的栈内存(通常为2MB),而 Go 语言中的 goroutine 非常轻量级,一个 goroutine 的初始栈空间很小(一般为2KB),所以在 Go 语言中一次创建数万个 goroutine 也是可能的。并且 goroutine 的栈不是固定的,可以根据需要动态地增大或缩小, Go 的 runtime 会自动为 goroutine 分配合适的栈空间。
goroutine调度
操作系统的线程会被操作系统内核调度时会挂起当前执行的线程并将它的寄存器内容保存到内存中,选出下一次要执行的线程并从内存中恢复该线程的寄存器信息,然后恢复执行该线程的现场并开始执行线程。从一个线程切换到另一个线程需要完整的上下文切换。因为可能需要多次内存访问,索引这个切换上下文的操作开销较大,会增加运行的cpu周期。
区别于操作系统内核调度操作系统线程,goroutine 的调度是Go语言运行时(runtime)层面的实现,是完全由 Go 语言本身实现的一套调度系统——go scheduler。它的作用是按照一定的规则将所有的 goroutine 调度到操作系统线程上执行。
在经历数个版本的迭代之后,目前 Go 语言的调度器采用的是 GPM
调度模型。
- G:表示 goroutine,每执行一次
go f()
就创建一个 G,包含要执行的函数和上下文信息。 - 全局队列(Global Queue):存放等待运行的 G。
- P:表示 goroutine 执行所需的资源,最多有 GOMAXPROCS 个。
- P 的本地队列:同全局队列类似,存放的也是等待运行的G,存的数量有限,不超过256个。新建 G 时,G 优先加入到 P 的本地队列,如果本地队列满了会批量移动部分 G 到全局队列。
- M:线程想运行任务就得获取 P,从 P 的本地队列获取 G,当 P 的本地队列为空时,M 也会尝试从全局队列或其他 P 的本地队列获取 G。M 运行 G,G 执行之后,M 会从 P 获取下一个 G,不断重复下去。
- Goroutine 调度器和操作系统调度器是通过 M 结合起来的,每个 M 都代表了1个内核线程,操作系统调度器负责把内核线程分配到 CPU 的核上执行。
单从线程调度讲,Go语言相比起其他语言的优势在于OS线程是由OS内核来调度的, goroutine 则是由Go运行时(runtime)自己的调度器调度的,完全是在用户态下完成的, 不涉及内核态与用户态之间的频繁切换,包括内存的分配与释放,都是在用户态维护着一块大的内存池, 不直接调用系统的malloc函数(除非内存池需要改变),成本比调度OS线程低很多。 另一方面充分利用了多核的硬件资源,近似的把若干goroutine均分在物理线程上, 再加上本身 goroutine 的超轻量级,以上种种特性保证了 goroutine 调度方面的性能。
GOMAXPROCS
Go运行时的调度器使用 GOMAXPROCS
参数来确定需要使用多少个 OS 线程来同时执行 Go 代码。默认值是机器上的 CPU 核心数。例如在一个 8 核心的机器上,GOMAXPROCS 默认为 8。Go语言中可以通过 runtime.GOMAXPROCS
函数设置当前程序并发时占用的 CPU逻辑核心数。(Go1.5版本之前,默认使用的是单核心执行。Go1.5 版本之后,默认使用全部的CPU 逻辑核心数。)
channel
单纯地将函数并发执行是没有意义的。函数与函数间需要交换数据才能体现并发执行函数的意义。
虽然可以使用共享内存进行数据交换,但是共享内存在不同的 goroutine 中容易发生竞态问题。为了保证数据交换的正确性,很多并发模型中必须使用互斥量对内存进行加锁,这种做法势必造成性能问题。
Go语言采用的并发模型是 CSP(Communicating Sequential Processes)
,提倡通过通信共享内存而不是通过共享内存而实现通信。
如果说 goroutine 是Go程序并发的执行体,channel
就是它们之间的连接。channel
是可以让一个 goroutine 发送特定值到另一个 goroutine 的通信机制。
Go 语言中的通道(channel)是一种特殊的类型。通道像一个传送带或者队列,总是遵循先入先出(First In First Out)的规则,保证收发数据的顺序。每一个通道都是一个具体类型的导管,也就是声明channel的时候需要为其指定元素类型。
channel类型
channel
是 Go 语言中一种特有的类型。声明通道类型变量的格式如下:
var 变量名称 chan 元素类型
其中:
- chan:是关键字
- 元素类型:是指通道中传递元素的类型
举几个例子:
var ch1 chan int // 声明一个传递整型的通道
var ch2 chan bool // 声明一个传递布尔型的通道
var ch3 chan []int // 声明一个传递int切片的通道
channel零值
未初始化的通道类型变量其默认零值是 nil
var ch chan int
fmt.Println(ch) // <nil>
初始化channel
声明的通道类型变量需要使用内置的 make
函数初始化之后才能使用
make(chan 元素类型, [缓冲大小])
其中:channel的缓冲大小是可选的
ch4 := make(chan int)
ch5 := make(chan bool, 1) // 声明一个缓冲区大小为1的通道
channel操作
通道共有发送(send)、接收(receive)和关闭(close)三种操作。而发送和接收操作都使用 <-
符号
ch := make(chan int)
发送:将一个值发送到通道中
ch <- 10 // 把10发送到ch中
接收:从一个通道中接收值
x := <- ch // 从ch中接收值并赋值给变量x
<-ch // 从ch中接收值,忽略结果
关闭:通过调用内置的 close
函数来关闭通道
close(ch)
注意:一个通道值是可以被垃圾回收掉的。通道通常由发送方执行关闭操作,并且只有在接收方明确等待通道关闭的信号时才需要执行关闭操作。它和关闭文件不一样,通常在结束操作之后关闭文件是必须要做的,但关闭通道不是必须的。
关闭后的通道有以下特点:
- 对一个关闭的通道再发送值就会导致 panic。
- 对一个关闭的通道进行接收会一直获取值直到通道为空。
- 对一个关闭的并且没有值的通道执行接收操作会得到对应类型的零值。
- 关闭一个已经关闭的通道会导致 panic。
无缓冲的通道
无缓冲的通道又称为阻塞的通道
func main() {
ch := make(chan int)
ch <- 10
fmt.Println("发送成功")
}
上面这段代码能够通过编译,但是执行的时候会出现以下错误:
fatal error: all goroutines are asleep - deadlock!
goroutine 1 [chan send]:
main.main()
.../main.go:9 +0x31
exit status 2
deadlock
表示我们程序中的 goroutine 都被挂起导致程序死锁,这因为我们使用 ch := make(chan int) 创建的是无缓冲的通道,无缓冲的通道只有在有接收方能够接收值的时候才能发送成功,否则会一直处于等待发送的阶段。同理,如果对一个无缓冲通道执行接收操作时,没有任何向通道中发送值的操作那么也会导致接收操作阻塞。就像田径比赛中的4x100接力赛,想要完成交棒必须有一个能够接棒的运动员,否则只能等待。简单来说就是无缓冲的通道必须有至少一个接收方才能发送成功。
因为我们使用 ch := make(chan int)
创建的是无缓冲的通道,无缓冲的通道只有在有接收方能够接收值的时候才能发送成功,否则会一直处于等待发送的阶段。同理,如果对一个无缓冲通道执行接收操作时,没有任何向通道中发送值的操作那么也会导致接收操作阻塞。就像田径比赛中的4x100接力赛,想要完成交棒必须有一个能够接棒的运动员,否则只能等待。简单来说就是无缓冲的通道必须有至少一个接收方才能发送成功。
上面的代码会阻塞在 ch <- 10
这一行代码形成死锁,那如何解决这个问题呢?
其中一种可行的方法是创建一个 goroutine 去接收值,例如:
func recv(c chan int) {
ret := <-c
fmt.Println("接收成功", ret)
}
func main() {
ch := make(chan int)
go recv(ch) // 创建一个 goroutine 从通道接收值
ch <- 10
fmt.Println("发送成功")
}
首先无缓冲通道 ch
上的发送操作会阻塞,直到另一个 goroutine 在该通道上执行接收操作,这时数字10才能发送成功,两个 goroutine 将继续执行。相反,如果接收操作先执行,接收方所在的 goroutine 将阻塞,直到 main goroutine 中向该通道发送数字10。
使用无缓冲通道进行通信将导致发送和接收的 goroutine 同步化。因此,无缓冲通道也被称为同步通道
。
有缓冲的通道
还有另外一种解决上面死锁问题的方法,那就是使用有缓冲区的通道。我们可以在使用 make 函数初始化通道时,可以为其指定通道的容量,例如:
func main() {
ch := make(chan int, 1) // 创建一个容量为1的有缓冲区通道
ch <- 10
fmt.Println("发送成功")
}
只要通道的容量大于零,那么该通道就属于有缓冲的通道,通道的容量表示通道中最大能存放的元素数量。当通道内已有元素数达到最大容量后,再向通道执行发送操作就会阻塞,除非有从通道执行接收操作。就像你小区的快递柜只有那么个多格子,格子满了就装不下了,就阻塞了,等到别人取走一个快递员就能往里面放一个。
我们可以使用内置的 len
函数获取通道内元素的数量,使用 cap
函数获取通道的容量,虽然我们很少会这么做。
多返回值模式
当向通道中发送完数据时,我们可以通过 close
函数来关闭通道。当一个通道被关闭后,再往该通道发送值会引发 panic
,从该通道取值的操作会先取完通道中的值。通道内的值被接收完后再对通道执行接收操作得到的值会一直都是对应元素类型的零值。那我们如何判断一个通道是否被关闭了呢?
对一个通道执行接收操作时支持使用如下多返回值模式。
value, ok := <- ch
其中:
- value:从通道中取出的值,如果通道被关闭则返回对应类型的零值。
- ok:通道ch关闭时返回 false,否则返回 true。
下面代码片段中的 f2
函数会循环从通道 ch
中接收所有值,直到通道被关闭后退出。
func f2(ch chan int) {
for {
v, ok := <-ch
if !ok {
fmt.Println("通道已关闭")
break
}
fmt.Printf("v:%#v ok:%#v\n", v, ok)
}
}
func main() {
ch := make(chan int, 1)
go f2(ch)
ch <- 1
ch <- 2
close(ch)
fmt.Println("发送成功")
}
for range接收值
通常我们会选择使用 for range
循环从通道中接收值,当通道被关闭后,会在通道内的所有值被接收完毕后会自动退出循环。上面那个示例我们使用 for range
改写后会很简洁。
func f3(ch chan int) {
for v := range ch {
fmt.Println(v)
}
}
func main() {
ch := make(chan int, 1)
go f3(ch)
ch <- 1
ch <- 2
close(ch)
fmt.Println("发送成功")
}
注意:目前Go语言中并没有提供一个不对通道进行读取操作就能判断通道是否被关闭的方法。不能简单的通过 len(ch)
操作来判断通道是否被关闭。
单向通道
在某些场景下我们可能会将通道作为参数在多个任务函数间进行传递,通常我们会选择在不同的任务函数中对通道的使用进行限制,比如限制通道在某个函数中只能执行发送或只能执行接收操作。想象一下,我们现在有 Producer
和 Consumer
两个函数,其中 Producer
函数会返回一个通道,并且会持续将符合条件的数据发送至该通道,并在发送完成后将该通道关闭。而 Consumer
函数的任务是从通道中接收值进行计算,这两个函数之间通过 Processer
函数返回的通道进行通信。
// Producer 返回一个通道
// 并持续将符合条件的数据发送至返回的通道中
// 数据发送完成后会将返回的通道关闭
func Producer() chan int {
ch := make(chan int, 2)
// 创建一个新的goroutine执行发送数据的任务
go func() {
for i := 0; i < 10; i++ {
if i%2 == 1 {
ch <- i
}
}
close(ch) // 任务完成后关闭通道
}()
return ch
}
// Consumer 从通道中接收数据进行计算
func Consumer(ch chan int) int {
sum := 0
for v := range ch {
sum += v
}
return sum
}
func main() {
ch := Producer()
res := Consumer(ch)
fmt.Println(res) // 25
}
从上面的示例代码中可以看出正常情况下 Consumer
函数中只会对通道进行接收操作,但是这不代表不可以在 Consumer
函数中对通道进行发送操作。作为 Producer
函数的提供者,我们在返回通道的时候可能只希望调用方拿到返回的通道后只能对其进行接收操作。但是我们没有办法阻止在 Consumer
函数中对通道进行发送操作。
Go语言中提供了单向通道来处理这种需要限制通道只能进行某种操作的情况。
<- chan int // 只收通道,只能接收不能发送
chan <- int // 只发通道,只能发送不能接收
其中,箭头 <-
和关键字 chan
的相对位置表明了当前通道允许的操作,这种限制将在编译阶段进行检测。另外对一个只接收通道执行close也是不允许的,因为默认通道的关闭操作应该由发送方来完成。
我们使用单向通道将上面的示例代码进行如下改造。
// Producer2 返回一个接收通道
func Producer2() <-chan int {
ch := make(chan int, 2)
// 创建一个新的goroutine执行发送数据的任务
go func() {
for i := 0; i < 10; i++ {
if i%2 == 1 {
ch <- i
}
}
close(ch) // 任务完成后关闭通道
}()
return ch
}
// Consumer2 参数为接收通道
func Consumer2(ch <-chan int) int {
sum := 0
for v := range ch {
sum += v
}
return sum
}
func main() {
ch2 := Producer2()
res2 := Consumer2(ch2)
fmt.Println(res2) // 25
}
这一次,Producer
函数返回的是一个只接收通道,这就从代码层面限制了该函数返回的通道只能进行接收操作,保证了数据安全。很多读者看到这个示例可能会觉着这样的限制是多余的,但是试想一下如果 Producer
函数可以在其他地方被其他人调用,你该如何限制他人不对该通道执行发送操作呢?并且返回限制操作的单向通道也会让代码语义更清晰、更易读。
在函数传参及任何赋值操作中全向通道(正常通道)可以转换为单向通道,但是无法反向转换。
func main() {
var ch3 = make(chan int, 1)
ch3 <- 10
close(ch3)
Consumer2(ch3) // 函数传参时将ch3转为单向通道
var ch4 = make(chan int, 1)
ch4 <- 10
var ch5 <-chan int = ch4 // 声明一个只接收通道ch5,变量赋值时将ch4转为单向通道
<-ch5
}
总结
下面的表格中总结了对不同状态下的通道执行相应操作的结果。
注意:对已经关闭的通道再执行 close 也会引发 panic。
select多路复用
在某些场景下我们可能需要同时从多个通道接收数据。通道在接收数据时,如果没有数据可以被接收那么当前 goroutine 将会发生阻塞。你也许会写出如下代码尝试使用遍历的方式来实现从多个通道中接收值。Go 语言内置了 select
关键字,使用它可以同时响应多个通道的操作。
select {
case <-ch1:
//...
case data := <-ch2:
//...
case ch3 <- 10:
//...
default:
//默认操作
}
Select 语句具有以下特点。
- 可处理一个或多个 channel 的发送/接收操作。
- 如果多个 case 同时满足,select 会随机选择一个执行。
- 对于没有 case 的 select 会一直阻塞,可用于阻塞 main 函数,防止退出。
下面的示例代码能够在终端打印出10以内的奇数,我们借助这个代码片段来看一下 select 的具体使用。
package main
import "fmt"
func main() {
ch := make(chan int, 1)
for i := 1; i <= 10; i++ {
select {
case x := <-ch:
fmt.Println(x)
case ch <- i:
}
}
}
输出:
1
3
5
7
9
示例中的代码首先是创建了一个缓冲区大小为1的通道 ch,在进入 for 循环后,此时 i = 1,select 语句中包含两个 case 分支,此时由于通道中没有值可以接收,所以 x := <-c
这个 case 分支不满足,而 ch <- i
这个分支可以执行,会把1发送到通道中,结束本次 for 循环;第二次 for 循环时,i = 2,由于通道缓冲区已满,所以 ch <- i
这个分支不满足,而 x := <-ch
这个分支可以执行,从通道接收值1并赋值给变量 x ,所以会在终端打印出 1;后续的 for 循环同理会依次打印出3、5、7、9。
通道误用示例
示例1
// demo1 通道误用导致的bug
func demo1() {
wg := sync.WaitGroup{}
ch := make(chan int, 10)
for i := 0; i < 10; i++ {
ch <- i
}
close(ch)
wg.Add(3)
for j := 0; j < 3; j++ {
go func() {
for {
task := <-ch
// 这里假设对接收的数据执行某些操作
fmt.Println(task)
}
wg.Done()
}()
}
wg.Wait()
}
将上述代码编译执行后,匿名函数所在的 goroutine 并不会按照预期在通道被关闭后退出。因为 task := <- ch
的接收操作在通道被关闭后会一直接收到零值,而不会退出。此处的接收操作应该使用 task, ok := <- ch
,通过判断布尔值 ok
为假时退出;或者使用 select 来处理通道。
示例2
// demo2 通道误用导致的bug
func demo2() {
ch := make(chan string)
go func() {
// 这里假设执行一些耗时的操作
time.Sleep(3 * time.Second)
ch <- "job result"
}()
select {
case result := <-ch:
fmt.Println(result)
case <-time.After(time.Second): // 较小的超时时间
return
}
}
上述代码片段可能导致 goroutine 泄露(goroutine 并未按预期退出并销毁)。由于 select 命中了超时逻辑,导致通道没有消费者(无接收操作),而其定义的通道为无缓冲通道,因此 goroutine 中的 ch <- "job result"
操作会一直阻塞,最终导致 goroutine 泄露。
并发安全和锁
有时候我们的代码中可能会存在多个 goroutine 同时操作一个资源(临界区)的情况,这种情况下就会发生 竞态问题
(数据竞态)。这就好比现实生活中十字路口被各个方向的汽车竞争,还有火车上的卫生间被车厢里的人竞争。
var (
x int64
wg sync.WaitGroup // 等待组
)
// add 对全局变量x执行5000次加1操作
func add() {
for i := 0; i < 5000; i++ {
x = x + 1
}
wg.Done()
}
func main() {
wg.Add(2)
go add()
go add()
wg.Wait()
fmt.Println(x)
}
我们将上面的代码编译后执行,不出意外每次执行都会输出诸如9537、5865、6527等不同的结果。这是为什么呢?
在上面的示例代码片中,我们开启了两个 goroutine 分别执行 add 函数,这两个 goroutine 在访问和修改全局的 x
变量时就会存在数据竞争,某个 goroutine 中对全局变量 x
的修改可能会覆盖掉另一个 goroutine 中的操作,所以导致最后的结果与预期不符。
互斥锁
互斥锁是一种常用的控制共享资源访问的方法,它能够保证同一时间只有一个 goroutine 可以访问共享资源。Go 语言中使用 sync
包中提供的 Mutex
类型来实现互斥锁。
sync.Mutex
提供了两个方法供我们使用。
方法名 | 功能 |
---|---|
func (m *Mutex) Lock() | 获取互斥锁 |
func (m *Mutex) Unlock() | 释放互斥锁 |
我们在下面的示例代码中使用互斥锁限制每次只有一个 gorouti
var (
x int64
wg sync.WaitGroup // 等待组
m sync.Mutex // 互斥锁
)
// add 对全局变量x执行5000次加1操作
func add() {
for i := 0; i < 5000; i++ {
m.Lock() // 修改x前加锁
x = x + 1
m.Unlock() // 改完解锁
}
wg.Done()
}
func main() {
wg.Add(2)
go add()
go add()
wg.Wait()
fmt.Println(x)
}
将上面的代码编译后多次执行,每一次都会得到预期中的结果——10000。
使用互斥锁能够保证同一时间有且只有一个 goroutine 进入临界区,其他的 goroutine 则在等待锁;当互斥锁释放后,等待的 goroutine 才可以获取锁进入临界区,多个 goroutine 同时等待一个锁时,唤醒的策略是随机的。
读写互斥锁
互斥锁是完全互斥的,但是实际上有很多场景是读多写少的,当我们并发的去读取一个资源而不涉及资源修改的时候是没有必要加互斥锁的,这种场景下使用读写锁是更好的一种选择。读写锁在 Go 语言中使用 sync
包中的 RWMutex
类型。
sync.RWMutex
提供了以下5个方法。
方法名 | 功能 |
---|---|
func (rw *RWMutex) Lock() | 获取写锁 |
func (rw *RWMutex) Unlock() | 释放写锁 |
func (rw *RWMutex) RLock() | 获取读锁 |
func (rw *RWMutex) RUnlock() | 释放读锁 |
func (rw *RWMutex) RLocker() Locker | 返回一个实现Locker接口的读写锁 |
读写锁分为两种:读锁和写锁。当一个 goroutine 获取到读锁之后,其他的 goroutine 如果是获取读锁会继续获得锁,如果是获取写锁就会等待;而当一个 goroutine 获取写锁之后,其他的 goroutine 无论是获取读锁还是写锁都会等待。
下面我们使用代码构造一个读多写少的场景,然后分别使用互斥锁和读写锁查看它们的性能差异。
var (
x int64
wg sync.WaitGroup
mutex sync.Mutex
rwMutex sync.RWMutex
)
// writeWithLock 使用互斥锁的写操作
func writeWithLock() {
mutex.Lock() // 加互斥锁
x = x + 1
time.Sleep(10 * time.Millisecond) // 假设读操作耗时10毫秒
mutex.Unlock() // 解互斥锁
wg.Done()
}
// readWithLock 使用互斥锁的读操作
func readWithLock() {
mutex.Lock() // 加互斥锁
time.Sleep(time.Millisecond) // 假设读操作耗时1毫秒
mutex.Unlock() // 释放互斥锁
wg.Done()
}
// writeWithLock 使用读写互斥锁的写操作
func writeWithRWLock() {
rwMutex.Lock() // 加写锁
x = x + 1
time.Sleep(10 * time.Millisecond) // 假设读操作耗时10毫秒
rwMutex.Unlock() // 释放写锁
wg.Done()
}
// readWithRWLock 使用读写互斥锁的读操作
func readWithRWLock() {
rwMutex.RLock() // 加读锁
time.Sleep(time.Millisecond) // 假设读操作耗时1毫秒
rwMutex.RUnlock() // 释放读锁
wg.Done()
}
func do(wf, rf func(), wc, rc int) {
start := time.Now()
// wc个并发写操作
for i := 0; i < wc; i++ {
wg.Add(1)
go wf()
}
// rc个并发读操作
for i := 0; i < rc; i++ {
wg.Add(1)
go rf()
}
wg.Wait()
cost := time.Since(start)
fmt.Printf("x:%v cost:%v\n", x, cost)
}
func main() {
// 使用互斥锁,10并发写,1000并发读
do(writeWithLock, readWithLock, 10, 1000) // x:10 cost:15.7487139s
// 使用读写互斥锁,10并发写,1000并发读
do(writeWithRWLock, readWithRWLock, 10, 1000) // x:20 cost:174.1578ms
}
从最终的执行结果可以看出,使用读写互斥锁在读多写少的场景下能够极大地提高程序的性能。不过需要注意的是如果一个程序中的读操作和写操作数量级差别不大,那么读写互斥锁的优势就发挥不出来。
sync.Once
在某些场景下我们需要确保某些操作即使在高并发的场景下也只会被执行一次,例如只加载一次配置文件等。
Go语言中的 sync
包中提供了一个针对只执行一次场景的解决方案—— sync.Once
,sync.Once
只有一个 Do
方法,其签名如下:
func (o *Once) Do(f func())
注意:如果要执行的函数 f
需要传递参数就需要搭配闭包来使用。
加载配置文件示例
延迟一个开销很大的初始化操作到真正用到它的时候再执行是一个很好的实践。因为预先初始化一个变量(比如在init函数中完成初始化)会增加程序的启动耗时,而且有可能实际执行过程中这个变量没有用上,那么这个初始化操作就不是必须要做的。我们来看一个例子:
var icons map[string]image.Image
func loadIcons() {
icons = map[string]image.Image{
"left": loadIcon("left.png"),
"up": loadIcon("up.png"),
"right": loadIcon("right.png"),
"down": loadIcon("down.png"),
}
}
// Icon 被多个 goroutine 调用时不是并发安全的
func Icon(name string) image.Image {
if icons == nil {
loadIcons()
}
return icons[name]
}
多个 goroutine 并发调用 Icon 函数时不是并发安全的,现代的编译器和CPU可能会在保证每个 goroutine 都满足串行一致的基础上自由地重排访问内存的顺序。loadIcons函数可能会被重排为以下结果:
func loadIcons() {
icons = make(map[string]image.Image)
icons["left"] = loadIcon("left.png")
icons["up"] = loadIcon("up.png")
icons["right"] = loadIcon("right.png")
icons["down"] = loadIcon("down.png")
}
在这种情况下就会出现即使判断了 icons
不是 nil 也不意味着变量初始化完成了。考虑到这种情况,我们能想到的办法就是添加互斥锁,保证初始化icons
的时候不会被其他的 goroutine 操作,但是这样做又会引发性能问题。
使用 sync.Once
改造的示例代码如下:
var icons map[string]image.Image
var loadIconsOnce sync.Once
func loadIcons() {
icons = map[string]image.Image{
"left": loadIcon("left.png"),
"up": loadIcon("up.png"),
"right": loadIcon("right.png"),
"down": loadIcon("down.png"),
}
}
// Icon 是并发安全的
func Icon(name string) image.Image {
loadIconsOnce.Do(loadIcons)
return icons[name]
}
并发安全的单例模式
下面是借助 sync.Once
实现的并发安全的单例模式:
type singleton struct {}
var instance *singleton
var once sync.Once
func GetInstance() *singleton {
once.Do(func() {
instance = &singleton{}
})
return instance
}
sync.Once
其实内部包含一个互斥锁和一个布尔值,互斥锁保证布尔值和数据的安全,而布尔值用来记录初始化是否完成。这样设计就能保证初始化操作的时候是并发安全的并且初始化操作也不会被执行多次。
sync.Map
Go 语言中内置的 map 不是并发安全的,请看下面这段示例代码。
var m = make(map[string]int)
func get(key string) int {
return m[key]
}
func set(key string, value int) {
m[key] = value
}
func main() {
wg := sync.WaitGroup{}
for i := 0; i < 10; i++ {
wg.Add(1)
go func(n int) {
key := strconv.Itoa(n)
set(key, n)
fmt.Printf("k=:%v,v:=%v\n", key, get(key))
wg.Done()
}(i)
}
wg.Wait()
}
将上面的代码编译后执行,会报出 fatal error: concurrent map writes
错误。我们不能在多个 goroutine 中并发对内置的 map 进行读写操作,否则会存在数据竞争问题。
像这种场景下就需要为 map 加锁来保证并发的安全性了,Go语言的sync
包中提供了一个开箱即用的并发安全版 map—— sync.Map
。开箱即用表示其不用像内置的 map 一样使用 make 函数初始化就能直接使用。同时 sync.Map
内置了诸如 Store
、Load
、LoadOrStore
、Delete
、Range
等操作方法。
方法名 | 功能 |
---|---|
func (m *Map) Store(key, value interface{}) | 存储key-value数据 |
func (m *Map) Load(key interface{}) (value interface{}, ok bool) | 查询key对应的value |
func (m *Map) LoadOrStore(key, value interface{}) (actual interface{}, loaded bool) | 查询或存储key对应的value |
func (m *Map) LoadAndDelete(key interface{}) (value interface{}, loaded bool) | 查询并删除key |
func (m *Map) Delete(key interface{}) | 删除key |
func (m *Map) Range(f func(key, value interface{}) bool) | 对map中的每个key-value依次调用f |
下面的代码示例演示了并发读写 sync.Map
。
// 并发安全的map
var m = sync.Map{}
func main() {
wg := sync.WaitGroup{}
// 对m执行20个并发的读写操作
for i := 0; i < 20; i++ {
wg.Add(1)
go func(n int) {
key := strconv.Itoa(n)
m.Store(key, n) // 存储key-value
value, _ := m.Load(key) // 根据key取值
fmt.Printf("k=:%v,v:=%v\n", key, value)
wg.Done()
}(i)
}
wg.Wait()
}
原子操作
针对整数数据类型(int32、uint32、int64、uint64)我们还可以使用原子操作来保证并发安全,通常直接使用原子操作比使用锁操作效率更高。Go语言中原子操作由内置的标准库sync/atomic
提供。
atomic包
方法 | 解释 |
---|---|
func LoadInt32(addr *int32) (val int32) func LoadInt64(addr *int64) (val int64) func LoadUint32(addr *uint32) (val uint32) func LoadUint64(addr *uint64) (val uint64) func LoadUintptr(addr *uintptr) (val uintptr) func LoadPointer(addr *unsafe.Pointer) (val unsafe.Pointer) |
读取操作 |
func StoreInt32(addr *int32, val int32) func StoreInt64(addr *int64, val int64) func StoreUint32(addr *uint32, val uint32) func StoreUint64(addr *uint64, val uint64) func StoreUintptr(addr *uintptr, val uintptr) func StorePointer(addr *unsafe.Pointer, val unsafe.Pointer) |
写入操作 |
func AddInt32(addr *int32, delta int32) (new int32) func AddInt64(addr *int64, delta int64) (new int64) func AddUint32(addr *uint32, delta uint32) (new uint32) func AddUint64(addr *uint64, delta uint64) (new uint64) func AddUintptr(addr *uintptr, delta uintptr) (new uintptr) |
修改操作 |
func SwapInt32(addr *int32, new int32) (old int32) func SwapInt64(addr *int64, new int64) (old int64) func SwapUint32(addr *uint32, new uint32) (old uint32) func SwapUint64(addr *uint64, new uint64) (old uint64) func SwapUintptr(addr *uintptr, new uintptr) (old uintptr) func SwapPointer(addr *unsafe.Pointer, new unsafe.Pointer) (old unsafe.Pointer) |
交换操作 |
func CompareAndSwapInt32(addr *int32, old, new int32) (swapped bool) func CompareAndSwapInt64(addr *int64, old, new int64) (swapped bool) func CompareAndSwapUint32(addr *uint32, old, new uint32) (swapped bool) func CompareAndSwapUint64(addr *uint64, old, new uint64) (swapped bool) func CompareAndSwapUintptr(addr *uintptr, old, new uintptr) (swapped bool) func CompareAndSwapPointer(addr *unsafe.Pointer, old, new unsafe.Pointer) (swapped bool) |
比较并交换操作 |
示例
我们填写一个示例来比较下互斥锁和原子操作的性能。
type Counter interface {
Inc()
Load() int64
}
// 普通版
type CommonCounter struct {
counter int64
}
func (c CommonCounter) Inc() {
c.counter++
}
func (c CommonCounter) Load() int64 {
return c.counter
}
// 互斥锁版
type MutexCounter struct {
counter int64
lock sync.Mutex
}
func (m *MutexCounter) Inc() {
m.lock.Lock()
defer m.lock.Unlock()
m.counter++
}
func (m *MutexCounter) Load() int64 {
m.lock.Lock()
defer m.lock.Unlock()
return m.counter
}
// 原子操作版
type AtomicCounter struct {
counter int64
}
func (a *AtomicCounter) Inc() {
atomic.AddInt64(&a.counter, 1)
}
func (a *AtomicCounter) Load() int64 {
return atomic.LoadInt64(&a.counter)
}
func test(c Counter) {
var wg sync.WaitGroup
start := time.Now()
for i := 0; i < 1000; i++ {
wg.Add(1)
go func() {
c.Inc()
wg.Done()
}()
}
wg.Wait()
end := time.Now()
fmt.Println(c.Load(), end.Sub(start))
}
func main() {
c1 := CommonCounter{} // 非并发安全
test(c1)
c2 := MutexCounter{} // 使用互斥锁实现并发安全
test(&c2)
c3 := AtomicCounter{} // 并发安全且比互斥锁效率更高
test(&c3)
}
atomic
包提供了底层的原子级内存操作,对于同步算法的实现很有用。这些函数必须谨慎地保证正确使用。除了某些特殊的底层应用,使用通道或者 sync 包的函数/类型实现同步更好。
处理并发错误
recover goroutine 中的 panic
我们知道可以在代码中使用 recover 来会恢复程序中意想不到的 panic,而 panic 只会触发当前 goroutine 中的 defer 操作。
例如在下面的示例代码中,无法在 main 函数中 recover 另一个 goroutine 中引发的 panic。
func f1() {
defer func() {
if e := recover(); e != nil {
fmt.Printf("recover panic:%v\n", e)
}
}()
// 开启一个goroutine执行任务
go func() {
fmt.Println("in goroutine....")
// 只能触发当前goroutine中的defer
panic("panic in goroutine")
}()
time.Sleep(time.Second)
fmt.Println("exit")
}
输出:
in goroutine....
panic: panic in goroutine
goroutine 6 [running]:
main.f1.func2()
/Users/liwenzhou/workspace/github/the-road-to-learn-golang/ch12/goroutine_recover.go:20 +0x65
created by main.f1
/Users/liwenzhou/workspace/github/the-road-to-learn-golang/ch12/goroutine_recover.go:17 +0x48
Process finished with exit code 2
从输出结果可以看到程序并没有正常退出,而是由于 panic 异常退出了(exit code 2)。
正如上面示例演示的那样,在启用 goroutine 去执行任务的场景下,如果想要 recover goroutine 中可能出现的 panic 就需要在 goroutine 中使用 recover。就像下面的 f2 函数那样。
func f2() {
defer func() {
if r := recover(); r != nil {
fmt.Printf("recover outer panic:%v\n", r)
}
}()
// 开启一个goroutine执行任务
go func() {
defer func() {
if r := recover(); r != nil {
fmt.Printf("recover inner panic:%v\n", r)
}
}()
fmt.Println("in goroutine....")
// 只能触发当前goroutine中的defer
panic("panic in goroutine")
}()
time.Sleep(time.Second)
fmt.Println("exit")
}
输出:
in goroutine....
recover inner panic:panic in goroutine
exit
程序中的 panic 被 recover 成功捕获,程序最终正常退出。
errgroup
在以往演示的并发示例中,我们通常像下面的示例代码那样在 go 关键字后,调用一个函数或匿名函数。
go func(){
// ...
}
go foo()
在之前讲解并发的代码示例中我们默认被并发的那些函数都不会返回错误,但真实的情况往往是事与愿违。
当我们想要将一个任务拆分成多个子任务交给多个 goroutine 去运行,这时我们该如何获取到子任务可能返回的错误呢?
假设我们有多个网址需要并发去获取它们的内容,这时候我们会写出类似下面的代码。
// fetchUrlDemo 并发获取url内容
func fetchUrlDemo() {
wg := sync.WaitGroup{}
var urls = []string{
"http://pkg.go.dev",
"http://www.liwenzhou.com",
"http://www.yixieqitawangzhi.com",
}
for _, url := range urls {
wg.Add(1)
go func(url string) {
defer wg.Done()
resp, err := http.Get(url)
if err == nil {
fmt.Printf("获取%s成功\n", url)
resp.Body.Close()
}
return // 如何将错误返回呢?
}(url)
}
wg.Wait()
// 如何获取goroutine中可能出现的错误呢?
}
执行上述fetchUrlDemo
函数得到如下输出结果,由于 http://www.yixieqitawangzhi.com 是我随意编造的一个并不真实存在的 url,所以对它的 HTTP 请求会返回错误。
获取http://pkg.go.dev成功
获取http://www.liwenzhou.com成功
在上面的示例代码中,我们开启了 3 个 goroutine 分别去获取3个 url 的内容。类似这种将任务分为若干个子任务的场景会有很多,那么我们如何获取子任务中可能出现的错误呢?
errgroup 包就是为了解决这类问题而开发的,它能为处理公共任务的子任务而开启的一组 goroutine 提供同步、error 传播和基于context 的取消功能。
errgroup 包中定义了一个 Group 类型,它包含了若干个不可导出的字段。
type Group struct {
cancel func()
wg sync.WaitGroup
errOnce sync.Once
err error
}
errgroup.Group 提供了Go
和Wait
两个方法。
func (g *Group) Go(f func() error)
- Go 函数会在新的 goroutine 中调用传入的函数f。
- 第一个返回非零错误的调用将取消该Group;下面的Wait方法会返回该错误
func (g *Group) Wait() error
- Wait 会阻塞直至由上述 Go 方法调用的所有函数都返回,然后从它们返回第一个非nil的错误(如果有)。
下面的示例代码演示了如何使用 errgroup 包来处理多个子任务 goroutine 中可能返回的 error。
// fetchUrlDemo2 使用errgroup并发获取url内容
func fetchUrlDemo2() error {
g := new(errgroup.Group) // 创建等待组(类似sync.WaitGroup)
var urls = []string{
"http://pkg.go.dev",
"http://www.liwenzhou.com",
"http://www.yixieqitawangzhi.com",
}
for _, url := range urls {
url := url // 注意此处声明新的变量
// 启动一个goroutine去获取url内容
g.Go(func() error {
resp, err := http.Get(url)
if err == nil {
fmt.Printf("获取%s成功\n", url)
resp.Body.Close()
}
return err // 返回错误
})
}
if err := g.Wait(); err != nil {
// 处理可能出现的错误
fmt.Println(err)
return err
}
fmt.Println("所有goroutine均成功")
return nil
}
执行上面的fetchUrlDemo2
函数会得到如下输出结果。
获取http://pkg.go.dev成功
获取http://www.liwenzhou.com成功
Get "http://www.yixieqitawangzhi.com": dial tcp: lookup www.yixieqitawangzhi.com: no such host
当子任务的 goroutine 中对http://www.yixieqitawangzhi.com
发起 HTTP 请求时会返回一个错误,这个错误会由 errgroup.Group 的 Wait 方法返回。
通过阅读下方 errgroup.Group 的 Go 方法源码,我们可以看到当任意一个函数 f 返回错误时,会通过g.errOnce.Do
只将第一个返回的错误记录,并且如果存在 cancel 方法则会调用cancel。
func (g *Group) Go(f func() error) {
g.wg.Add(1)
go func() {
defer g.wg.Done()
if err := f(); err != nil {
g.errOnce.Do(func() {
g.err = err
if g.cancel != nil {
g.cancel()
}
})
}
}()
}
那么如何创建带有 cancel 方法的 errgroup.Group 呢?
答案是通过 errorgroup 包提供的 WithContext 函数。
func WithContext(ctx context.Context) (*Group, context.Context)
WithContext 函数接收一个父 context,返回一个新的 Group 对象和一个关联的子 context 对象。下面的代码片段是一个官方文档给出的示例。
package main
import (
"context"
"crypto/md5"
"fmt"
"io/ioutil"
"log"
"os"
"path/filepath"
"golang.org/x/sync/errgroup"
)
// Pipeline demonstrates the use of a Group to implement a multi-stage
// pipeline: a version of the MD5All function with bounded parallelism from
// https://blog.golang.org/pipelines.
func main() {
m, err := MD5All(context.Background(), ".")
if err != nil {
log.Fatal(err)
}
for k, sum := range m {
fmt.Printf("%s:\t%x\n", k, sum)
}
}
type result struct {
path string
sum [md5.Size]byte
}
// MD5All reads all the files in the file tree rooted at root and returns a map
// from file path to the MD5 sum of the file's contents. If the directory walk
// fails or any read operation fails, MD5All returns an error.
func MD5All(ctx context.Context, root string) (map[string][md5.Size]byte, error) {
// ctx is canceled when g.Wait() returns. When this version of MD5All returns
// - even in case of error! - we know that all of the goroutines have finished
// and the memory they were using can be garbage-collected.
g, ctx := errgroup.WithContext(ctx)
paths := make(chan string)
g.Go(func() error {
return filepath.Walk(root, func(path string, info os.FileInfo, err error) error {
if err != nil {
return err
}
if !info.Mode().IsRegular() {
return nil
}
select {
case paths <- path:
case <-ctx.Done():
return ctx.Err()
}
return nil
})
})
// Start a fixed number of goroutines to read and digest files.
c := make(chan result)
const numDigesters = 20
for i := 0; i < numDigesters; i++ {
g.Go(func() error {
for path := range paths {
data, err := ioutil.ReadFile(path)
if err != nil {
return err
}
select {
case c <- result{path, md5.Sum(data)}:
case <-ctx.Done():
return ctx.Err()
}
}
return nil
})
}
go func() {
g.Wait()
close(c)
}()
m := make(map[string][md5.Size]byte)
for r := range c {
m[r.path] = r.sum
}
// Check whether any of the goroutines failed. Since g is accumulating the
// errors, we don't need to send them (or check for them) in the individual
// results sent on the channel.
if err := g.Wait(); err != nil {
return nil, err
}
return m, nil
}
或者这里有另外一个示例。
func GetFriends(ctx context.Context, user int64) (map[string]*User, error) {
g, ctx := errgroup.WithContext(ctx)
friendIds := make(chan int64)
// Produce
g.Go(func() error {
defer close(friendIds)
for it := GetFriendIds(user); ; {
if id, err := it.Next(ctx); err != nil {
if err == io.EOF {
return nil
}
return fmt.Errorf("GetFriendIds %d: %s", user, err)
} else {
select {
case <-ctx.Done():
return ctx.Err()
case friendIds <- id:
}
}
}
})
friends := make(chan *User)
// Map
workers := int32(nWorkers)
for i := 0; i < nWorkers; i++ {
g.Go(func() error {
defer func() {
// Last one out closes shop
if atomic.AddInt32(&workers, -1) == 0 {
close(friends)
}
}()
for id := range friendIds {
if friend, err := GetUserProfile(ctx, id); err != nil {
return fmt.Errorf("GetUserProfile %d: %s", user, err)
} else {
select {
case <-ctx.Done():
return ctx.Err()
case friends <- friend:
}
}
}
return nil
})
}
// Reduce
ret := map[string]*User{}
g.Go(func() error {
for friend := range friends {
ret[friend.Name] = friend
}
return nil
})
return ret, g.Wait()
}
网络编程
互联网协议介绍
互联网的核心是一系列协议,总称为”互联网协议”(Internet Protocol Suite),正是这一些协议规定了电脑如何连接和组网。我们理解了这些协议,就理解了互联网的原理。由于这些协议太过庞大和复杂,没有办法在这里一概而全,只能介绍一下我们日常开发中接触较多的几个协议。
互联网分层模型
如上图所示,互联网按照不同的模型划分会有不用的分层,但是不论按照什么模型去划分,越往上的层越靠近用户,越往下的层越靠近硬件。在软件开发中我们使用最多的是上图中将互联网划分为五个分层的模型。
接下来我们一层一层的自底向上介绍一下每一层。
物理层
我们的电脑要与外界互联网通信,需要先把电脑连接网络,我们可以用双绞线、光纤、无线电波等方式。这就叫做”实物理层”,它就是把电脑连接起来的物理手段。它主要规定了网络的一些电气特性,作用是负责传送0和1的电信号。
数据链路层
单纯的0和1没有任何意义,所以我们使用者会为其赋予一些特定的含义,规定解读电信号的方式:例如:多少个电信号算一组?每个信号位有何意义?这就是”数据链接层”的功能,它在”物理层”的上方,确定了物理层传输的0和1的分组方式及代表的意义。早期的时候,每家公司都有自己的电信号分组方式。逐渐地,一种叫做”以太网”(Ethernet)的协议,占据了主导地位。
以太网规定,一组电信号构成一个数据包,叫做”帧”(Frame)。每一帧分成两个部分:标头(Head)和数据(Data)。其中”标头”包含数据包的一些说明项,比如发送者、接受者、数据类型等等;”数据”则是数据包的具体内容。”标头”的长度,固定为18字节。”数据”的长度,最短为46字节,最长为1500字节。因此,整个”帧”最短为64字节,最长为1518字节。如果数据很长,就必须分割成多个帧进行发送。
那么,发送者和接受者是如何标识呢?以太网规定,连入网络的所有设备都必须具有”网卡”接口。数据包必须是从一块网卡,传送到另一块网卡。网卡的地址,就是数据包的发送地址和接收地址,这叫做MAC地址。每块网卡出厂的时候,都有一个全世界独一无二的MAC地址,长度是48个二进制位,通常用12个十六进制数表示。前6个十六进制数是厂商编号,后6个是该厂商的网卡流水号。有了MAC地址,就可以定位网卡和数据包的路径了。
我们会通过ARP协议来获取接受方的MAC地址,有了MAC地址之后,如何把数据准确的发送给接收方呢?其实这里以太网采用了一种很”原始”的方式,它不是把数据包准确送到接收方,而是向本网络内所有计算机都发送,让每台计算机读取这个包的”标头”,找到接收方的MAC地址,然后与自身的MAC地址相比较,如果两者相同,就接受这个包,做进一步处理,否则就丢弃这个包。这种发送方式就叫做”广播”(broadcasting)。
网络层
按照以太网协议的规则我们可以依靠MAC地址来向外发送数据。理论上依靠MAC地址,你电脑的网卡就可以找到身在世界另一个角落的某台电脑的网卡了,但是这种做法有一个重大缺陷就是以太网采用广播方式发送数据包,所有成员人手一”包”,不仅效率低,而且发送的数据只能局限在发送者所在的子网络。也就是说如果两台计算机不在同一个子网络,广播是传不过去的。这种设计是合理且必要的,因为如果互联网上每一台计算机都会收到互联网上收发的所有数据包,那是不现实的。
因此,必须找到一种方法区分哪些MAC地址属于同一个子网络,哪些不是。如果是同一个子网络,就采用广播方式发送,否则就采用”路由”方式发送。这就导致了”网络层”的诞生。它的作用是引进一套新的地址,使得我们能够区分不同的计算机是否属于同一个子网络。这套地址就叫做”网络地址”,简称”网址”。
“网络层”出现以后,每台计算机有了两种地址,一种是MAC地址,另一种是网络地址。两种地址之间没有任何联系,MAC地址是绑定在网卡上的,网络地址则是网络管理员分配的。网络地址帮助我们确定计算机所在的子网络,MAC地址则将数据包送到该子网络中的目标网卡。因此,从逻辑上可以推断,必定是先处理网络地址,然后再处理MAC地址。
规定网络地址的协议,叫做IP协议。它所定义的地址,就被称为IP地址。目前,广泛采用的是IP协议第四版,简称IPv4。IPv4这个版本规定,网络地址由32个二进制位组成,我们通常习惯用分成四段的十进制数表示IP地址,从0.0.0.0一直到255.255.255.255。
根据IP协议发送的数据,就叫做IP数据包。IP数据包也分为”标头”和”数据”两个部分:”标头”部分主要包括版本、长度、IP地址等信息,”数据”部分则是IP数据包的具体内容。IP数据包的”标头”部分的长度为20到60字节,整个数据包的总长度最大为65535字节。
传输层
有了MAC地址和IP地址,我们已经可以在互联网上任意两台主机上建立通信。但问题是同一台主机上会有许多程序都需要用网络收发数据,比如QQ和浏览器这两个程序都需要连接互联网并收发数据,我们如何区分某个数据包到底是归哪个程序的呢?也就是说,我们还需要一个参数,表示这个数据包到底供哪个程序(进程)使用。这个参数就叫做”端口”(port),它其实是每一个使用网卡的程序的编号。每个数据包都发到主机的特定端口,所以不同的程序就能取到自己所需要的数据。
“端口”是0到65535之间的一个整数,正好16个二进制位。0到1023的端口被系统占用,用户只能选用大于1023的端口。有了IP和端口我们就能实现唯一确定互联网上一个程序,进而实现网络间的程序通信。
我们必须在数据包中加入端口信息,这就需要新的协议。最简单的实现叫做UDP协议,它的格式几乎就是在数据前面,加上端口号。UDP数据包,也是由”标头”和”数据”两部分组成:”标头”部分主要定义了发出端口和接收端口,”数据”部分就是具体的内容。UDP数据包非常简单,”标头”部分一共只有8个字节,总长度不超过65,535字节,正好放进一个IP数据包。
UDP协议的优点是比较简单,容易实现,但是缺点是可靠性较差,一旦数据包发出,无法知道对方是否收到。为了解决这个问题,提高网络可靠性,TCP协议就诞生了。TCP协议能够确保数据不会遗失。它的缺点是过程复杂、实现困难、消耗较多的资源。TCP数据包没有长度限制,理论上可以无限长,但是为了保证网络的效率,通常TCP数据包的长度不会超过IP数据包的长度,以确保单个TCP数据包不必再分割。
应用层
应用程序收到”传输层”的数据,接下来就要对数据进行解包。由于互联网是开放架构,数据来源五花八门,必须事先规定好通信的数据格式,否则接收方根本无法获得真正发送的数据内容。”应用层”的作用就是规定应用程序使用的数据格式,例如我们TCP协议之上常见的Email、HTTP、FTP等协议,这些协议就组成了互联网协议的应用层。
如下图所示,发送方的HTTP数据经过互联网的传输过程中会依次添加各层协议的标头信息,接收方收到数据包之后再依次根据协议解包得到数据。
socket编程
Socket是BSD UNIX的进程通信机制,通常也称作”套接字”,用于描述IP地址和端口,是一个通信链的句柄。Socket可以理解为TCP/IP网络的API,它定义了许多函数或例程,程序员可以用它们来开发TCP/IP网络上的应用程序。电脑上运行的应用程序通常通过”套接字”向网络发出请求或者应答网络请求。
socket图解
Socket
是应用层与TCP/IP协议族通信的中间软件抽象层。在设计模式中,Socket
其实就是一个门面模式,它把复杂的TCP/IP协议族隐藏在 Socket
后面,对用户来说只需要调用Socket规定的相关函数,让 Socket
去组织符合指定的协议数据然后进行通信。
Go语言实现TCP通信
TCP协议
TCP/IP(Transmission Control Protocol/Internet Protocol) 即传输控制协议/网间协议,是一种面向连接(连接导向)的、可靠的、基于字节流的传输层(Transport layer)通信协议,因为是面向连接的协议,数据像水流一样传输,会存在黏包问题。
TCP服务端
一个TCP服务端可以同时连接很多个客户端,例如世界各地的用户使用自己电脑上的浏览器访问淘宝网。因为Go语言中创建多个 goroutine 实现并发非常方便和高效,所以我们可以每建立一次链接就创建一个 goroutine 去处理。
TCP服务端程序的处理流程:
- 监听端口
- 接收客户端请求建立链接
- 创建 goroutine 处理链接。
我们使用 Go 语言的 net 包实现的 TCP 服务端代码如下:
// tcp/server/main.go
// TCP server端
// 处理函数
func process(conn net.Conn) {
defer conn.Close() // 关闭连接
for {
reader := bufio.NewReader(conn)
var buf [128]byte
n, err := reader.Read(buf[:]) // 读取数据
if err != nil {
fmt.Println("read from client failed, err:", err)
break
}
recvStr := string(buf[:n])
fmt.Println("收到client端发来的数据:", recvStr)
conn.Write([]byte(recvStr)) // 发送数据
}
}
func main() {
listen, err := net.Listen("tcp", "127.0.0.1:20000")
if err != nil {
fmt.Println("listen failed, err:", err)
return
}
for {
conn, err := listen.Accept() // 建立连接
if err != nil {
fmt.Println("accept failed, err:", err)
continue
}
go process(conn) // 启动一个goroutine处理连接
}
}
将上面的代码保存之后编译成 server
或 server.exe
可执行文件。
TCP客户端
一个TCP客户端进行TCP通信的流程如下:
- 建立与服务端的链接
- 进行数据收发
- 关闭链接
使用Go语言的net包实现的TCP客户端代码如下:
// tcp/client/main.go
// 客户端
func main() {
conn, err := net.Dial("tcp", "127.0.0.1:20000")
if err != nil {
fmt.Println("err :", err)
return
}
defer conn.Close() // 关闭连接
inputReader := bufio.NewReader(os.Stdin)
for {
input, _ := inputReader.ReadString('\n') // 读取用户输入
inputInfo := strings.Trim(input, "\r\n")
if strings.ToUpper(inputInfo) == "Q" { // 如果输入q就退出
return
}
_, err = conn.Write([]byte(inputInfo)) // 发送数据
if err != nil {
return
}
buf := [512]byte{}
n, err := conn.Read(buf[:])
if err != nil {
fmt.Println("recv failed, err:", err)
return
}
fmt.Println(string(buf[:n]))
}
}
将上面的代码编译成 client
或 client.exe
可执行文件,先启动server端再启动client端,在client端输入任意内容回车之后就能够在server端看到client端发送的数据,从而实现TCP通信。
TCP黏包
示例
服务端代码如下:
// socket_stick/server/main.go
func process(conn net.Conn) {
defer conn.Close()
reader := bufio.NewReader(conn)
var buf [1024]byte
for {
n, err := reader.Read(buf[:])
if err == io.EOF {
break
}
if err != nil {
fmt.Println("read from client failed, err:", err)
break
}
recvStr := string(buf[:n])
fmt.Println("收到client发来的数据:", recvStr)
}
}
func main() {
listen, err := net.Listen("tcp", "127.0.0.1:30000")
if err != nil {
fmt.Println("listen failed, err:", err)
return
}
defer listen.Close()
for {
conn, err := listen.Accept()
if err != nil {
fmt.Println("accept failed, err:", err)
continue
}
go process(conn)
}
}
客户端代码如下:
// socket_stick/client/main.go
func main() {
conn, err := net.Dial("tcp", "127.0.0.1:30000")
if err != nil {
fmt.Println("dial failed, err", err)
return
}
defer conn.Close()
for i := 0; i < 20; i++ {
msg := `Hello, Hello. How are you?`
conn.Write([]byte(msg))
}
}
将上面的代码保存后,分别编译。先启动服务端再启动客户端,可以看到服务端输出结果如下:
收到client发来的数据: Hello, Hello. How are you?Hello, Hello. How are you?Hello, Hello. How are you?Hello, Hello. How are you?Hello, Hello. How are you?
收到client发来的数据: Hello, Hello. How are you?Hello, Hello. How are you?Hello, Hello. How are you?Hello, Hello. How are you?Hello, Hello. How are you?Hello, Hello. How are you?Hello, Hello. How are you?Hello, Hello. How are you?
收到client发来的数据: Hello, Hello. How are you?Hello, Hello. How are you?
收到client发来的数据: Hello, Hello. How are you?Hello, Hello. How are you?Hello, Hello. How are you?
收到client发来的数据: Hello, Hello. How are you?Hello, Hello. How are you?
客户端分10次发送的数据,在服务端并没有成功的输出10次,而是多条数据“粘”到了一起。
为什么会出现粘包
主要原因就是tcp数据传递模式是流模式,在保持长连接的时候可以进行多次的收和发。
“粘包”可发生在发送端也可发生在接收端:
- 由Nagle算法造成的发送端的粘包:Nagle算法是一种改善网络传输效率的算法。简单来说就是当我们提交一段数据给TCP发送时,TCP并不立刻发送此段数据,而是等待一小段时间看看在等待期间是否还有要发送的数据,若有则会一次把这两段数据发送出去。
- 接收端接收不及时造成的接收端粘包:TCP会把接收到的数据存在自己的缓冲区中,然后通知应用层取数据。当应用层由于某些原因不能及时的把TCP的数据取出来,就会造成TCP缓冲区中存放了几段数据。
解决办法
出现”粘包”的关键在于接收方不确定将要传输的数据包的大小,因此我们可以对数据包进行封包和拆包的操作。
封包:封包就是给一段数据加上包头,这样一来数据包就分为包头和包体两部分内容了(过滤非法包时封包会加入”包尾”内容)。包头部分的长度是固定的,并且它存储了包体的长度,根据包头长度固定以及包头中含有包体长度的变量就能正确的拆分出一个完整的数据包。
我们可以自己定义一个协议,比如数据包的前4个字节为包头,里面存储的是发送的数据的长度。
// socket_stick/proto/proto.go
package proto
import (
"bufio"
"bytes"
"encoding/binary"
)
// Encode 将消息编码
func Encode(message string) ([]byte, error) {
// 读取消息的长度,转换成int32类型(占4个字节)
var length = int32(len(message))
var pkg = new(bytes.Buffer)
// 写入消息头
err := binary.Write(pkg, binary.LittleEndian, length)
if err != nil {
return nil, err
}
// 写入消息实体
err = binary.Write(pkg, binary.LittleEndian, []byte(message))
if err != nil {
return nil, err
}
return pkg.Bytes(), nil
}
// Decode 解码消息
func Decode(reader *bufio.Reader) (string, error) {
// 读取消息的长度
lengthByte, _ := reader.Peek(4) // 读取前4个字节的数据
lengthBuff := bytes.NewBuffer(lengthByte)
var length int32
err := binary.Read(lengthBuff, binary.LittleEndian, &length)
if err != nil {
return "", err
}
// Buffered返回缓冲中现有的可读取的字节数。
if int32(reader.Buffered()) < length+4 {
return "", err
}
// 读取真正的消息数据
pack := make([]byte, int(4+length))
_, err = reader.Read(pack)
if err != nil {
return "", err
}
return string(pack[4:]), nil
}
接下来在服务端和客户端分别使用上面定义的proto
包的Decode
和Encode
函数处理数据。
服务端代码如下:
// socket_stick/server2/main.go
func process(conn net.Conn) {
defer conn.Close()
reader := bufio.NewReader(conn)
for {
msg, err := proto.Decode(reader)
if err == io.EOF {
return
}
if err != nil {
fmt.Println("decode msg failed, err:", err)
return
}
fmt.Println("收到client发来的数据:", msg)
}
}
func main() {
listen, err := net.Listen("tcp", "127.0.0.1:30000")
if err != nil {
fmt.Println("listen failed, err:", err)
return
}
defer listen.Close()
for {
conn, err := listen.Accept()
if err != nil {
fmt.Println("accept failed, err:", err)
continue
}
go process(conn)
}
}
客户端代码如下:
// socket_stick/client2/main.go
func main() {
conn, err := net.Dial("tcp", "127.0.0.1:30000")
if err != nil {
fmt.Println("dial failed, err", err)
return
}
defer conn.Close()
for i := 0; i < 20; i++ {
msg := `Hello, Hello. How are you?`
data, err := proto.Encode(msg)
if err != nil {
fmt.Println("encode msg failed, err:", err)
return
}
conn.Write(data)
}
}
Go语言实现UDP通信
UDP协议
UDP协议(User Datagram Protocol)中文名称是用户数据报协议,是OSI(Open System Interconnection,开放式系统互联)参考模型中一种无连接的传输层协议,不需要建立连接就能直接进行数据发送和接收,属于不可靠的、没有时序的通信,但是UDP协议的实时性比较好,通常用于视频直播相关领域。
UDP服务端
使用Go语言的net
包实现的UDP服务端代码如下:
// UDP/server/main.go
// UDP server端
func main() {
listen, err := net.ListenUDP("udp", &net.UDPAddr{
IP: net.IPv4(0, 0, 0, 0),
Port: 30000,
})
if err != nil {
fmt.Println("listen failed, err:", err)
return
}
defer listen.Close()
for {
var data [1024]byte
n, addr, err := listen.ReadFromUDP(data[:]) // 接收数据
if err != nil {
fmt.Println("read udp failed, err:", err)
continue
}
fmt.Printf("data:%v addr:%v count:%v\n", string(data[:n]), addr, n)
_, err = listen.WriteToUDP(data[:n], addr) // 发送数据
if err != nil {
fmt.Println("write to udp failed, err:", err)
continue
}
}
}
UDP客户端
使用Go语言的net
包实现的UDP客户端代码如下:
// UDP 客户端
func main() {
socket, err := net.DialUDP("udp", nil, &net.UDPAddr{
IP: net.IPv4(0, 0, 0, 0),
Port: 30000,
})
if err != nil {
fmt.Println("连接服务端失败,err:", err)
return
}
defer socket.Close()
sendData := []byte("Hello server")
_, err = socket.Write(sendData) // 发送数据
if err != nil {
fmt.Println("发送数据失败,err:", err)
return
}
data := make([]byte, 4096)
n, remoteAddr, err := socket.ReadFromUDP(data) // 接收数据
if err != nil {
fmt.Println("接收数据失败,err:", err)
return
}
fmt.Printf("recv:%v addr:%v count:%v\n", string(data[:n]), remoteAddr, n)
}