gopher [ˈɡoʊfər] n. 囊地鼠(产自北美的一种地鼠)
命令
数据类型 声明方式
声明一个变量
声明多个变量1 2 3 4 var ( i string = "10" j int = 20 )
省略类型声明1 2 3 4 var ( i = "10" j = 20 )
省略var
(常用)
基础数据类型 整形
有符号整型(负数、0和正数):如 int(可能是 32bit,也可能是 64bit,和硬件设备 CPU 有关)、int8、int16、int32 和 int64
无符号整型(0和正数):如 uint(可能是 32bit,也可能是 64bit,和硬件设备 CPU 有关)、uint8(=byte)、uint16、uint32 和 uint64
P.S. int 跟机器字长一致,这样可以获取最大的执行效率。在不关心数值范围的场景下 int 足够了 ,比如数组下标。相反如果你在 32 位机器上使用 int64,本来一条指令的事情要变成多条指令。int32 和 int64 这些一般用于编解码 、底层硬件相关 ,或者是数值范围敏感 的场景。
浮点型
float32:var f32 float32 = 2.2
float64:var f64 float64 = 10.3456
(常用,误差小)
布尔型
bool:var bf bool = false
或者 var bf bool = true
字符串 1 2 3 var s1 string = "Hello" var s2 string = "世界" fmt.Println("s1 is" ,s1,",s2 is" ,s2)
拼接字符串
1 2 3 4 var s1 string = "Hello" var s2 string = "世界" fmt.Println("s1+s2" , s1 + s2)
其他数据类型 变量 在 Go 语言中,指针对应的是变量在内存中的存储位置,也就说指针的值就是变量的内存地址。通过 & 可以获取一个变量的地址,也就是指针。
在以下的代码中,pi 就是指向变量 i 的指针。要想获得指针 pi 指向的变量值,通过*pi这个表达式即可。尝试运行这段程序,会看到输出结果和变量 i 的值一样。
1 2 3 4 5 6 7 i := 20 p := &i fmt.Println(p, *p) i += 1 fmt.Println(p, *p) c := p fmt.Println(c, p, *c, *p)
常量
补充 类型转换 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 i := "1" j := 2 k := "true" v := false m := 3.14 n := "6.28" p, _ := strconv.Atoi(i) q := strconv.Itoa(j) r, _ := strconv.ParseBool(k) s := strconv.FormatBool(v) x := strconv.FormatFloat(m, 'f' , 2 , 64 ) y, _ := strconv.ParseFloat(n, 64 ) println (p, q, r, s, x, y)
Strings包 官方文档:strings 文档
1 2 3 4 5 6 7 8 9 i := "你好" prefix := strings.HasPrefix(i, "你" ) fmt.Println(prefix) j := "mciu9b21v" index := strings.Index(j, "v" ) fmt.Println(index)
集合类型 array 数组 数组在内存中都是连续存放的
声明:array := [5]int{1, 2, 3, 4, 5}
省略长度声明:array := [...]int{1, 2, 3, 4, 5}
指定下标:
1 2 3 4 5 6 7 8 9 10 array := [...]string {1 : "b" , 3 : "d" } fmt.Println(array) fmt.Println(len (array)) for i, v := range array { fmt.Printf("The %d is %s\n" , i, v) }
P.S. 没有初始化的索引,其默认值都是数组类型的零值
slice 切片 切片是基于数组实现的,它的底层就是一个数组。对数组任意分隔,就可以得到一个切片。切片和数组类似,可以把它理解为动态数组 。
切片特性 左开右闭 、从1开始
1 2 3 4 array := [5 ]string {"a" , "b" , "c" , "d" , "e" } slice := array[2 :5 ] fmt.Println(slice) fmt.Println(len (slice))
省略开始下标:
1 2 3 4 array := [5 ]string {"a" , "b" , "c" , "d" , "e" } slice := array[:5 ] fmt.Println(slice) fmt.Println(len (slice))
省略结束下标:
1 2 3 4 array := [5 ]string {"a" , "b" , "c" , "d" , "e" } slice := array[1 :] fmt.Println(slice) fmt.Println(len (slice))
全部省略:
1 2 3 4 array := [5 ]string {"a" , "b" , "c" , "d" , "e" } slice := array[:] fmt.Println(slice) fmt.Println(len (slice))
可更改切片的值:
1 2 3 4 5 6 7 8 9 10 11 array := [5 ]string {"a" , "b" , "c" , "d" , "e" } fmt.Println(array) fmt.Println(len (array)) slice := array[2 :] slice[0 ] = "f" fmt.Println(slice) fmt.Println(len (slice)) fmt.Println(array) fmt.Println(len (array))
P.S. 切片的值更改会影响原数组的值
切片声明 make()
声明:
1 2 3 4 5 6 7 8 9 slice := make ([]string , 4 ) fmt.Println(slice) fmt.Println(len (slice)) fmt.Println(cap (slice)) slice2 := make ([]string , 4 , 8 ) fmt.Println(slice2) fmt.Println(len (slice2)) fmt.Println(cap (slice2))
P.S. 第二种声明指定了数组长度为4,容量为8。
切片增加 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 slice := make ([]string , 4 ) fmt.Println(slice) fmt.Println(len (slice)) fmt.Println(cap (slice)) slice2 := make ([]string , 4 , 8 ) fmt.Println(slice2) fmt.Println(len (slice2)) fmt.Println(cap (slice2)) strings := append (slice2, "z" , "x" , "v" , "b" , "n" ) fmt.Println(slice2) fmt.Println(len (slice2)) fmt.Println(cap (slice2)) fmt.Println(strings) fmt.Println(len (strings)) fmt.Println(cap (strings))
P.S. 切片追加元素,返回的是新切片 ,超过容量会自动扩容 。在创建新切片的时候,最好要让新切片的长度和容量一样 ,这样在追加操作的时候就会生成新的底层数组,从而和原有数组分离,就不会因为共用底层数组导致修改内容的时候影响多个切片。在 Go 语言开发中,切片是使用最多的,尤其是作为函数的参数时,相比数组,通常会优先选择切片,因为它高效 ,内存占用小 。
map 映射 map[K]V
,Key 的类型必须支持 ==
比较运算符,这样才可以判断它是否存在,并保证 Key 的唯一。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 nameAgeMap := make (map [string ]int ) nameAgeMap["leopold" ] = 18 fmt.Println(nameAgeMap) fmt.Println(nameAgeMap["leopold" ]) fmt.Println(nameAgeMap["fitz" ]) age, contains := nameAgeMap["james" ] if contains { fmt.Println(age) } else { fmt.Println("james doesn't exist" ) } for k, v := range nameAgeMap { fmt.Printf("The key is %s, value is %d\n" , k, v) } for k := range nameAgeMap { fmt.Printf("The key is %s\n" , k) } fmt.Println(len (nameAgeMap)) delete (nameAgeMap, "james" )fmt.Println(nameAgeMap) delete (nameAgeMap, "leopold" )fmt.Println(nameAgeMap)
P.S. 1.age
是map
的值,contains
是key是否存在
。由于key
不存在会返回零值,所以要先判断key
是否存在 。删除不存在的键不会报错 。2. for range map
的时候,也可以使用一个值返回。使用一个返回值的时候,这个返回值默认是 map
的 Key
。
string 和 []byte 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 name := "Leopold测试" fmt.Println(name) fmt.Println(len (name)) fmt.Println(utf8.RuneCountInString(name)) fmt.Println(name[0 ], name[1 ], name[11 ]) for i, b := range name { fmt.Println(i, b) } firstName := []byte (name) fmt.Println(firstName) fmt.Println(len (firstName)) fmt.Println(firstName[0 ], firstName[1 ], firstName[11 ]) for i, b := range firstName { fmt.Println(i, b) }
P.S.
一个汉字占3个字节,所以len(firstName)
的结果为13
for range
自动调用utf8.RuneCountInString
方法,所以对于字符串而言,for range name
只循环了9
次
<br/ >
函数 普通函数 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 package mainimport ( "errors" "fmt" "strconv" ) func main () { result := myPrint(1 , 2 ) fmt.Println(result) myPrint2(1 , 2 ) res2, err2 := myPrint3(-1 , 2 ) if err2 != nil { fmt.Println(err2) } else { fmt.Println(res2) } res3, _ := myPrint3(-1 , 2 ) fmt.Println(res3) res4, _ := myPrint4(7 , 8 ) fmt.Println(res4) fmt.Println(myPrint5()) fmt.Println(myPrint5(1 )) fmt.Println(myPrint5(1 , 2 , 3 )) } func myPrint (a, b int ) string { return strconv.Itoa(a + b) } func myPrint2 (a, b int ) { strconv.Itoa(a + b) } func myPrint3 (a, b int ) (int , error ) { if a < 0 || b < 0 { return 0 , errors.New("参数必须大于0" ) } return a + b, nil } func myPrint4 (a, b int ) (result int , err error ) { if a < 0 || b < 0 { return 0 , errors.New("参数必须大于0" ) } result = a + b err = nil return } func myPrint5 (param ...int ) int { sum := 0 for _, i := range param { sum += i } return sum }
包级函数 /test/test.go
1 2 3 4 5 6 7 8 9 10 11 package testfunc MyPrint6 (a, b int ) int { return a + b } func myPrint7 (a, b int ) int { return a + b }
/main.go
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 package mainimport ( "fmt" "go-learn/test" ) func main () { fmt.Println(test.MyPrint6(1 , 2 )) fmt.Println(test.MyPrint7(1 , 2 )) fmt.Println(test.MyPrint8(1 , 2 )) }
/test/test2.go
1 2 3 4 5 6 package testfunc MyPrint8 (a, b int ) int { return myPrint7(a, b) }
匿名函数和闭包 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 package mainimport "fmt" func main () { c := 3 sum := func (a, b int ) int { result := a + b return result + c } fmt.Println(sum(1 , 2 )) }
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 func main () { cl:=colsure() fmt.Println(cl()) fmt.Println(cl()) fmt.Println(cl()) } func colsure () func () int { i:=0 return func () int { i++ return i } }
方法 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 package mainimport "fmt" func main () { age := Age(25 ) age.String() } type Age uint func (age Age) String() { fmt.Println("the age is" , age) }
指针类型接收函数 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 package mainimport "fmt" func main () { age := Age(25 ) age.String() age.Modify() age.String() } type Age uint func (age Age) String() { fmt.Println("the age is" , age) } func (age *Age) Modify() { *age = Age(30 ) }
工厂函数 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 package mainimport ( "fmt" "strings" ) type person struct { name string age int } func GeneratePerson (name string ) *person { return &person{ name: name, age: 18 , } } func (p *person) GetFormatName() string { return strings.ToUpper(p.name) } func main () { p := GeneratePerson("leopold" ) fmt.Println(p.name) fmt.Println(p.GetFormatName()) }
Deferred 函数 在一个自定义函数中,你打开了一个文件,然后需要关闭它以释放资源。不管你的代码执行了多少分支,是否出现了错误,文件是一定要关闭的,这样才能保证资源的释放。
如果这个事情由开发人员来做,随着业务逻辑的复杂会变得非常麻烦,而且还有可能会忘记关闭。基于这种情况,Go 语言为我们提供了 defer 函数,可以保证文件关闭后一定会被执行,不管你自定义的函数出现异常还是错误。
下面的代码是 Go 语言标准包 ioutil 中的 ReadFile 函数,它需要打开一个文件,然后通过 defer 关键字确保在 ReadFile 函数执行结束后,f.Close() 方法被执行,这样文件的资源才一定会释放。
1 2 3 4 5 6 7 8 9 func ReadFile (filename string ) ([]byte , error ) { f, err := os.Open(filename) if err != nil { return nil , err } defer f.Close() return readAll(f, n) }
defer 关键字用于修饰一个函数或者方法,使得该函数或者方法在返回前才会执行,也就说被延迟,但又可以保证一定会执行。
以上面的 ReadFile 函数为例,被 defer 修饰的 f.Close 方法延迟执行,也就是说会先执行 readAll(f, n),然后在整个 ReadFile 函数 return 之前执行 f.Close 方法。
defer 语句常被用于成对的操作,如文件的打开和关闭,加锁和释放锁,连接的建立和断开等。不管多么复杂的操作,都可以保证资源被正确地释放。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 package mainimport "fmt" func testDefer () { defer fmt.Println("1" ) defer fmt.Println("2" ) defer fmt.Println("3" ) fmt.Println("4" ) } func main () { testDefer() }
结构体 1 2 3 4 5 type structName struct { fieldName typeName .... .... }
type 和 struct 是 Go 语言的关键字,二者组合就代表要定义一个新的结构体类型。
structName 是结构体类型的名字。
fieldName 是结构体的字段名,而 typeName 是对应的字段类型。
字段可以是零个、一个或者多个。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 package mainimport "fmt" type person struct { name string age int homeMsg home } type home struct { address string number int } func main () { var p person fmt.Printf("p.name -> %s, p.age -> %d\n" , p.name, p.age) p1 := person{name: "leopold" } fmt.Printf("p1.name -> %s, p1.age -> %d\n" , p1.name, p1.age) p2 := person{"leopold" , 18 , home{"The earth" , 40 }} fmt.Printf("p2.name -> %s, p2.age -> %d, p2.homeMsg.address -> %s, p2.homeMsg.number -> %d\n" , p2.name, p2.age, p2.homeMsg.address, p2.homeMsg.number) p3 := person{ age: 19 , name: "leopold" , homeMsg: home{ number: 41 , address: "The moon" , }} fmt.Printf("p3.name -> %s, p3.age -> %d, p3.homeMsg.address -> %s, p3.homeMsg.number -> %d\n" , p3.name, p3.age, p3.homeMsg.address, p3.homeMsg.number) }
接口 定义接口 test.go
1 2 3 4 5 6 7 8 9 10 11 12 13 package testimport "fmt" type IString interface { MyString() string } func PrintString (s IString) { fmt.Println(s.MyString()) }
main.go
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 package mainimport ( "fmt" "go-learn/test" ) type person struct { name string age int } func (p person) MyString() string { return fmt.Sprintf("The name is %s, age is %d" , p.name, p.age) } func main () { p := person{age: 18 , name: "leopold" } test.PrintString(p) test.PrintString(&p) }
继承与组合 在 Go 语言中没有继承的概念,所以结构、接口之间也没有父子关系,Go 语言提倡的是组合,利用组合达到代码复用的目的,这也更灵活。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 package testtype IString interface { MyString() string } type IUpper interface { MyUpper() string } type IStringUpper interface { IString IUpper }
类型断言 有了接口和实现接口的类型,就会有类型断言。类型断言用来判断一个接口的值是否是实现该接口的某个具体类型。
还是以我们上面小节的示例演示,我们先来回忆一下它们,如下所示:
1 2 3 4 5 6 7 func (p *person) String() string { return fmt.Sprintf("the name is %s,age is %d" ,p.name,p.age) } func (addr address) String() string { return fmt.Sprintf("the addr is %s%s" ,addr.province,addr.city) }
可以看到,*person 和 address 都实现了接口 Stringer,然后我通过下面的示例讲解类型断言:
1 2 3 4 var s fmt.Stringers = p1 p2:=s.(*person) fmt.Println(p2)
如上所示,接口变量 s 称为接口 fmt.Stringer 的值,它被 p1 赋值。然后使用类型断言表达式 s.(person),尝试返回一个 p2。如果接口的值 s 是一个 person,那么类型断言正确,可以正常返回 p2。如果接口的值 s 不是一个 *person,那么在运行时就会抛出异常,程序终止运行。
小提示:这里返回的 p2 已经是 *person 类型了,也就是在类型断言的时候,同时完成了类型转换。
在上面的示例中,因为 s 的确是一个 *person,所以不会异常,可以正常返回 p2。但是如果我再添加如下代码,对 s 进行 address 类型断言,就会出现一些问题:
1 2 a:=s.(address) fmt.Println(a)
这个代码在编译的时候不会有问题,因为 address 实现了接口 Stringer,但是在运行的时候,会抛出如下异常信息:
1 panic: interface conversion: fmt.Stringer is *main.person, not main.address
这显然不符合我们的初衷,我们本来想判断一个接口的值是否是某个具体类型,但不能因为判断失败就导致程序异常。考虑到这点,Go 语言为我们提供了类型断言的多值返回,如下所示:
1 2 3 4 5 6 a,ok:=s.(address) if ok { fmt.Println(a) }else { fmt.Println("s不是一个address" ) }
类型断言返回的第二个值“ok”就是断言是否成功的标志,如果为 true 则成功,否则失败。
自定义Error 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 package mainimport "fmt" type commonError struct { code int msg string } func (ce *commonError) Error() string { return ce.msg } func add (a, b int ) (int , error ) { if a < 0 || b < 0 { return 0 , &commonError{ code: 500 , msg: "must > 0" , } } else { return a + b, nil } } func main () { i, err := add(-1 , 2 ) if cm, ok := err.(*commonError); ok { fmt.Printf("Error: The code is %d, error msg is %s\n" , cm.code, cm.msg) } else { fmt.Printf("Result is %d\n" , i) } }
协程(Goroutine) Go 语言中没有线程的概念,只有协程,也称为 goroutine。相比线程来说,协程更加轻量,一个程序可以随意启动成千上万个 goroutine。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 package mainimport ( "fmt" "time" ) func main () { go fmt.Println("leopold goroutine" ) fmt.Println("我是 main goroutine" ) time.Sleep(time.Second) }
从输出结果也可以看出,程序是并发的,go 关键字启动的 goroutine 并不阻塞 main goroutine 的执行,所以我们才会看到如上打印结果。
小提示:示例中的 time.Sleep(time.Second) 表示等待一秒,这里是让 main goroutine 等一秒,不然 main goroutine 执行完毕程序就退出了,也就看不到启动的新 goroutine 中“leopold goroutine”的打印结果了。
Channel 在 Go 语言中,提倡通过通信来共享内存,而不是通过共享内存来通信,其实就是提倡通过 channel 发送接收消息的方式进行数据传递,而不是通过修改同一个变量。所以在数据流动、传递的场景中要优先使用 channel,它是并发安全的,性能也不错。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 package mainimport ( "fmt" ) func main () { ch := make (chan string ) go func () { fmt.Println("leopold goroutine" ) ch <- "goroutine 完成" }() fmt.Println("我是 main goroutine" ) v := <-ch fmt.Println("接收到的chan中的值为:" , v) }
无缓冲 channel 上面的示例中,使用 make
创建的 chan 就是一个无缓冲 channel
,它的容量是 0 ,不能存储任何数据 。所以无缓冲 channel 只起到传输数据 的作用,数据并不会在 channel 中做任何停留。这也意味着,无缓冲 channel 的发送和接收操作是同时进行的,它也可以称为同步 channel 。
有缓冲 channel 有缓冲 channel 类似一个可阻塞的队列,内部的元素先进先出。通过 make 函数的第二个参数可以指定 channel 容量的大小,进而创建一个有缓冲 channel,如cacheCh:=make(chan int,5)
一个有缓冲 channel 具备以下特点:
有缓冲 channel 的内部有一个缓冲队列;
发送操作是向队列的尾部插入元素,如果队列已满,则阻塞等待,直到另一个 goroutine 执行,接收操作释放队列的空间;
接收操作是从队列的头部获取元素并把它从队列中删除,如果队列为空,则阻塞等待,直到另一个 goroutine 执行,发送操作插入新的元素。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 package mainimport "fmt" func main () { cacheCh := make (chan int , 5 ) cacheCh <- 2 cacheCh <- 3 fmt.Println("cacheCh容量为:" , cap (cacheCh), ",元素个数为:" , len (cacheCh)) close (cacheCh) cacheCh <- 4 }
单向 channel 有时候,我们有一些特殊的业务需求,比如限制一个 channel 只可以接收但是不能发送,或者限制一个 channel 只能发送但不能接收,这种 channel 称为单向 channel。
单向 channel 的声明也很简单,只需要在声明的时候带上 <- 操作符即可,如下面的代码所示:
1 2 onlySend := make (chan <- int ) onlyReceive:=make (<-chan int )
注意,声明单向 channel <- 操作符的位置和上面讲到的发送和接收操作是一样的。
在函数或者方法的参数中,使用单向 channel 的较多,这样可以防止一些操作影响了 channel。
下面示例中的 counter 函数,它的参数 out 是一个只能发送的 channel,所以在 counter 函数体内使用参数 out 时,只能对其进行发送操作,如果执行接收操作,则程序不能编译通过。
1 2 3 func counter (out chan <- int ) { }
select+channel 示例1 假设要从网上下载一个文件,我启动了 3 个 goroutine 进行下载,并把结果发送到 3 个 channel 中。其中,哪个先下载好,就会使用哪个 channel 的结果。
在这种情况下,如果我们尝试获取第一个 channel 的结果,程序就会被阻塞,无法获取剩下两个 channel 的结果,也无法判断哪个先下载好。这个时候就需要用到多路复用操作了,在 Go 语言中,通过 select 语句可以实现多路复用,其语句格式如下:
1 2 3 4 5 6 7 8 select { case i1 = <-c1: case c2 <- i2: default : }
整体结构和 switch 非常像,都有 case 和 default,只不过 select 的 case 是一个个可以操作的 channel。
小提示:多路复用可以简单地理解为,N 个 channel 中,任意一个 channel 有数据产生,select 都可以监听到,然后执行相应的分支,接收数据并处理。
有了 select 语句,就可以实现下载的例子了。如下面的代码所示:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 func main () { firstCh := make (chan string ) secondCh := make (chan string ) threeCh := make (chan string ) go func () { firstCh <- downloadFile("firstCh" ) }() go func () { secondCh <- downloadFile("secondCh" ) }() go func () { threeCh <- downloadFile("threeCh" ) }() select { case filePath := <-firstCh: fmt.Println(filePath) case filePath := <-secondCh: fmt.Println(filePath) case filePath := <-threeCh: fmt.Println(filePath) } } func downloadFile (chanName string ) string { time.Sleep(time.Second) return chanName+":filePath" }
如果这些 case 中有一个可以执行,select 语句会选择该 case 执行,如果同时有多个 case 可以被执行,则随机选择一个,这样每个 case 都有平等的被执行的机会。如果一个 select 没有任何 case,那么它会一直等待下去。
并发控制 没有并发控制 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 package mainimport ( "fmt" "time" ) var sum = 0 func main () { for i := 0 ; i < 100 ; i++ { go add(10 ) } time.Sleep(2 * time.Second) fmt.Println(sum) } func add (a int ) { sum += a }
互斥锁 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 package mainimport ( "fmt" "sync" "time" ) var ( sum = 0 mutex sync.Mutex ) func main () { for i := 0 ; i < 100 ; i++ { go add(10 ) } time.Sleep(2 * time.Second) fmt.Println(sum) } func add (a int ) { mutex.Lock() defer mutex.Unlock() sum += a }
读写锁 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 package commonimport ( "sync" ) type SynchronizedMap struct { rw *sync.RWMutex data map [interface {}]interface {} } func (sm *SynchronizedMap) Put(k, v interface {}) { sm.rw.Lock() defer sm.rw.Unlock() sm.data[k] = v } func (sm *SynchronizedMap) Get(k interface {}) interface {} { sm.rw.RLock() defer sm.rw.RUnlock() return sm.data[k] } func (sm *SynchronizedMap) Delete(k interface {}) { sm.rw.Lock() defer sm.rw.Unlock() delete (sm.data, k) } func (sm *SynchronizedMap) Each(cb func (interface {}, interface {}) ) { sm.rw.RLock() defer sm.rw.RUnlock() for k, v := range sm.data { cb(k, v) } } func NewSynchronizedMap () *SynchronizedMap { return &SynchronizedMap{ rw: new (sync.RWMutex), data: make (map [interface {}]interface {}), } }
等待锁 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 package mainimport ( "fmt" "sync" "time" ) func main () { race() } func race () { cond := sync.NewCond(&sync.Mutex{}) var wg sync.WaitGroup wg.Add(11 ) for i := 0 ; i < 10 ; i++ { go func (num int ) { fmt.Println(num, "号已经就位" ) cond.L.Lock() cond.Wait() fmt.Println(num, "号开始跑……" ) cond.L.Unlock() wg.Done() }(i) } time.Sleep(2 * time.Second) go func () { fmt.Println("裁判已经就位,准备发令枪" ) fmt.Println("比赛开始,大家准备跑" ) cond.Broadcast() wg.Done() }() wg.Wait() }
Context 前言:Select channel 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 package mainimport ( "fmt" "sync" "time" ) var wg sync.WaitGroupfunc main () { wg.Add(1 ) flag := make (chan bool ) go func () { defer wg.Done() watch("init" , flag) }() time.Sleep(time.Second * 5 ) flag <- true wg.Wait() } func watch (str string , flag chan bool ) { for { select { case <-flag: fmt.Println("stop" ) return default : fmt.Printf("str的值为 -> %s\n" , str) } time.Sleep(time.Second) } }
结果:
1 2 3 4 5 6 str的值为 -> init str的值为 -> init str的值为 -> init str的值为 -> init str的值为 -> init stop
通过 select+channel 让协程退出的方式比较优雅,但是如果我们希望做到同时取消很多个协程呢?如果是定时取消协程又该怎么办?这时候 select+channel 的局限性就凸现出来了,即使定义了多个 channel 解决问题,代码逻辑也会非常复杂、难以维护。
要解决这种复杂的协程问题,必须有一种可以跟踪协程的方案,只有跟踪到每个协程,才能更好地控制它们,这种方案就是 Go 语言标准库为我们提供的 Context
Context 一个任务会有很多个协程协作完成,一次 HTTP 请求也会触发很多个协程的启动,而这些协程有可能会启动更多的子协程,并且无法预知有多少层协程、每一层有多少个协程。
如果因为某些原因导致任务终止了,HTTP 请求取消了,那么它们启动的协程怎么办?该如何取消呢?因为取消这些协程可以节约内存,提升性能,同时避免不可预料的 Bug。
Context 就是用来简化解决这些问题的,并且是并发安全的。Context 是一个接口,它具备手动、定时、超时发出取消信号、传值等功能,主要用于控制多个协程之间的协作,尤其是取消操作。一旦取消指令下达,那么被 Context 跟踪的这些协程都会收到取消信号,就可以做清理和退出操作。
Context 接口只有四个方法,下面进行详细介绍,在开发中你会经常使用它们,你可以结合下面的代码来看。
1 2 3 4 5 6 7 8 9 10 11 type Context interface { Deadline() (deadline time.Time, ok bool ) Done() <-chan struct {} Err() error Value(key interface {}) interface {} }
Deadline 方法可以获取设置的截止时间,第一个返回值 deadline 是截止时间,到了这个时间点,Context 会自动发起取消请求,第二个返回值 ok 代表是否设置了截止时间。
Done 方法返回一个只读的 channel,类型为 struct{}。在协程中,如果该方法返回的 chan 可以读取,则意味着 Context 已经发起了取消信号。通过 Done 方法收到这个信号后,就可以做清理操作,然后退出协程,释放资源。
Err 方法返回取消的错误原因,即因为什么原因 Context 被取消。
Value 方法获取该 Context 上绑定的值,是一个键值对,所以要通过一个 key 才可以获取对应的值。
Context 接口的四个方法中最常用的就是 Done 方法,它返回一个只读的 channel,用于接收取消信号。当 Context 取消的时候,会关闭这个只读 channel,也就等于发出了取消信号。
Context 树 我们不需要自己实现 Context 接口,Go 语言提供了函数可以帮助我们生成不同的 Context,通过这些函数可以生成一颗 Context 树,这样 Context 才可以关联起来,父 Context 发出取消信号的时候,子 Context 也会发出,这样就可以控制不同层级的协程退出。
从使用功能上分,有四种实现好的 Context。
空 Context :不可取消,没有截止时间,主要用于 Context 树的根节点。
可取消的 Context :用于发出取消信号,当取消的时候,它的子 Context 也会取消。
可定时取消的 Context :多了一个定时的功能。
值 Context :用于存储一个 key-value 键值对。
从下图 Context 的衍生树可以看到,最顶部的是空 Context,它作为整棵 Context 树的根节点,在 Go 语言中,可以通过 context.Background() 获取一个根节点 Context。
(四种 Context 的衍生树)
有了根节点 Context 后,这颗 Context 树要怎么生成呢?需要使用 Go 语言提供的四个函数。
**WithCancel(parent Context)**:生成一个可取消的 Context。
**WithDeadline(parent Context, d time.Time)**:生成一个可定时取消的 Context,参数 d 为定时取消的具体时间。
**WithTimeout(parent Context, timeout time.Duration)**:生成一个可超时取消的 Context,参数 timeout 用于设置多久后取消
**WithValue(parent Context, key, val interface{})**:生成一个可携带 key-value 键值对的 Context。
以上四个生成 Context 的函数中,前三个都属于可取消的 Context,它们是一类函数,最后一个是值 Context,用于存储一个 key-value 键值对。
使用 Context 取消多个协程 取消多个协程也比较简单,把 Context 作为参数传递给协程即可,还是以监控狗为例,如下所示:
ch10/main.go
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 wg.Add(3 ) go func () { defer wg.Done() watchDog(ctx,"【监控狗2】" ) }() go func () { defer wg.Done() watchDog(ctx,"【监控狗3】" ) }()
示例中增加了两个监控狗,也就是增加了两个协程,这样一个 Context 就同时控制了三个协程,一旦 Context 发出取消信号,这三个协程都会取消退出。
以上示例中的 Context 没有子 Context,如果一个 Context 有子 Context,在该 Context 取消时会发生什么呢?下面通过一幅图说明:
(Context 取消)
可以看到,当节点 Ctx2 取消时,它的子节点 Ctx4、Ctx5 都会被取消,如果还有子节点的子节点,也会被取消。也就是说根节点为 Ctx2 的所有节点都会被取消,其他节点如 Ctx1、Ctx3 和 Ctx6 则不会。
Context 传值 Context 不仅可以取消,还可以传值,通过这个能力,可以把 Context 存储的值供其他协程使用。我通过下面的代码来说明:
ch10/main.go
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 func main () { wg.Add(4 ) valCtx:=context.WithValue(ctx,"userId" ,2 ) go func () { defer wg.Done() getUser(valCtx) }() } func getUser (ctx context.Context) { for { select { case <-ctx.Done(): fmt.Println("【获取用户】" ,"协程退出" ) return default : userId:=ctx.Value("userId" ) fmt.Println("【获取用户】" ,"用户ID为:" ,userId) time.Sleep(1 * time.Second) } } }
这个示例是和上面的示例放在一起运行的,所以我省略了上面实例的重复代码。其中,通过 context.WithValue 函数存储一个 userId 为 2 的键值对,就可以在 getUser 函数中通过 ctx.Value(“userId”) 方法把对应的值取出来,达到传值的目的。
Context 使用原则 Context 是一种非常好的工具,使用它可以很方便地控制取消多个协程。在 Go 语言标准库中也使用了它们,比如 net/http 中使用 Context 取消网络的请求。
要更好地使用 Context,有一些使用原则需要尽可能地遵守。
Context 不要放在结构体中,要以参数的方式传递。
Context 作为函数的参数时,要放在第一位,也就是第一个参数。
要使用 context.Background 函数生成根节点的 Context,也就是最顶层的 Context。
Context 传值要传递必须的值,而且要尽可能地少,不要什么都传。
Context 多协程安全,可以在多个协程中放心使用。
以上原则是规范类的,Go 语言的编译器并不会做这些检查,要靠自己遵守。
流水线Pipeline Pipeline 模式也称为流水线模式,模拟的就是现实世界中的流水线生产。以手机组装为例,整条生产流水线可能有成百上千道工序,每道工序只负责自己的事情,最终经过一道道工序组装,就完成了一部手机的生产。
从技术上看,每一道工序的输出,就是下一道工序的输入,在工序之间传递的东西就是数据,这种模式称为流水线模式,而传递的数据称为数据流。
以组装一个iPhone为例,包含三道工序,采购、组装和打包
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 package mainimport "fmt" func buy (n int ) <-chan string { out := make (chan string ) go func () { defer close (out) for i := 1 ; i <= n; i++ { out <- fmt.Sprint("配件" , i) } }() return out } func build (in <-chan string ) <-chan string { out := make (chan string ) go func () { defer close (out) for c := range in { out <- "正在组装(" + c + ")" } }() return out } func pack (in <-chan string ) <-chan string { out := make (chan string ) go func () { defer close (out) for c := range in { out <- "正在打包(" + c + ")" } }() return out } func main () { buys := buy(10 ) builds := build(buys) packs := pack(builds) for c := range packs { fmt.Println(c) } }
扇出和扇入 如果组装的太慢,那就要增大组装并行个数。
不难发现,整个工序我们只需要再增加一个merge
就可以了。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 package mainimport ( "fmt" "sync" ) func buy (n int ) <-chan string { out := make (chan string ) go func () { defer close (out) for i := 1 ; i <= n; i++ { out <- fmt.Sprint("配件" , i) } }() return out } func build (in <-chan string ) <-chan string { out := make (chan string ) go func () { defer close (out) for c := range in { out <- "正在组装(" + c + ")" } }() return out } func pack (in <-chan string ) <-chan string { out := make (chan string ) go func () { defer close (out) for c := range in { out <- "正在打包(" + c + ")" } }() return out } func merge (ins ...<-chan string ) <-chan string { var wg sync.WaitGroup wg.Add(len (ins)) out := make (chan string ) p := func (in <-chan string ) { defer wg.Done() for c := range in { out <- c } } for _, cs := range ins { go p(cs) } go func () { wg.Wait() close (out) }() return out } func main () { buys := buy(10 ) builds1 := build(buys) builds2 := build(buys) builds3 := build(buys) buildMerge := merge(builds1, builds2, builds3) packs := pack(buildMerge) for c := range packs { fmt.Println(c) } }
新增的 merge 函数的核心逻辑就是对输入的每个 channel 使用单独的协程处理,并将每个协程处理的结果都发送到变量 out 中,达到扇入的目的。总结起来就是通过多个协程并发,把多个 channel 合成一个。
在整条手机组装流水线中,merge 函数非常小,而且和业务无关,不能当作一道工序,所以我把它叫作组件 。该 merge 组件是可以复用的,流水线中的任何工序需要扇入的时候,都可以使用 merge 组件。
小提示:这次的改造新增了 merge 函数,其他函数保持不变,符合开闭原则。开闭原则规定“软件中的对象(类,模块,函数等等)应该对于扩展是开放的,但是对于修改是封闭的”。
Futures 模式 Pipeline 流水线模式中的工序是相互依赖的,上一道工序做完,下一道工序才能开始。但是在我们的实际需求中,也有大量的任务之间相互独立、没有依赖,所以为了提高性能,这些独立的任务就可以并发执行。
Futures 模式可以理解为未来模式,主协程不用等待子协程返回的结果,可以先去做其他事情,等未来需要子协程结果的时候再来取,如果子协程还没有返回结果,就一直等待。
现在我们细化一下购买配件的步骤:购买屏幕、购买膜具。这两个购买的工序是可以并行的,但必须要等到屏幕和磨具都买齐了才能去组装。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 package mainimport ( "fmt" "sync" "time" ) func screen () <-chan string { out := make (chan string ) go func () { defer close (out) fmt.Println("正在采购屏幕ing" ) time.Sleep(time.Second * 1 ) out <- "屏幕" }() return out } func mold () <-chan string { out := make (chan string ) go func () { defer close (out) fmt.Println("正在模具屏幕ing" ) time.Sleep(time.Second * 1 ) out <- "模具" }() return out } func buy (n int ) <-chan string { out := make (chan string ) go func () { defer close (out) for i := 1 ; i <= n; i++ { screenCh := screen() moldCh := mold() out <- fmt.Sprintf("配件(【%s + %s】%d)" , <-screenCh, <-moldCh, i) } }() return out } func build (in <-chan string ) <-chan string { out := make (chan string ) go func () { defer close (out) for c := range in { out <- "正在组装(" + c + ")" } }() return out } func pack (in <-chan string ) <-chan string { out := make (chan string ) go func () { defer close (out) for c := range in { out <- "正在打包(" + c + ")" } }() return out } func merge (ins ...<-chan string ) <-chan string { var wg sync.WaitGroup wg.Add(len (ins)) out := make (chan string ) p := func (in <-chan string ) { defer wg.Done() for c := range in { out <- c } } for _, cs := range ins { go p(cs) } go func () { wg.Wait() close (out) }() return out } func main () { buys := buy(10 ) builds1 := build(buys) builds2 := build(buys) builds3 := build(buys) buildMerge := merge(builds1, builds2, builds3) packs := pack(buildMerge) for c := range packs { fmt.Println(c) } }
Futures 模式下的协程和普通协程最大的区别是可以返回结果,而这个结果会在未来的某个时间点使用。所以在未来获取这个结果的操作必须是一个阻塞的操作,要一直等到获取结果为止。
如果你的大任务可以拆解为一个个独立并发执行的小任务,并且可以通过这些小任务的结果得出最终大任务的结果,就可以使用 Futures 模式。
指针 定义 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 package mainimport "fmt" func change (a int ) { a = 20 } func change2 (a *int ) { *a = 20 } func main () { a := 18 change(a) fmt.Println(a) b := 19 change2(&b) fmt.Println(b) }
不要对 map
、slice
、channel
这类引用类型 使用指针;
如果需要修改方法接收者内部的数据或者状态 时,需要使用指针;
如果需要修改参数的值 或者内部数据 时,也需要使用指针类型的参数;
如果是比较大的结构体 ,每次参数传递或者调用方法都要内存拷贝,内存占用多 ,这时候可以考虑使用指针;
像 int
、bool
这样的小数据类型没必要使用指针;
如果需要并发安全 ,则尽可能地不要使用指针 ,使用指针一定要保证并发安全;
指针最好不要嵌套 ,也就是不要使用一个指向指针的指针,虽然 Go 语言允许这么做,但是这会使你的代码变得异常复杂。
new
和 make
的区别new 函数只用于分配内存 ,并且把内存清零,也就是返回一个指向对应类型零值的指针 。new 函数一般用于需要显式地返回指针的情况,不是太常用。
make 函数只用于 slice
、chan
和 map
这三种内置类型的创建和初始化,因为这三种类型的结构比较复杂,比如 slice 要提前初始化好内部元素的类型,slice 的长度和容量等,这样才可以更好地使用它们。
反射 在 Go 反射中,标准库为我们提供了两种类型 reflect.Value
和 reflect.Type
来分别表示变量的值和类型,并且提供了两个函数 reflect.ValueOf()
和 reflect.TypeOf()
分别获取任意对象的 reflect.Value
和 reflect.Type
。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 package mainimport ( "fmt" "reflect" ) type person struct { Name string address string age int } func (p person) MyString() string { return fmt.Sprintf(p.Name, p.address, p.age) } func (p person) myString() string { return fmt.Sprintf(p.Name, p.address, p.age) } func main () { i := 3 irt := reflect.TypeOf(i) irv := reflect.ValueOf(i) fmt.Println(irt) fmt.Println(irv) i1 := irv.Interface().(int ) fmt.Println(i1) irv2 := reflect.ValueOf(&i) irv2.Elem().SetInt(4 ) fmt.Println(i) p := person{Name: "leopold" , address: "earth" , age: 18 } p1 := reflect.ValueOf(&p) p1.Elem().Field(0 ).SetString("leopold fitz" ) fmt.Println(p) typeP := reflect.TypeOf(p) for i := 0 ; i < typeP.NumField(); i++ { fmt.Println("字段:" , typeP.Field(i).Name) } for i := 0 ; i < typeP.NumMethod(); i++ { fmt.Println("方法:" , typeP.Method(i).Name) } }
反射三大定律:
任何接口值 interface{} 都可以反射出反射对象,也就是 reflect.Value 和 reflect.Type,通过函数 reflect.ValueOf 和 reflect.TypeOf 获得。
反射对象也可以还原为 interface{} 变量,也就是第 1 条定律的可逆性,通过 reflect.Value 结构体的 Interface 方法获得。
要修改反射的对象,该值必须可设置,也就是可寻址。
小提示:任何类型的变量都可以转换为空接口 intferface{},所以第 1 条定律中函数 reflect.ValueOf 和 reflect.TypeOf 的参数就是 interface{},表示可以把任何类型的变量转换为反射对象。在第 2 条定律中,reflect.Value 结构体的 Interface 方法返回的值也是 interface{},表示可以把反射对象还原为对应的类型变量。