@dungan
2022-11-29T09:26:54.000000Z
字数 43676
阅读 543
Go
包导入
import (
// 导入内置包
"fmt"
"log"
"net/http"
"time"
// 导入当前项目下的包
"http/sessionManager"
"http/sessionProvider"
//匿名导入
_ "packageA" //导入不使用
//内嵌导入
. "packageB" //类似php的trait,内嵌一个包的方法到当前包中
//别名导入
"crypto/rand"
mrand "math/rand" // 将名称替换为mrand避免冲突
)
空白标识符 _
_ 用于忽略某个变量或返回值,或者只是执行导入包的init()函数; Go 语言要求所有变量或导入的包都必须被使用,如果你不想使用,这时候就可以使用空白标识符 _ 来忽略。
init 函数
一个包中可以有个名为 init 的函数来负责包的初始化, 相当于构造方法construct,它没有返回值,init 函数会在 main 函数之前执行, 如果导入了其他包,则先执行其他包的init函数,注意:如果同一个包内的多个 init 顺序是不受保证的。
func init() {
fmt.Println("execute before than main")
}
bool、string、complex64、complex128、array
int, uint,int8、uint8(byte)、int16、uint16、int32(rune)、uint32、int64、uint64
float32、float64
slice
map
chan
使用 type 关键字,基于一个现有的类型创造新的类型称之为自定义类型
或者 类型定义
。
type Fuck int64
type T struct
Type I interface
关键字
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
函数
append 用来追加元素到数组、slice中,返回修改后的数组、slice
close 主要用来关闭channel
delete 从map中删除key对应的value
panic 停止常规的goroutine (panic和recover:用来做错误处理)
recover 允许程序定义goroutine的panic动作
real 返回complex的实部 (complex、real imag:用于创建和操作复数)
imag 返回complex的虚部
make 用来分配内存,返回Type本身(只能应用于slice, map, channel)
new 用来分配内存,主要用来分配值类型,比如int、struct。返回指向Type的指针
cap capacity是容量的意思,用于返回某个类型的最大容量(只能用于切片和 map)
copy 用于复制和连接slice,返回复制的数目
len 获取长度,比如string、array、slice、map、channel ,返回长度
print、println 底层打印函数,在部署环境中建议使用 fmt 包
全局变量与局部变量
- 全局变量:在函数体外声明的变量称之为全局变量,全局变量可以在整个包甚至外部包(被导出后)使用。
- 局部变量:在函数体内声明的变量称之为局部变量,它们的作用域只在函数体内,参数和返回值变量也是局部变量
大多数变量的默认值是其类型的零值。nil 是 interface、function、pointer、map、slice 和 channel 类型变量的默认初始值。
一、一行声明一个变量
var name sting
var title sting = "Python编程时光"
二、多个变量一起声明
var (
name string
age int
gender string
)
三、短变量申明(函数特有)
name := "Python编程时光"
四、声明和初始化多个变量
name, age := "wangbm", 28
常量使用 const 关键字申明,注意:常量不能用 := 语法声明。
const (
A = "A"
B = 1
)
const C = true
iota是常量的计数器,从0开始,组中每定义1个常量自动递增1,每遇到一个const关键字,iota就会重置为0。
package main
import (
"fmt"
)
func main() {
const (
A = "A"
B
C = iota
D
)
const E = iota
fmt.Println(A, B, C, D, E) // A A 2 3 0
}
类型转换时一般需要显式的使用某个类型的表达式进行转换,例如表达式 T(v) 将值 v 转换为类型 T。
package main
import (
"fmt"
"reflect"
"strconv"
)
func main() {
//同类型转换,只能在两种相互兼容的类型之间
f := 3.15
int_f := int(f) //浮点变整形
fmt.Println(int_f)
bool := true
fmt.Println(int(bool)) //报错,整型无法和bool类型兼容
//int 和 string 转换
num := 3
str := strconv.Itoa(num) // 字符串 3
num, _ = strconv.Atoi(str) // int 3
fmt.Println(reflect.TypeOf(str), str, num)
//ascii 转换
zs := 65
asc := string(zs)
fmt.Println(asc) // A
//浮点型精度处理
a := 1690 // 表示1.69
b := 1700 // 表示1.70
c := a * b // 结果应该是2873000表示 2.873
fmt.Println(c) // 2873000
fmt.Println(float64(c) / 1000000) // 2.873
}
不仅可以使用内置类型进行转换,我们自定义的类型也可以进行转换
package main
import (
"fmt"
)
type tcl int
func main() {
num := 456
p := (*tcl)(&num)
val, isTcl := interface{}(p).(*tcl)
fmt.Println(*val, isTcl) // 456, true
}
import "fmt"
func main() {
// 单个字符的类型可以申明为 byte
var a byte = 'A'
var b uint8 = 'B'
fmt.Printf("a 的值: %c \nb 的值: %c", a, b)
}
由于单个 byte 代表一个ACSII字符,那多个 byte 放在一起就组成了字符串,也就是 string 类型(一堆byte组成了字符串,即 byte[]);使用 byte[]来进行默认字符串处理 ,性能和扩展性都有照顾。
func main() {
// string 转 byte数组
byteAB := []byte("AB") // [97 98]
// byte数组 转 string
stringAB := string(byteAB) // AB
fmt.Println(byteAB, stringAB)
}
要修改字符串,需要先将其转换成[]rune或[]byte,完成后再转换为string。因为一个 UTF8 编码的字符可能会占多个字节,因此更推荐使用 []rune 来处理字符。
s1 := "hello"
//转换为字节数组
byteS1 := []byte(s1)
byteS1[0] = 'H'
fmt.Println(string(byteS1))
s2 := "博客"
//转换为字节数组
runeS2 := []rune(s2)
runeS2[0] = '狗'
fmt.Println(string(runeS2))
go 函数可以接受多个参数,并且在函数名之后可以指出返回的类型。
package main
import "fmt"
func add(x int, y int) int {
return x + y
}
func main() {
fmt.Println(add(42, 13))
}
如果参数数量不确定,可以通过 ...type
来定义不定参数函数。
... type
只能是位于函数参数最后,如果你希望函数的参数传任意类型,可以指定类型为 interface{}。
package main
import "fmt"
func myFunc(args ...int) {
// 使用for循环获取每个输入的参数
for _, arg := range args {
fmt.Println(arg)
}
//在内部转发参数
myFunc2(args ...)
}
func myFunc2(args ...int){
fmt.Println(args)
}
func main() {
myFunc(2, 3, 4)
}
如果函数体中的变量名和返回值的变量名一样,你无需显式的 return 这个变量。
func (manager *Manager) SessionStart() (session Session) {
...
//生成 session
session, _ = manager.provider.SessionInit(sid)
...
return
}
函数还可以有多个返回值。
package main
import "fmt"
func swap(x, y string) (string, string) {
return y, x
}
func main() {
a, b := swap("hello", "world")
fmt.Println(a, b)
}
nil 是任意类型的0值,当一个函数你没有要求可返回的类型,那么你就返回 nil。
func (provider MemoryDriver) SessionRead(sid string) (sessionManager.Session, error) {
...
return nil, nil
}
函数可以用作另一函数的参数,也就是所谓的回调函数。
package main
import (
"fmt"
"math"
)
func compute(x,y float64,fn func(float64, float64) float64) float64 {
return fn(x, y)
}
func main() {
hypot := func(x, y float64) float64 {
return math.Sqrt(x*x + y*y)
}
fmt.Println(compute(3, 4, hypot))
}
函数的返回值类型如果是函数,这时候就形成了闭包
。
package main
import (
"fmt"
)
//将 【func(int) int】 看作一个整体,意为 a() 函数的返回值类型为函数
func a() (func(n int) int) {
// 这里的func 没有函数名,我们称之为匿名函数
return func(n int) int {
return n+1
}
}
func main() {
b := a()
fmt.Println(b(5)) // 6
}
对于大多数函数,如果要返回错误,将 error 作为多个返回值中的最后一个。
func ListPosts(...) ([]Post, error) {
conn, err := gorm.Open(...)
if err != nil {
return []Post{}, err
}
var posts []Post
if err := conn.Find(&posts).Error; err != nil {
return []Post{}, err
}
return posts, nil
}
当然,我们还可以生成自定义 error 信息。
var ErrTimeOut = errors.New("执行者执行超时")
var ErrInterrupt = errors.New("执行者被中断")
var fmtError = fmt.Errorf("借助fmt生成error信息")
//函数调用判断是否为error
...
err := r.run()
if(err != nil){
// 出错了
return fmtError
}
defer 会在当前函数返回前执行传入的函数,它会经常被用于关闭文件描述符、关闭数据库连接以及解锁资源
defer 函数会按照后进先出的顺序调用(最早定义的最后执行)。
package main
import "fmt"
func main() {
fmt.Println("counting") // 立即输出
// 最先输出的是9,符合最早定义,最后执行的特点
for i := 0; i < 10; i++ {
defer fmt.Println(i)
}
fmt.Println("done") // 立即输出
}
外层函数中的 return 语句会等到 defer 函数执行完毕后才会返回。
package main
import "fmt"
func main() {
fmt.Println(testDefer()) // 2
}
func testDefer() (n int){
i := 1
// 这里通过匿名的自执行函数来求值
defer func(i int) {
n = i + 1
}(i)
return n
}
在 panic 抛出异常之前,会先处理完当前协程上已经defer 的任务,执行完成后再抛出。
func main() {
defer func() {
fmt.Println("defer func")
}()
arr := []int{1, 2, 3}
fmt.Println(arr[4])
}
这两个函数是go的异常处理函数,panic() 相当于抛出异常,recover() 相当于捕获异常。
- panic : 用来抛出错误,该函数接收任意类型的数据,比如整型,字符串,对象等,panic 退出前会执行 defer 指定的内容。
- recover : 用来捕获由 panic 函数抛出的错误,recover 函数应放置于函数体的最前面,并且只有在 defer 调用的函数中有效。
package main
import (
"errors"
"fmt"
)
func main() {
err := doSomeThing()
if err != nil {
fmt.Println(err)
} else {
fmt.Println("success")
}
}
func doSomeThing() (err error) {
defer func() {
if r := recover(); r != nil {
//通过类型断言,将捕获的错误值转换为 string 类型,并最终将捕获的错误转换成 error类型返回给调用者
str , ok := r.(string)
if ok {
err = errors.New(str)
} else {
panic("new error")
}
}
}()
panic("some error")
return err
}
go 只有 for 这一种循环结构,for 是 Go 中的 "while"。需要注意 for,if,switch 都具有块级作用域,即在语句外部无法在语句中定义的变量。
package main
import "fmt"
func main() {
// for循环
sum := 0
for i := 0; i < 10; i++ {
sum += i
}
fmt.Println(sum)
fmt.Println(i) //报错 undefined: i
// while 循环
sum := 1
for sum < 1000 {
sum += sum
}
fmt.Println(sum)
}
可以使用 for range 对数组,切片,map,通道
等迭代。
package main
import (
"fmt"
)
func main() {
slice := []int{1, 2, 3, 4, 5}
// 普通遍历
for i, v := range slice {
fmt.Println(i , v)
}
// 如果不想要索引,可以使用_来忽略它
for _, v := range slice {
fmt.Println(v)
}
// 如果你用一个变量来接收的话,接收到的是索引
for i := range slice {
fmt.Println(i)
}
//range也可以用在map的键值对上。
kvs := map[string]string{"a": "apple", "b": "banana"}
for k, v := range kvs {
fmt.Printf("%s -> %s\n", k, v)
}
//range还可以用来枚举字符串。
for i2, c := range "go" {
fmt.Println(i2, string(c)) //得出的c是ASCII值
}
}
if 语句与 for 循环类似,表达式外无需小括号 ( ) ,而大括号 { } 则是必须的。
func main() {
a := 10
if a := 1; a > 0 {
a++
fmt.Println(a) // 2
} else if a < 0 {
a--
fmt.Println(a)
}
fmt.Println(a) // 10,说明if的a只是在局部有用
}
不同于其他语言中的switch语句,go 中一旦条件符合则会自动终止,不需要 break,而如果你希望继续执行下一个case,需使用 fallthrough 语句。
G另一点重要的不同在于 switch 的 case 无需为常量,且取值不必为整数。
package main
import (
"fmt"
"runtime"
"time"
)
func main() {
fmt.Print("Go runs on ")
os := runtime.GOOS
switch os {
case "darwin":
fmt.Println("OS X.")
case "linux":
fmt.Println("Linux.")
default:
fmt.Printf("%s.\n", os)
}
// fallthrough 语句
switch a := 200; {
case a == 100:
fmt.Println("a=100")
case a == 200:
fmt.Println("a=200")
fallthrough // 不退出,进入下个case语句
default:
fmt.Println("a=" + strconv.Itoa(a+a))
}
// 没有没有条件的 switch 能将一长串 if-then-else 写得更加清晰
t := time.Now()
switch {
case t.Hour() < 12:
fmt.Println("Good morning!")
case t.Hour() < 17:
fmt.Println("Good afternoon.")
default:
fmt.Println("Good evening.")
}
}
Go中支持空block{},这个大括号有自己的作用域,里面的代码只执行一次,退出大括号就退出作用域。
func main() {
{
v := 1
{
v := 2
fmt.Println(v) // 输出2
}
fmt.Println(v) // 输出1
}
}
Go语言中的指针操作非常简单,只需要记住两个符号:&(取地址)
和 *(根据地址取值)
,取地址操作符&和取值操作符*是一对互补操作符,&取出地址,*根据地址取出地址指向的值。
package main
import (
"fmt"
)
func main() {
//================= 指向变量的指针 =====================
var num = 3
var p *int = &num // 指针类型的变量,值必须是用 & 标识的内存地址
var np *int = 3 // 报错, cannot use 3 (type int) as type *int in assignment
fmt.Println(&num) //0xc0420080c8
fmt.Println(p); //0xc0420080c8,p和num指向了同一个内存地址
fmt.Println(*p) //3
//=============== 指向指针的指针 ======================
var pp **int
pp = &p
fmt.Println(pp); //0xc042004028,获取指针 p 的内存地址,而指针 p 的值存的是它指向的内存地址
fmt.Println(*pp); //0xc0420080c8,获取指针p 指向的内存地址
fmt.Println(**pp) //3 ,** 获取指针p 指向的内存地址的值
}
类型 [n]T 表示拥有 n 个 T 类型的值的数组,数组用作函数参数是值传递。
package main
import "fmt"
func main() {
var a [2]string
a[0] = "Hello"
a[1] = "World"
fmt.Println(a[0], a[1])
fmt.Println(a)
// 注意:{} 中的元素个数不能大于 [] 中的数字
primes := [6]int{2, 3, 5, 7, 11, 13}
fmt.Println(primes)
}
切片是对数组一个连续片段的引用,没有设置元素个数的数组我们称之为切片, []T 表示一个元素类型为 T 的切片,切片用作函数参数是引用传递,切片比数组更常用,Go 中的大部分数组编程都是通过切片来完成的。
切片的零值是 nil,nil 切片的长度和容量为 0 且没有底层数组
package main
import "fmt"
func main() {
var s []int
fmt.Println(s, len(s), cap(s))
if s == nil {
fmt.Println("nil!")
}
}
冒号分隔后的切片或数组,包括第一个元素,但排除最后一个元素。
package main
import "fmt"
func main() {
primes := [6]int{2, 3, 5, 7, 11, 13}
var s []int = primes[1:4]
fmt.Println(s) // [3 5 7]
}
切片是对数组的引用,切片并不存储任何数据,它只是描述了底层数组中的一段,更改切片的元素会修改其底层数组中对应的元素。
package main
import "fmt"
func main() {
names := [4]string{
"John",
"Paul",
"George",
"Ringo",
}
fmt.Println(names) // [John Paul George Ringo]
b := names[1:3]
b[0] = "XXX"
fmt.Println(names) // [John XXX George Ringo]
}
相关函数
- len() : 获取切片的元素个数。
- cap() : 获取切片的容量。
- make([]T, len, cap) : 创建切片,len 是数组的元素个数,cap 是数组的容量,如果cap 省略,则其值和 len 相同。
- copy(dest, from) :复制切片,会在底层创建一个匿名的数组,这样就切断了与原切片间的引用关系,实现的效果是会覆盖相同索引处的元素,如果没有对应的索引,dest不变。
- append() : 向数组/切片追加元素。
切片截取
package main
import "fmt"
func main() {
a := make([]int, 5)
printSlice("a", a)
b := make([]int, 0, 5)
printSlice("b", b)
c := b[:2]
printSlice("c", c)
d := c[2:5]
printSlice("d", d)
}
func printSlice(s string, x []int) {
fmt.Printf("%s len=%d cap=%d %v\n",
s, len(x), cap(x), x)
}
使用 append 追加元素
package main
import (
"fmt"
)
func main() {
s := []int {}
s2 := []int {1,2,3}
s = append(s, 4,5,6) // 添加元素到切片
s2= append(s2, s...) // 通过...操作符合并切片
fmt.Println(s, s2) // [4 5 6] [1 2 3 4 5 6]
}
使用 copy 复制切片
package main
import (
"fmt"
)
func main() {
s := []int{1}
arr := []int{4, 5, 6, 7, 8, 9}
copy(arr, s)
fmt.Println(arr, s) //[1 5 6 7 8 9] [1],arr和s有相同的索引(index=0),所以index=0处的4被覆盖成了1
s[0] = 0
fmt.Println(arr, s) //[1 5 6 7 8 9] [0], 切断了引用关系,即使修改了源切片也不会影响copy生成的切片
//复制整个切片
arr2 := []int{1, 2, 3, 4, 5}
arr3 := make([]int, len(arr2))
copy(arr3, arr2)
arr3[0] = 0
fmt.Println(arr2, arr3) // [1 2 3 4 5] [0 2 3 4 5]
}
字符串的底层就是一个 byte 的数组,因此,也可以进行切片操作
package main
import (
"fmt"
"strings"
)
func main() {
host := "www.baidu.com"
pdIndex := strings.LastIndex(host, ".")
fmt.Println(host[pdIndex:]) //.com
s := []byte(host)
s[0] = 'W'
fmt.Println(string(s)) //Www.baidu.com
}
map 其实就是key-value形式的数据结构,在别的语言也叫hash表,字典或关联数组。
申明但未初始化的 Map 零值是 nil,既没有键,也不能添加键。
package main
import (
"fmt"
)
func main(){
var m1 map[string]string
fmt.Println(m1, m1 == nil) // map[] true
}
可以在申明时给 Map 赋值或者通过 make 函数来初始化 Map,当然可以用任意类型初始化 Map,例如 struct。
package main
import (
"fmt"
)
type User struct {
Name string
Age int
}
func main(){
m2 := map[string]string{"a":"a"}
m3 := make(map[string]string)
// 结构体作为 value
m4 := map[string]User{
"zhangsan": { "张三", 18},
"lisi": {"李四", 19},
}
fmt.Println(m2, m3) // map[a:a] map[]
fmt.Println(m4) // map[lisi:{李四 19} zhangsan:{张三 18}]
}
对 Map 进行增删查改。
package main
import "fmt"
func main() {
m := make(map[string]int)
// 插入元素
m["Answer"] = 42
fmt.Println("The value:", m["Answer"])
// 修改元素
m["Answer"] = 48
fmt.Println("The value:", m["Answer"])
// 元素数量
fmt.Println("The count:", len(m))
// 删除元素
delete(m, "Answer")
fmt.Println("The value:", m["Answer"])
// 判断某个key是否存在
// 若 key 在 m 中,isset 为 true,否则 isset 为 false。
// 若 key 不在映射中,那么 v 是该映射元素类型的零值。
v, isset := m["Answer"]
if(isset) {
fmt.Println("v value is", v)
}else{
fmt.Println("v not found ", v)
}
}
对 Map 进行迭代时,它是随机输出元素的,如果你想顺序迭代,可以先对Map中的键排序,然后遍历排好序的键,把对应的值取出来。
package main
import (
"fmt"
"sort"
)
func main() {
m := map[string]string{"a": "A", "b": "B", "c": "C", "d": "D"}
//===================随机取值=================
for k, v := range m {
fmt.Println(k, "=", v) // C D A b
}
//================= 顺序取值 ==================
var keys []string
for k := range m {
keys = append(keys, k)
}
//对key进行排序
sort.Strings(keys)
for _, k := range keys {
fmt.Println(k, "=", m[k]) // A b C D
}
}
new
import "fmt"
type Student struct {
name string
age int
}
func main() {
// new 一个内建类型
var a *int
a = new(int)
*a = 10
fmt.Println(*a) //10
// new 一个自定义类型
s := new(Student)
s.name = "bob"
}
make
package main
import "fmt"
func main() {
var b = make(map[string]string)
b["name"] = "bob"
fmt.Println(b)
}
go 没有面向对象的概念,面向对象中我们使用
class
关键字来申明一个类,而在 go 中我们可以通过type
关键词来申明一个类型。可以将类型看作是面向对象中的对象(相当于申明了一个类)。
- 自定义类型:自定义类型是基于内置的基本类型,使用 type 关键字定义的一种全新类型,和原类型是不同的两个类型。
- 类型别名:类型别名和原类型完全一样,只不过是另一种叫法而已。
package main
type D = int // 类型别名,有等号
type I int // 自定义类型,无等号
func main() {
v := 100
var d D = v // 不报错
var i I = v // 报错
var K I = I(v) // 转换成 I 类型后就不报错了
}
1.自定义类型
有这么一个闭包函数,参数类型和返回值类型都是函数,对于这样的代码其实是不好读的。
func getValue(func(n int) int) func(n int) int {
return func(n int) int {
return c(n) + 1
}
}
我们可以借助自定义类型重构如下,这样一来这个函数就比较可读了。
type IntCompute func(n int) int
func getValue(c IntCompute) IntCompute {
return func(n int) int {
return c(n) + 1
}
}
2.类型别名
类型别名还可以为其它包中的类型定义别名,只要这个类型在其它包中是导出的:
package main
import (
"fmt"
"time"
)
type MyTime = time.Time
func main() {
var t MyTime = time.Now()
fmt.Println(t)
}
如果类型别名导出是的,那么别的包中就可以使用,它和原始类型是否是导出没关系:
type t1 struct {
S string
}
type T2 = t1
在switch中,你不能将原类型和类型别名作为两个分支,因为这是重复的case:
package main
import "fmt"
type D = int
func main() {
var v interface{}
var d D = 100
v = d
switch i := v.(type) {
case int:
fmt.Println("it is an int:", i)
//报错: duplicate case int in type switch
case D:
fmt.Println("it is D type:", i)
}
}
既然类型别名和原始类型是相同的,那么它们的方法集也是相同的:
package main
type T1 struct{}
type T3 = T1
func (t1 T1) say() {}
func (t3 *T3) greeting() {}
func main() {
var t1 T1
var t3 T3
t1.say()
t1.greeting()
t3.say()
t3.greeting()
}
类型别名建立的类型只能为本地类型添加方法,不能为内置类型添加方法:
package main
import (
"fmt"
"time"
)
type NTime = time.Time
// 报错 :cannot define new methods on non-local type time.Time
func (t NTime) Dida() {
fmt.Println("嘀嗒嘀嗒嘀嗒嘀嗒搜索")
}
func main() {
t := time.Now()
t.Dida()
}
package main
import "fmt"
//对象定义
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 (p *person) SetAge(newAge int8) {
p.age = newAge
}
func main() {
p := newPerson("bob", "beijing", 20)
p.SetAge(18)
fmt.Println(p.name, (*p).age)
}
Go语言中的基础数据类型可以表示一些事物的基本属性,但是当我们想表达一个事物的全部或部分属性时,这时候再用单一的基本数据类型明显就无法满足需求了,Go语言提供了一种自定义数据类型,可以封装多个基本数据类型,这种数据类型叫结构体(struct),通过 struct 可以实现面向对象。
package main
import (
"fmt"
)
func main() {
//============================结构体定义=========================
type T_simple struct {
id int
name string
phone string
age int
}
//基本的实例化
var t T_simple
t.id = 10
t.name = "tcl"
fmt.Println(t) //{10 tcl 0}
//new 函数实例化
new_simple = new(T_simple) //类似其他语言 new 对象一样
t.id = 10
t.name = "tcl"
fmt.Println(t) //{10 tcl 0}
//键值对方式实例化
t_simple := T_simple{ id:11, name:"tcl"}
fmt.Println(t_simple) // {11 tcl 0}
//定义并初始化
t_default := struct {
Name string
Age int
}{
"tcl", 28,
}
fmt.Println(t_default) // tcl 28
}
结构体中字段大写开头表示可公开访问,小写表示私有(仅在定义当前结构体的包中可访问),结构体成员使用 .
访问。
package main
import "fmt"
type Vertex struct {
X int
Y int // 公开的
z int // 私有的
}
func main() {
v := Vertex{1, 2}
v.X = 4
fmt.Println(v.X) // 4
}
指向结构体的指针 p,可以通过 (*p).X 来访问其字段 X,也可以通过 p.X 访问。p.X 的底层其实就是 (*p).X,这是go为了方便结构体使用帮我们实现的语法糖。
package main
import "fmt"
type Vertex struct {
X int
Y int
}
func main() {
v := Vertex{1, 2}
p := &v
(*p).X = 10
p.Y = 20
fmt.Println(v) // {10 20}
}
方法只是个带接收者
参数的函数,接收者位于 func 关键字和方法名之间,接收者只能是通过 type
关键词申明的类型。接收者的类型定义和方法声明必须在同一包内。
package main
import (
"fmt"
)
type Rect struct {
width float64
height float64
}
// Area 方法拥有一个名为 r,类型为 Rect 的接收者
func (r *Rect) Area() float64 {
return r.width * r.height
}
//在Go语言中没有构造函数的概念,通常以 NewXXX 命名的函数来表示【构造函数】
func NewRect(width, height float64) *Rect {
return &Rect{width, height}
}
func main() {
r := NewRect(3,4)
fmt.Println(r.Area()) // 12
}
如果结构体比较复杂的话,值拷贝性能开销会比较大,因此构造函数推荐返回结构体的指针类型。
方法调用时没有严格要求接收者的类型,即方法的接收者为指针类型的话,也可以通过值类型来调用该方法,反之也是这样。
一般来说指针接收者比值接收者更常用:
- 方法经常需要修改它的接收者指向的值。
- 可以避免在每次调用方法时复制该值,若值的类型为大型结构体时,这样做会更加高效。
package main
import (
"fmt"
)
type person struct {
name string
}
// 接收者为值
func (p person) Call(){
fmt.Println("value call")
}
// 接收者为指针
func (p *person) Call2(){
fmt.Println("point call")
}
func main() {
p := person{}
p.Call() // value call
(&p).Call() // value call
p.Call2() // point call
(&p).Call2() // point call
}
一个结构体中可以嵌套包含另一个结构体或结构体指针,我们称为结构体嵌套
,这样就能将一个有简单行为的对象组合成有复杂行为的对像。
结构体还以可以包含一个或多个匿名结构体
,即结构体没有字段名只有类型。匿名结构体其实仍然拥有自己的字段名,只是字段名就是其类型本身而己
。
结构体嵌套
package main
import "fmt"
//Address 地址结构体
type Address struct {
Province string
City string
}
//User 用户结构体
type User struct {
Name string
Gender string
Address Address
}
func main() {
user := User{
Name: "bob",
Gender: "man",
Address: Address{
Province: "广东",
City: "深圳",
},
}
fmt.Printf("user=%#v\n", user)
}
匿名结构体嵌套
嵌套多个匿名机结构体时,要求字段名称必须唯一,如果有同名字段会有冲突,如果结构体内部有同名字段,就不要使用匿名结构体嵌套了。
package main
import "fmt"
//Address 地址结构体
type Address struct {
Province string
City string
}
//User 用户结构体
type User struct {
Name string
Gender string
Address //这里的
}
func main() {
user := User{
Name: "bob",
Gender: "man",
Address: Address{
Province: "广东",
City: "深圳",
},
}
// 通过匿名结构体.字段名访问
fmt.Println(user.Address.City)
// 直接访问匿名结构体的字段名
fmt.Println(user.Province)
}
结构体标签是它是一个附属于字段的字符串,存储了字段的元数据(如字段映射,数据校验,对象关系映射等等)。
最常用的一个场景就是 json 的编解码,标签名会以结构体字段别名的方式出现在 json 中:
package main
import (
"encoding/json"
"fmt"
)
// 在编解码时 "omitempty" 会忽略 false,0,空指针,空接口,空数组,空切片,空映射,空字符。
// "-" 会完全跳过该字段。
// 小写的字段也会被忽略。
type User struct{
Name string `json:"name"`
Age int `json:"age"`
Phone string `json:"phone,omitempty"`
Id int `json:"-"`
}
func main() {
h := User{
Name : "tcl",
Age : 27,
Phone: "",
Id : 100,
}
jsonData, err := json.Marshal(h)
if err != nil {
fmt.Println(err)
}
fmt.Println(string(jsonData)) //{"name":"tcl","age":27}
}
字段的标签可以有多个键值对,多个键值对使用空格分开:
type User struct{
Name string `json:"name"`
Age int `json:"age"`
Phone string `json:"phone,omitempty"`
Id int `json:"-"`
Area string `province:"广东" city:"shenzhen"`
}
通过反射可以获取字段的标签信息:
package main
import (
"fmt"
"reflect"
)
type User struct{
Name string `json:"name"`
Age int `json:"age"`
Phone string `json:"phone,omitempty"`
Id int `json:"-"`
Area string `province:"广东" city:"shenzhen"`
}
func main() {
var u User
t:=reflect.TypeOf(u)
// 遍历字段
for i:=0;i<t.NumField();i++{
field:=t.Field(i)
fmt.Println(field.Tag)
}
//获取特定字段的标签信息
area, _ := t.FieldByName("Area")
fmt.Println(area.Tag.Get("province")) //获取键值对中的某个 key
fmt.Println(area.Tag.Lookup("city")) //查找标签中某个 key 是否存在
}
encoding/json 包中的 Marshal() 和 Unmarshal() 方法可以用来对json数据进行编解码, struct、slice、array、map都可以转换成json。
`Marshal()`:Go数据对象 -> json数据
`UnMarshal()`:Json数据 -> Go数据对象
首字母小写的字段是不会被编码输出的(未导出),但如果你就想编码后的字段名是小写的,那么这时候结构体标签(json:tag
)就能帮你实现这种效果。
package main
import (
"encoding/json"
"fmt"
"time"
)
/*
ref 是 Id 字段编解码时的别名
private 字段不会被编码,因为它是小写(未导出的)
*/
type FruitBasket struct {
Name string
Fruit []string
Id int64 `json:"ref"`
private string
Created time.Time
}
func main() {
basket := FruitBasket{
Name: "Standard",
Fruit: []string{"Apple", "Banana", "Orange"},
Id: 999,
private: "Second-rate",
Created: time.Now(),
}
jsonData, err := json.Marshal(basket)
if err != nil {
fmt.Println(err)
}
// json.Marshal 生成数据类型是字节数组([]byte),因此我们需要string()将其转换成字符串
fmt.Println(string(jsonData))
// "Name":"Standard","Fruit":["Apple","Banana","Orange"],"ref":999,"Created":"2019-07-27T15:13:5"}
}
解码时,如果你明确知道待解码字符串的类型,你可以为 json 字符串指定一个解析模板,让其解析为特定的某个对象。
package main
import (
"encoding/json"
"fmt"
"time"
)
type FruitBasket struct {
Name string
Fruit []string
Id int64 `json:"ref"`
private string
Created time.Time
}
func main() {
// json.Unmarshal 需要参数类型为字节数组, 需要把字符串转为 []byte 类型
jsonStr := []byte(`
{
"Name": "Standard",
"Fruit": [
"Apple",
"Banana",
"Orange"
],
"ref": 999
}
`)
// 让 jsonStr 解析为对象 basketStruct
var basketStruct FruitBasket
err := json.Unmarshal(jsonStr, &basketStruct)
if err != nil {
fmt.Println(err)
}
fmt.Println(basketStruct)
}
JSON解析的时候只会解析能找得到的字段,找不到的字段会被忽略,这样的一个好处是:当你接收到一个很大的JSON数据结构而你却只想获取其中的部分数据的时候,你只需将你想要的数据对应的字段名大写,即可轻松解决这个问题。
如果你不知道待解码字符串有哪些字段,可以定义一个 interface{}
类型,interface{} 在 go 表示任意类型,然后利用类型断言特性var.(T)
,把解码结果转换为 map[string]interface{}
类型来进行后续操作。
package main
import (
"encoding/json"
"fmt"
"time"
)
type FruitBasket struct {
Name string
Fruit []string
Id int64 `json:"ref"`
private string
Created time.Time
}
func main() {
jsonStr := []byte(`
{
"Name": "Standard",
"Fruit": [
"Apple",
"Banana",
"Orange"
],
"ref": 999
}
`)
// 申明一个任意类型的对象
var any interface{}
var basketStruct FruitBasket
err := json.Unmarshal(jsonStr, &basketStruct)
err2 := json.Unmarshal(jsonStr, &any)
if err != nil || err2 != nil {
fmt.Println(err)
}
// interface{}()是类型转换的语法,因为类型断言要求变量类型必须接口类型,这里将 basketStruct 转换为接口类型
value1, isBasketStruct := interface{}(basketStruct).(FruitBasket)
fmt.Println(value1, isBasketStruct)
// {Standard [Apple Banana Orange] 999 0001-01-01 00:00:00 +0000 UTC} true
// 使用类型断言特性将结果转换为 map[string]interface{}
value2, isMap := any.(map[string]interface{})
fmt.Println(value2, isMap)
//map[Fruit:[Apple Banana Orange] Name:Standard ref:999] true
}
接口
是由一组方法签名定义的集合,和传统面向对象语言中的接口一样,go 接口中的定义方法 只能是没有方法名的抽象方法,而通过接口中嵌入其它接口,就能实现传统面向对象中的接口继承。
接口也是一种类型,如果一个变量的类型是接口,我们就称该变量为接口型变量,可以用接口型变量保存任何实现了接口方法的对象。
package main
import (
"fmt"
)
//=============== 接口申明 ============================
type Abser interface {
Abs() float64
}
//==============MyFloat 实现了接口 Abser==============
type MyFloat float64
func (f MyFloat) Abs() float64 {
if f < 0 {
return float64(-f)
}
return float64(f)
}
//==============Vertex 实现了接口 Abser ===================
type Vertex struct {
X, Y float64
}
func (v *Vertex) Abs() float64 {
return v.X*v.X + v.Y*v.Y
}
func main() {
//申明接口型变量
var a Abser
var b Abser
// 接口类型的变量可以保存实现该接口的类型
a = &Vertex{3, 4}
b = MyFloat(-0.1)
// 尽管方法名一样,但这两者的方法体行为完全不一样,这就是多态
fmt.Println(a.Abs())
fmt.Println(b.Abs())
}
类型 Vertex
的方法 Abs()
, 它的接收者是指针类型的 *Vertex
,不能把值类型的 Vertex 赋值给接口型变量
,因为值类型的 Vertex 并未实现 Abser 接口。
但如果 Abs() 的接收者为值类型的 Vertex
,那么指针类型的 *Vertex
是可以赋值给接口型变量
。
func main() {
var a Abser
var c Abser
a = &Vertex{3, 4}
c = Vertex{10, 20} //报错:Vertex does not implement Abser (Abs method has pointer receiver)
fmt.Println(a.Abs())
fmt.Println(c.Abs())
}
嵌入的类型如果实现了接口,也视为当前类型实现了接口,即接口的方法,不一定需要由一个类型完全实现。
package main
import (
"fmt"
)
type Servicer interface {
a()
b()
}
type serviceA struct{}
func (sa serviceA) a() {
fmt.Println("a")
}
// serviceB 尽管只实现了接口中的 b() 方法,但由于嵌入了 serviceA,则视 ServiceB 实现了 Service 接口
type serviceB struct {
serviceA
}
func (sb serviceB) b() {
fmt.Println("b")
}
func main() {
sv := serviceB{}
v, isService := interface{}(sv).(Servicer)
fmt.Println(v, isService)
}
接口与接口间可以通过嵌套创造出新的接口。
type Servicer interface {
discover()
Loger
}
type Loger interface {
log(driver interface{})
}
空接口(interface{})是特殊形式的接口类型,普通的接口都有方法,而空接口没有定义任何方法。我们可以说所有类型都至少实现了空接口,任何类型值都可以赋值给空接口变量。因此,空 interface{} 代表任意类型(Any)
。
package main
import (
"fmt"
)
type Any interface {}
// 接口作为函数参数和返回值
func Ascll(anyParam Any) interface{}{
if anyParam == "A" {
return 65
}else if anyParam == 65 {
return "A"
}
// 任何类型值都可以赋值给空接口变量
var anyValue interface{} = map[interface{}]Any{}
return anyValue
}
func main() {
fmt.Println(Ascll("A")) // 65
fmt.Println(Ascll(65)) // A
fmt.Println(Ascll(nil)) // map[]
}
t, ok := obj.(T)
用来判断某个接口型变量
是否属于某个类型并获取它的动态值,类型断言常用于对静态类型为空接口的对象进行断言。
某些情况下要断言的对象未必是接口类型,我们知道接口也是一种类型
,因此可以通过 interface{}(obj)
将其转换为接口型变量。
t, ok := interface{}(obj).(T)
package main
import (
"fmt"
)
type It interface {
Test()
}
type Person struct {
name string
}
func (p Person) Test() {
// todo
}
func main() {
var a It = Person{name:"tcl"}
val,isPerson := a.(Person)
fmt.Println(val, isPerson) // {tcl} true
var b interface{} = nil
val2,isPerson2 := b.(Person)
fmt.Println(val2, isPerson2) // {} false
}
i.(type)
类型选择用在断言多次
的场景,其实就是将多个if判断的断言使用switch语句来实现。有点像别的语言中 get_type() 之类的函数。
类型选择中的声明与类型断言 i.(T) 的语法相同,只是具体类型 T 被替换成了关键字
type
。
package main
import "fmt"
func do(i interface{}) {
switch v := i.(type) {
case int:
fmt.Printf("Twice %v is %v\n", v, v*2)
case string:
fmt.Printf("%q is %v bytes long\n", v, len(v))
default:
fmt.Printf("I don't know about type %T!\n", v)
}
}
func main() {
do(21)
do("hello")
do(true)
}
借助类型断言可以实现类型转换。
if r := recover(); r != nil {
//通过类型断言,将捕获的错误值 r 转换为 string 类型,
//然后传入到 errors.New 中返回 error 类型的消息
str , ok := r.(string)
if ok {
err = errors.New(str)
}
}
空接口可以代表任意类型,借助类型断言的特性,我们可以将任何空接口值转换为我们需要的类型。
package main
import (
"fmt"
)
func main() {
var i interface{} = 100
val := i.(int)
fmt.Println(val + 100) // 200
}
如果你构造一个变量时,不确定它的类型,可以先定义为 interface{} 进行赋值,后面用到该变量时进行类型断言转化为对应类型就行了。
var dbConfig = map[string]interface{}{
"host" : "xxx.com:6379",
"passwd": "xxx",
"db" : 0,
}
func newPool(cfg interface{}) *redis.Pool {
// dbConfig 中的每一项都是 interface{} 类型
dbConfig := cfg.(map[string]interface{})
host := dbConfig["host"].(string)
passwd := redis.DialPassword(dbConfig["passwd"].(string))
db := redis.DialDatabase(dbConfig["db"].(int))
...
}
使用类型断言修改对象值,断言的类型必须是指针。
package main
import (
"fmt"
)
type User struct {
id int
name string
}
func main() {
u := User{1, "Tom"}
var vi, pi interface{} = u, &u //copy u and assign to vi
// vi.(User).name = "Jack" // Error: cannot assign to vi.(User).name
pi.(*User).name = "Jack"
fmt.Printf("%v\n", vi.(User)) //{1 Tom}
fmt.Printf("%v\n", pi.(*User)) //&{1 Jack}
}
以接口组织代码的方式在 Go 语言中非常常见,较大的接口定义,可以由多个小接口定义组合而成。
type Reader interface {
Read(p []byte) (n int, err error)
}
type Writer interface {
Write(p []byte) (n int, err error)
}
type ReadWriter interface {
Reader
Writer
}
//只依赖于必要功能的最小接口
func StoreData(reader Reader) error {
...
}
面向接口是 Go 语言鼓励的开发方式,我们应该遵循固定的模式对外提供功能。
- 使用大写的 Service 对外暴露方法;
- 使用小写的 service 实现接口中定义的方法;
- 通过 func NewService(...) (Service, error) 函数初始化 Service 接口;
package post
type Service interface {
ListPosts() ([]*Post, error)
}
type service struct {
conn *grpc.ClientConn
}
func NewService(conn *grpc.ClientConn) Service {
return &service{
conn: conn,
}
}
func (s *service) ListPosts() ([]*Post, error) {
posts, err := s.conn.ListPosts(...)
if err != nil {
return []*Post{}, err
}
return posts, nil
}
为我们提供一种可以在运行时操作任意类型对象的能力。比如我们可以查看一个接口变量的具体类型,看看一个结构体有多少字段,如何修改某个字段的值等等.
go 的 reflect 为我们提供了两个方法用来获取对象的类型和实例:
- reflect.TypeOf(): 获取任意对象的具体类型,如果为空则返回 nil。
- reflect.ValueOf():获取对象的实例,如果为空则返回 0。
package main
import (
"fmt"
"reflect"
)
type User struct{
Name string
Age int
}
func main() {
user := User{"张三",20}
// TypeOf会返回目标数据的类型,比如上文我们通过 type 关键字定义的 User 类型
reflectType := reflect.TypeOf(user)
// valueOf返回目标数据的的值,比如上文的 {张三 20}
reflectValue := reflect.ValueOf(user)
fmt.Println("type: ", reflectType)
fmt.Println("value: ", reflectValue)
}
底层类型指的是基础的数据类型,比如上面我们申明的 User 类型,其底层基础类型是 struct,底层类型可以通过 Kind()
方法获取。
package main
import (
"fmt"
"reflect"
)
type User struct{
Name string
Age int
}
func main() {
user := User{"张三",20}
reflectType := reflect.TypeOf(user)
concreteType := reflectType.Kind()
if concreteType == reflect.Struct {
fmt.Println("底层类型是 struct")
}else {
fmt.Println("类型不是 struct")
}
}
NumField() 和 NumMethod() 可以获取目标对象的字段数量和方法数量。
package main
import (
"fmt"
"reflect"
)
type User struct{
Name string
Age int
}
func (u User) GetName() string{
return u.Name
}
func main() {
user := User{"张三",20}
t := reflect.TypeOf(user)
v := reflect.ValueOf(user)
fmt.Println("类型名称:", t.Name())
for i:=0;i<t.NumField();i++ {
fmt.Println("字段名:",t.Field(i).Name)
fmt.Println("字段值:",v.Field(i).Interface())
}
for i:=0;i<t.NumMethod() ;i++ {
m := t.Method(i)
fmt.Println("方法名:", m.Name)
fmt.Println("方法类型:", m.Type)
}
}
利用反射修改对象的字段值时需要注意:
- 传入 ValueOf() 的是对象的地址,也就是对象的指针。
- Elem() 方法可以找到这个指针指向的值。
- FieldByName() 方法可以找到要修改的目标字段。
package main
import (
"fmt"
"reflect"
)
type User struct {
Name string
Age int
}
func main() {
s := &User{Name: "张三", Age: 20}
v := reflect.ValueOf(s)
// 修改值必须是指针类型否则不可行
if v.Kind() != reflect.Ptr {
fmt.Println("不是指针类型,没法进行修改操作")
return
}
// 获取指针所指向的元素
v = v.Elem()
if !v.CanSet() {
fmt.Println("无法修改该对象")
return
}
// 获取目标key的Value的封装
name := v.FieldByName("Name")
if name.Kind() == reflect.String {
name.SetString("李四")
}
fmt.Printf("%#v \n", *s)
// 如果是整型的话
test := 888
testV := reflect.ValueOf(&test)
testV.Elem().SetInt(666)
fmt.Println(test)
}
package main
import (
"fmt"
"reflect"
)
type User struct {
Name string
Age int
}
func (s User) EchoName(name string){
fmt.Println("我的名字是:", name)
}
func main() {
s := User{Name: "张三", Age:20}
v := reflect.ValueOf(s)
// 获取目标方法
m := v.MethodByName("EchoName")
// 构造参数
args := []reflect.Value{reflect.ValueOf("李四")}
// 调用函数
m.Call(args) //我的名字是: 李四
}
更多关于反射的使用见 这里。
单线程也可以实现并发,go 中的并发由 Go 自身的调度器实现,并行是和主机的CPU 核数有关的,多核就可以并行并发,单核只能并发了。
协程
本质上是一种用户态线程,不需要操作系统来进行抢占式调度,且在真正的实现中寄存于线程中,因此系统开销极小.
通过 go + func()
即可创建一个 goroutine,就这样一个简单的 go
关键字就将同步代码转为异步代码:
import "fmt"
func mytest() {
fmt.Println("hello, go")
}
func main() {
// 启动一个协程
go mytest()
fmt.Println("hello, world")
}
main 函数本身相当于主线程,当 main 函数执行完成后,这个线程也就终结了, 它不会等待其下运行着的所有协程返回结果,因此上面的这段代码只会输出hello, world
,而不会输出 hello, go
。因为协程的创建需要时间,main 函数执行完后,它里面的协程可能还没来得及执行。
因此如果 main 函数等待几秒,则会看到其他协程的输出:
import (
"fmt"
"time"
)
func mytest() {
fmt.Println("hello, go")
}
func main() {
go mytest()
fmt.Println("hello, world")
time.Sleep(time.Second)
}
WaitGroup 的字面意思就是指等待一组协程执行完成后才会继续向下执行。细节见这里这里
sync.WaitGroup 的使用步骤:
WaitGroup 的局限:
其中等待的一组协程中有一个协程发生错误,则告诉协程组的其他协程,全部停止运行(本次任务失败)以免浪费系统资源。该场景WaitGroup是无法实现的,那么该场景该如何实现呢,就需要用到通知机制,其实也可以用channel来实现。
func main() {
var wg sync.WaitGroup
go func(){
wg.Add(1)
defer wg.Done()
for i:=1;i<100;i++ {
fmt.Println("A:",i)
}
}()
go func(){
wg.Add(1)
defer wg.Done()
for i:=1;i<100;i++ {
fmt.Println("B:",i)
}
}()
wg.Wait()
}
通道
用在多个协程间
同步数据。
注意:
waitGoup
只是用来协程间同步,但不通信,而 channel 既能同步又能通信。
通道是和 slice,map 一样的数据类型,可以使用内置的 make 函数声明初始化。
# 定义通道
ch:=make(chan int)
# 读写通道
ch <- 2
<-ch
# 关闭通道, 注意:通道只应该被发送者关闭。
close(ch)
# 检测通道是否关闭
# data为通道的值,open 为通道的状态: 未关闭为 true, 已关闭则为 false
data, open := <-pipline
通道读取有点像 php 的数组元素弹出函数 array_pop
一样,当你弹出一个元素后,源数组就少了一个,因此把通道读取也应看成弹出元素一样,比如说通道中只有一个元素,当你取完以后就不能再取了。
package main
import (
"fmt"
)
func main() {
ch := make(chan int, 1)
go func() {
ch <- 1
}()
fmt.Println(<-ch)
fmt.Println(<-ch) //fatal error: all goroutines are asleep - deadlock!
}
只能读或只能写的通道称为单项通道,单项通道一般用作函数参数。
- 只能写入的通道:ch chan <- int
- 只能读取的通道:ch <- chan int
// 只能写入
func output(ch chan <- int) {
//...
}
// 只能读取
func input( ch <- chan int) {
//...
}
创建通道的 make 函数可以接收第二个参数,为通道的容量。
- 容量 = 0 的通道称为
无缓冲通道
。无缓冲通道要求接收者和发送者不能处于同一个协程中,否则将会造成死锁,在接收者未读取通道前,发送者的后续执行都是阻塞的。- 容量 > 0 的通道称为
缓冲通道
;对于缓冲通道,cap(ch)可以获取通道的容量,len(ch)可以获取通道中的元素数量,缓冲通道对接着者和发送者的顺序没有要求,并且二者可以出现在同一协程中。
无缓冲通道
package main
import (
"fmt"
"time"
)
func hello(pipline chan string) {
pipline <- "hello world"
}
func main() {
pipline := make(chan string)
go hello(pipline) //发送者
fmt.Print(<-pipline) //接收者
time.Sleep(time.Second)
}
需要注意,无缓冲通道会阻塞后续的执行,直到通道中的数据被读取。
package main
import (
"fmt"
"time"
)
func hello(ch chan string) {
ch <- "hello world\n"
//阻塞后续代码的执行,顺序输出 hello world 和 end
fmt.Print("end\n")
}
func main() {
//未设置容量的非缓冲通道
ch := make(chan string)
go hello(ch)
time.Sleep(time.Millisecond * 100)
fmt.Print(<-ch)
//等待几秒以便子协程中的数据输出
time.Sleep(time.Millisecond * 100)
/*
hello world
end
*/
}
缓冲通道
package main
import "fmt"
func main() {
pipline := make(chan string, 1)
pipline <- "hello world"
fmt.Println(<-pipline)
//hello world
}
缓冲通道不会阻塞后续代码的执行,你会看到后续代码会立马输出。
package main
import (
"fmt"
"time"
)
func hello(ch chan string) {
ch <- "hello world\n"
//不会阻塞后续代码执行,你会看到 end 先于 hello world 输出
fmt.Print("end\n")
}
func main() {
//设置了容量的缓冲通道
ch := make(chan string, 1)
go hello(ch)
time.Sleep(time.Millisecond * 100)
fmt.Print(<-ch)
/*
end
hello world
*/
}
通道遍历可以有三种方式: for
,for-range
,任务数量
;前两种遍历通道时,务必确保发送者已关闭(close)通道,否则会造成接收者一直在等待数据 ,造成死锁
;最后一种则无需关闭通道, 但缺点是你必须知道任务数量是多少。
package main
import "fmt"
func main() {
taskNum := 10
c := make(chan int, taskNum)
//c := make(chan int)
// 发送者
go func() {
for i := 0; i < taskNum; i++ {
c <- i
}
//关闭channel
close(c)
}()
//第一种:for循环遍历
for {
if data, ok := <-c; ok {
fmt.Println("for:", data)
} else {
break
}
}
//第二种:for-range遍历
for i := range c {
fmt.Println("for-range:", i)
}
//第三种:通过任务数量遍历,这种发送者无需关闭 channel
for i := 0; i < taskNum; i++ {
fmt.Println("taskNum:", <-c)
}
}
再看一个通道遍历的变体,在协程中遍历通道。
package main
import (
"fmt"
)
func main() {
c := make(chan int)
execRes := make(chan string, 10)
//协程1: for
go func() {
for {
execRes <- fmt.Sprintf("task=%d exec by for", <-c)
}
}()
//协程2: for-range
go func() {
for i := range c {
execRes <- fmt.Sprintf("task=%d exec by for-range", i)
}
}()
// 生成10个任务,分配给给两个协程去执行
taskNum := 10
for i := 1; i <= taskNum; i++ {
c <- i
}
// 通过任务数量遍历通道,获取每一个任务的执行结果,每个协程最终能分配几个任务是随机的
for i := 0; i < taskNum; i++ {
fmt.Println(<-execRes)
}
}
select 会阻塞到某个 channel 可以继续执行为止,当多个 channel 就绪时,select 会随机选择一个执行。
select 的语法类似 switch 的功能,可以使用 break 或 return 就会退出 select 语句。
c := make(chan int)
go func() {
for v := range c {
// 随机输出了0,1, 证明select 是随机读取/写入通道的
fmt.Println(v)
}
}()
for i := 0; i < 10; i++ {
select {
case c <- 0:
case c <- 1:
}
}
为了防止长时间阻塞,可以为 select 设置超时时间(等待 channel 数据的时间)。
c := make(chan bool)
select {
case v := <-c:
fmt.Println(v)
case t := <-time.After(3 * time.Second):
fmt.Println(t)
fmt.Println("Timeout")
}
为了在发送或者接收时不发生阻塞,select 语句还支持定义一个 default 分支。
c := make(chan int)
select {
case i := <-c:
fmt.Println(i)
default:
fmt.Println("default")
}
无论缓冲/非缓冲通道,如果接收者出现在发送者前面会造成死锁,因为接收者为了读取数据会一直阻塞后面的发送者往通道里写数据。
//死锁
func main() {
// c := make(chan int, 1)
c := make(chan int)
//通道中没有数据会一直阻塞后续的执行
fmt.Println(<-c)
go func() {
c <- 1
}()
//fatal error: all goroutines are asleep - deadlock!
}
//正常
func main() {
// c := make(chan int, 1)
c := make(chan int)
go func() {
c <- 1
}()
//通道中能够读取数据
fmt.Println(<-c)
//1
}
非缓冲通道,发送者出现在前面 也会造成死锁,因为通道无法被读取,导致发送者一直阻塞;但如果是 缓冲通道则正常,因为缓冲通道写入不会阻塞后续执行。
//死锁(非缓冲通道)
func main() {
c := make(chan int)
//通道无法被读取,会一直阻塞后续的执行
c <- 1
go func() {
fmt.Println(<-c)
}()
time.Sleep(3 * time.Second)
// fatal error: all goroutines are asleep - deadlock!
}
//正常(非缓冲通道)
func main() {
c := make(chan int)
go func() {
fmt.Println(<-c)
}()
//通道能被读取,不会阻塞后续执行
c <- 1
time.Sleep(3 * time.Second)
//1
}
//正常(缓冲通道)
func main() {
c := make(chan int, 1)
//缓冲通道不会阻塞后续执行
c <- 1
go func() {
fmt.Println(<-c)
}()
time.Sleep(3 * time.Second)
// 1
}
无论缓冲/非缓冲通道,在同一协程中
通道写入超过通道容量限制会导致死锁;但如果不在同一协程
,写入数据超过通道容量限制则不会造成死锁。
//正常:未超过容量限制
func main() {
pipline := make(chan string, 1)
pipline <- "hello world"
fmt.Println(<-pipline)
}
//死锁:超过容量限制
func main() {
ch1 := make(chan string, 1)
ch1 <- "hello world"
ch1 <- "hello China" //这条写入超过容量限制,导致死锁
fmt.Println(<-ch1)
//fatal error: all goroutines are asleep - deadlock!
}
//正常:不在同一协程写入超过容量限制
package main
import "fmt"
func main() {
ch := make(chan string, 1)
go func() {
ch <- "hello world"
ch <- "hello China"
}()
fmt.Println(<-ch)
//hello world
}
无论缓冲/非缓冲通道,如果通道没有关闭,读取次数超过通道容量会造成死锁;但如果通道关闭的话,可以随意读取多次,如果值已取出,则读取的是类型的零值。
package main
import "fmt"
func main() {
UnDeadLocl()
DeadLocl()
}
//死锁
func DeadLocl() {
ch := make(chan int, 1)
go func() {
ch <- 1
//close(ch)
}()
fmt.Println(<-ch)
fmt.Println(<-ch)
}
//正常
func UnDeadLocl() {
ch := make(chan int, 1)
go func() {
ch <- 1
close(ch)
}()
fmt.Println(<-ch)
fmt.Println(<-ch)
}
遍历一个未关闭的缓冲/非缓冲通道,会造成死锁,为了避免这种情况,发送者在发送完数据后,需要调用 close() 来关闭通道。但如果你通过任务数量来遍历通道,则无需关闭通道。
package main
import "fmt"
func main() {
DeadLocl()
UnDeadLocl()
}
//死锁
func DeadLocl() {
pipline := make(chan string, 2)
go func() {
pipline <- "hello world"
pipline <- "hello China"
// close(pipline)
}()
for data := range pipline {
fmt.Println(data)
}
}
//正常(无需close通道)
func UnDeadLocl() {
taskNum := 2
ch := make(chan string, taskNum)
go func() {
ch <- "hello world"
ch <- "hello China"
// close(pipline)
}()
for i := 0; i < taskNum; i++ {
fmt.Println(<-ch)
}
}
用通道来做锁
:使用容量为1的缓冲通道以达到共享变量的目的,当通道达到设定的容量时,此时再往里发送数据会阻塞整个程序。
package main
import (
"fmt"
"time"
)
func main() {
// 注意要设置容量为 1 的缓冲信道以达到共享变量的目的
ch := make(chan bool, 1)
var x int
for i := 0; i < 1000; i++ {
go func(n int) {
//如果使用非缓冲通道,在同一协程中将出现死锁(发送者和接收者相互等待),将导致永远执行不到x++ 这里
ch <- true
// 由于 x++ 不是原子操作,所以应避免多个协程对x进行操作
// 使用容量为1的信道可以达到锁的效果
x++
<- ch
}(x)
}
time.Sleep(time.Second)
fmt.Println("x 的值:", x)
}
退出信号
。有并发就有资源竞争,两个或者多个goroutine在没有相互同步的情况下,访问某个共享的资源,比如同时对该资源进行读写时,就会处于相互竞争的状态。
下面一个资源竞争的例子,我们可以多运行几次这个程序,会发现结果可能是2,也可以是3,也可能是4。因为共享资源count变量没有任何同步保护,所以两个goroutine都会对其进行读写,会导致对已经计算好的结果覆盖。
package main
import (
"fmt"
"runtime"
"sync"
)
var (
count int32
wg sync.WaitGroup
)
func main() {
wg.Add(2)
go incCount()
go incCount()
wg.Wait()
fmt.Println(count)
}
func incCount() {
defer wg.Done()
for i := 0; i < 2; i++ {
value := count
runtime.Gosched()
value++
count = value
}
}
借助 go build -race
我们可以详细的查看 goroutine 间的竞争情况。它会为我们在当前项目下生成一个可以执行文件,然后我们运行这个可执行文件,就可以看到打印出的检测信息。
tcl@tcl:/data/goProject$ go build -race
tcl@tcl:/data/goProject$ ./goProject
==================
WARNING: DATA RACE
Read at 0x0000005ef5ec by goroutine 7:
main.incCount()
/data/goProject/index.go:25 +0x6f
Previous write at 0x0000005ef5ec by goroutine 6:
main.incCount()
/data/goProject/index.go:28 +0x8e
Goroutine 7 (running) created at:
main.main()
/data/goProject/index.go:17 +0x77
Goroutine 6 (running) created at:
main.main()
/data/goProject/index.go:16 +0x5f
==================
4
Found 1 data race(s)
可以看到提示我们:
goroutine 7在代码25行读取共享资源value := count,而这时goroutine 6正在代码28行修改共享资源count = value,这就出现了资源竞争了。
面对并发类的资源竞争问题,传统解决的办法就是对资源加锁,那么接下来我们看下 go 中提供的几种资源竞争的处理方案。
atomic包中的原子函数提供了对资源加锁的功能。
atomic.LoadInt32和atomic.StoreInt32两个函数,一个读取int32类型变量的值,一个是修改int32类型变量的值,这两个都是原子性的操作,Go已经帮助我们在底层使用加锁机制,保证了共享资源的同步和安全。
package main
import (
"fmt"
"runtime"
"sync"
"sync/atomic"
)
var (
count int32
wg sync.WaitGroup
)
func main() {
wg.Add(2)
go incCount()
go incCount()
wg.Wait()
fmt.Println(count)
}
func incCount() {
defer wg.Done()
for i := 0; i < 2; i++ {
value := atomic.LoadInt32(&count)
runtime.Gosched()
value++
atomic.StoreInt32(&count,value)
}
}
使用原子函数后,再次使用工具 go build -race 检查,发现不会提示有问题了。
tcl@tcl:/data/goProject$ go build -race
tcl@tcl:/data/goProject$ ./goProject
2
atomic虽然可以解决资源竞争问题,但是比较都是比较简单的,支持的数据类型也有限,所以Go语言还提供了另一种类型的锁-互斥锁
。
互斥锁可以让我们自己灵活的控制哪些代码,同时只能有一个goroutine访问,被互斥锁控制的这段代码范围,被称之为临界区,临界区的代码,同一时间,只能又一个goroutine访问。
package main
import (
"fmt"
"runtime"
"sync"
)
var (
count int32
wg sync.WaitGroup
mutex sync.Mutex
)
func main() {
wg.Add(2)
go incCount()
go incCount()
wg.Wait()
fmt.Println(count)
}
func incCount() {
defer wg.Done()
for i := 0; i < 2; i++ {
mutex.Lock()
value := count
runtime.Gosched()
value++
count = value
mutex.Unlock()
}
}
示例中我们先调用mutex.Lock()对有竞争资源的代码加锁,这样当一个goroutine进入这个区域的时候,其他goroutine就进不来了,只能等待,一直到调用mutex.Unlock() 释放这个锁为止。
互斥锁有个问题:
当一个goroutine访问的时候,其他goroutine都不能访问,这样肯定保证了资源的同步,避免了竞争,不过也降低了性能。
而我们希望是像 mysql 中的共享锁一样,当前进程读数据的时候,不要互斥其他的进程的读操作,因为数据不会被修改,那么其实是不存在资源竞争的问题的,不管怎么读取,多少goroutine同时读取,都是可以的那么。
所以这就延伸出来 go 中另外一种锁-读写锁
:
读写锁可以让多个读操作同时并发,同时读取,但是对于写操作是完全互斥的。也就是说,当一个goroutine进行写操作的时候,其他goroutine既不能进行读操作,也不能进行写操作。
var count int
var wg sync.WaitGroup
var rw sync.RWMutex
func main() {
wg.Add(10)
for i:=0;i<5;i++ {
go read(i)
}
for i:=0;i<5;i++ {
go write(i);
}
wg.Wait()
}
func read(n int) {
rw.RLock()
fmt.Printf("读goroutine %d 正在读取...\n",n)
v := count
fmt.Printf("读goroutine %d 读取结束,值为:%d\n", n,v)
wg.Done()
rw.RUnlock()
}
func write(n int) {
rw.Lock()
fmt.Printf("写goroutine %d 正在写入...\n",n)
v := rand.Intn(1000)
count = v
fmt.Printf("写goroutine %d 写入结束,新值为:%d\n", n,v)
wg.Done()
rw.Unlock()
}
我们在read里使用读锁,也就是RLock和RUnlock,写锁的方法名和我们平时使用的一样Lock和Unlock,这样我们就使用了读写锁,可以并发的读,但是同时只能有一个写,并且写的时候不能进行读操作。
sync.Cond 经常用在多个 goroutine 等待,一个 goroutine 通知(事件发生)的场景。
sync.Cond的作用并不是保证在同一时刻仅有一个线程访问某一个共享数据,而是在对应的共享数据的状态发生变化时,通知其他因此而被阻塞的协程。
sync.Cond 需要与锁(互斥锁,或者读写锁)一起使用, NewCond 函数可以返回 *sync.Cond 的对象指针,成员变量 L 代表与条件变量搭配使用的锁。
func NewCond(l Locker) *Cond {
return &Cond{L: l}
}
// A Locker represents an object that can be locked and unlocked.
type Locker interface {
Lock()
Unlock()
}
该对象主要有三个方法
- wait():阻塞等待通知,如源码所示Wait()方法会阻塞所在协程,直到收到其他协程的通知结束阻塞时,会重新给 c.L 加锁,并且继续执行 Wait 后面的代码。因此调用 Wait 方法前,必须加锁。
- Signal():单发通知,给一个正等待(阻塞)在该条件变量上的 goroutine 发送通知。
- Broadcast():广播通知,给正在等待(阻塞)在该条件变量上的所有 goroutine 发送通知。
func (c *Cond) Wait() {
c.checker.check()
t := runtime_notifyListAdd(&c.notify) // 等待的goruntine数+1
c.L.Unlock() // 释放锁资源
runtime_notifyListWait(&c.notify, t) // 阻塞,等待其他goruntine唤醒
c.L.Lock() // 获取资源
}
func (c *Cond) Signal() {
c.checker.check()
runtime_notifyListNotifyOne(&c.notify) // 唤醒最早被阻塞的goruntine
}
func (c *Cond) Broadcast() {
c.checker.check()
runtime_notifyListNotifyAll(&c.notify) // 唤醒所有goruntine
}
示例一
package main
import (
"fmt"
"sync"
"time"
)
var (
lock sync.Mutex //互斥锁
cond = sync.NewCond(&lock) //条件变量
)
func main() {
//调用 Wait 方法前必须加锁
cond.L.Lock()
fmt.Println("main thread got lock ")
go doSomething() //子线程
fmt.Println("main thread begin wait ")
//main 线程执行到 wait 释放锁后,这时子线程会获取锁成功
cond.Wait()
fmt.Println("main thread end wait ")
cond.L.Unlock()
fmt.Println("main thread release lock ")
/*
main thread got lock
main thread begin wait
sub thread begin get lock
sub thread got lock
sub thread release lock
main thread end wait
main thread release lock
*/
}
func doSomething() {
time.Sleep(time.Second * 2)
fmt.Println("sub thread begin get lock ")
//调用条件变量的signal方法之前,加锁这个并不是必须的,可以注释获取和释放锁代码,也是OK的
//cond.L.Lock()
//defer cond.L.Unlock()
fmt.Println("sub thread got lock ")
time.Sleep(5 * time.Second)
cond.Signal() //这时候main线程则会重新获取到锁,然后从wait返回
fmt.Println("sub thread release lock")
}
示例二
假设我们需要循环处理某队列中的任务,但是我们又想在没有任务时,能够出让CPU的空闲给别的进程。
未使用条件变量前的代码:
func addTask(t *Task) {
mutex.Lock()
tasks[t.Id] = t
mutex.UnLock()
}
func Loop() {
for {
mutex.Lock()
for taskId, task := range tasks {
// handle code
}
lenght := len(task)
mutex.UnLock()
// 为了减少cpu空转,当队列为空的时候sleep 2秒
if length == 0 {
time.Sleep(time.Second * 2)
}
}
}
使用条件变量后的代码:
func addTask(t *Task) {
mutex.Lock()
tasks[t.Id] = t
if len(task) == 1 {
cond.Signal()
}
mutex.UnLock()
}
func Loop() {
for {
mutex.Lock()
// 如果当前任务数为0 调用Wait()等待新任务,直到添加任务后被唤醒
if len(tasks) == 0 {
cond.Wait()
}
for taskId, task := range tasks {
// handle code
}
mutex.UnLock()
}
}
sync.Once
典型应用场景就是仅需执行一次的任务,例如数据库连接池的建立,全局变量的延迟初始化等。
sync.Once 类型的 Do() 可以实现只执行一次的效果,该方法接收一个无参数,无结果的函数作为参数,该方法一旦被调用,就会去调用作为参数的那个函数。
package main
import (
"fmt"
"math/rand"
"sync"
)
func main() {
var count = 0
var once sync.Once
max := rand.Intn(100)
for i:=0 ; i<max; i++{
once.Do(func() {
count++
})
}
fmt.Println("Count:",count) // Count: 1
}
可以看到无论运行了多少次,始终输出的是第一次运行的结果 Count: 1
。
一句话总结:保存和复用临时对象,减少内存分配,降低 GC 压力。
sync.Pool
只有一个公开的字段 New
,该字段的类型是一个函数类型,该函数一般仅在池中无可用对象的时候才调用,使用 New 生成的对象是持久的。
type Pool struct {
noCopy noCopy
local unsafe.Pointer // 本地P缓存池指针
localSize uintptr // 本地P缓存池大小
// 当池中没有可能对象时
// 会调用 New 函数构造构造一个对象
New func() interface{}
}
sync.Pool 还有两个方法 Put
和 Get
,前者用来向对象池中写入对象,后者用来从对象池中读取对象。
Put 方法写入的对象是临时的,因为这种对象通过 Get 方法访问时,从对象池中返回的同时并删除,并且这种临时对象也会受垃圾回收的影响。
对于 Get 方法,如果没有临时对象,那么就会访问 New 函数生成的持久对象,如果你在初始化对象池的时候没有设置 New 函数,那么 Get 方法只能返回 nil 了。
注意:
Get 方法只会删除 Put 方法写入的临时对象,并不会删除 New 函数创建的持久对象。
示例
package main
import (
"fmt"
"runtime"
"runtime/debug"
"sync"
"sync/atomic"
)
func main() {
// 禁用GC,并保证在main函数执行结束前恢复GC。
defer debug.SetGCPercent(debug.SetGCPercent(-1))
var count int32
newFunc := func() interface{} {
return atomic.AddInt32(&count, 1)
}
pool := sync.Pool{New: newFunc}
// 访问使用 New 函数创建的持久对象
v1 := pool.Get()
fmt.Printf("Value 1: %v\n", v1) //1
// 临时对象池的存取
pool.Put(10)
pool.Put(11)
pool.Put(12)
v2 := pool.Get()
fmt.Printf("Value 2: %v\n", v2) // 10
// 手动触发垃圾回收,导致之前使用 put 写入的三个值被回收掉
debug.SetGCPercent(100)
runtime.GC()
// 这时再访问对象池,就会返回 New 函数创建的对象
v3 := pool.Get()
fmt.Printf("Value 3: %v\n", v3) // 2
//置空对象池,这时再获取对象就会发现返回为 nil
pool.New = nil
v4 := pool.Get()
fmt.Printf("Value 4: %v\n", v4) // nil
}
Context 是一个可以帮助我们实现多 goroutine 协作流程的同步工具,从设计的角度来看,Context 提供了一种
父goroutine
对子goroutine
的管理功能。
我们为某个 HTTP 请求创建一个 goroutine 以并发地处理业务,同时这个 goroutine 也可能会创建更多的 goroutine 来访问数据库或者 RPC 服务。当上层的 goroutine 超时或者被终止的时候,我们希望其他的 子goroutine 及时停掉无用的工作减少额外资源的消耗。
这其实就是Context
的最大作用,如果没有Context,当上层执行的操作出现错误时,下层其实不会收到错误而是会继续执行下去。每一个 Context 都会从最顶层的 goroutine 一层一层传递到最下层,这也是 Context 最常见的使用方式,此外 Context 还能携带键值对信息传递给下层 goroutine。
Context 是 context 包对外暴露的接口,该接口定义了四个需要实现的方法:
type Context interface {
Deadline() (deadline time.Time, ok bool)
Done() <-chan struct{}
Err() error
Value(key interface{}) interface{}
}
Deadline方法是获取设置的截止时间的意思,第一个返回式是截止时间,到了这个时间点,Context会自动发起取消请求;第二个返回值ok==false时表示没有设置截止时间,如果需要取消的话,需要调用取消函数进行取消。
Done方法返回一个只读的chan,类型为struct{},我们在goroutine中,如果该方法返回的chan可以读取,则意味着parent context已经发起了取消请求,我们通过Done方法收到这个信号后,就应该做清理操作,然后退出goroutine,释放资源。多次调用 Done 方法会返回同一个 Channel。
Err 方法可以获取取消的错误原因,因为什么Context被取消。
Value方法获取该Context上绑定的值,是一个键值对,所以要通过一个Key才可以获取对应的值,这个值一般是线程安全的。
父 Context
在代码中,都是基于父Context
(也就是 根Context
) 派生出 子context
。这些 Context 对象形成一棵树:当一个 Context 对象被取消时,继承自它的所有 Context 都会被取消
有两种方法创建 父Context
:
- context.Background()
- context.TODO()
这两个方法只能用在最外层的 goroutine 代码中,比如 main 函数里。一般使用 Background() 方法创建父Context
。
TODO() 用于当前不确定使用何种 Context,留待以后调整。
子 Context
一个 Context 被 cancel,那么它派生出的 Context 都会收到取消信号(表现为 context.Done() 返回的 channel 收到值)。
有四种方法派生 context :
func WithCancel(parent Context) (ctx Context, cancel CancelFunc)
func WithDeadline(parent Context, deadline time.Time) (Context, CancelFunc)
func WithTimeout(parent Context, timeout time.Duration) (Context, CancelFunc)
func WithValue(parent Context, key, val interface{}) Context
这四个 With 函数,接收的都有一个 partent 参数,就是 父Context
,我们要基于这个父Context
创建出子Context
的意思,这种方式可以理解为子Context
对父Context
的继承,也就是基于父Context
的衍生。
可以看到前三个函数都返回一个取消函数 CancelFunc,这个函数的主要作用就是在 parent 和 child 之间同步取消和结束的信号,保证在 parent 被取消时,child 也会收到对应的信号,不会发生状态不一致的问题。
WithTimeout和WithDeadline基本上一样,表示多少时间后自动取消Context的意思,然我们也可以不等到这个时候,可以提前通过取消函数进行取消.
WithValue
函数和取消Context无关,它会创建一个携带信息的子context
,可以是 user 信息、认证 token等。该 Context 与其派生的子Context
都会携带这些信息。
示例 context.WithCancel
package main
import (
"context"
"fmt"
"time"
)
func main() {
ctx, cancel := context.WithCancel(context.Background())
go func(ctx context.Context) {
for {
select {
case <-ctx.Done():
fmt.Println("收到取消任务的通知了...")
return
default:
fmt.Println("执行任务中...")
time.Sleep(1 * time.Second)
}
}
}(ctx)
time.Sleep(3 * time.Second)
fmt.Println("任务执行出错,通知其他 goroutine 取消任务")
cancel()
time.Sleep(3 * time.Second)
fmt.Println("完毕...")
/*
执行任务中...
执行任务中...
执行任务中...
任务执行出错,通知其他 goroutine 取消任务
收到取消任务的通知了...
完毕...
*/
}
示例 WithTimeout
package main
import (
"context"
"fmt"
"time"
)
func main() {
timeOut := 1 * time.Second
ch := make(chan string)
ctx, _ := context.WithTimeout(context.Background(), timeOut)
go handle(ctx, ch)
fmt.Println(<-ch)
/*
开始运行...
超时退出...
*/
}
func handle(ctx context.Context, ch chan string) {
fmt.Println("开始运行...")
time.Sleep(1 * time.Second)
select {
case <-ctx.Done():
ch <- "超时退出..."
default:
ch <- "结束运行..."
}
}
上述示例中如果你将 timeOut 换成2s后你会发现它是正常输出的.
示例 WithValue
package main
import (
"context"
"fmt"
"time"
)
var key string = "name"
func main() {
ctx, cancel := context.WithCancel(context.Background())
//这样就生成了一个新的 Context,这个新的 Context 会带有键值对
valueCtx:=context.WithValue(ctx,key,"custom-value")
go watch(valueCtx)
//sleep 2s 以便等待协程输出后再输出
time.Sleep(2 * time.Second)
fmt.Println("任务执行出错,通知其他 goroutine 取消任务")
//取消父Context
cancel()
//sleep 2s 以便等待协程输出后再输出
time.Sleep(2 * time.Second)
fmt.Println("完毕...")
}
func watch(ctx context.Context) {
for {
select {
case <-ctx.Done():
//取出值
fmt.Println("收到取消任务的通知了,value:", ctx.Value(key))
return
default:
//取出值
fmt.Println("执行任务中,value:",ctx.Value(key))
time.Sleep(2 * time.Second)
}
}
}
/*
执行任务中,value: custom-value
任务执行出错,通知其他 goroutine 取消任务
收到取消任务的通知了,value: custom-value
完毕...
*/