[关闭]
@dungan 2022-11-29T09:26:54.000000Z 字数 43676 阅读 543

Go

Go 基础

编码约定

包导入

  1. import (
  2. // 导入内置包
  3. "fmt"
  4. "log"
  5. "net/http"
  6. "time"
  7. // 导入当前项目下的包
  8. "http/sessionManager"
  9. "http/sessionProvider"
  10. //匿名导入
  11. _ "packageA" //导入不使用
  12. //内嵌导入
  13. . "packageB" //类似php的trait,内嵌一个包的方法到当前包中
  14. //别名导入
  15. "crypto/rand"
  16. mrand "math/rand" // 将名称替换为mrand避免冲突
  17. )

空白标识符 _

_ 用于忽略某个变量或返回值,或者只是执行导入包的init()函数; Go 语言要求所有变量或导入的包都必须被使用,如果你不想使用,这时候就可以使用空白标识符 _ 来忽略。

init 函数

一个包中可以有个名为 init 的函数来负责包的初始化, 相当于构造方法construct,它没有返回值,init 函数会在 main 函数之前执行, 如果导入了其他包,则先执行其他包的init函数,注意:如果同一个包内的多个 init 顺序是不受保证的。

  1. func init() {
  2. fmt.Println("execute before than main")
  3. }

数据类型

值类型

  1. boolstringcomplex64complex128array
  2. int, uintint8uint8(byte)、int16uint16int32(rune)、uint32int64uint64
  3. float32float64

引用类型

  1. slice
  2. map
  3. chan

自定义类型

使用 type 关键字,基于一个现有的类型创造新的类型称之为自定义类型 或者 类型定义

  1. type Fuck int64
  2. type T struct
  3. Type I interface

内置关键字和函数

关键字

  1. break default func interface select
  2. case defer go map struct
  3. chan else goto package switch
  4. const fallthrough if range type
  5. continue for import return var

函数

  1. append 用来追加元素到数组、slice中,返回修改后的数组、slice
  2. close 主要用来关闭channel
  3. delete map中删除key对应的value
  4. panic 停止常规的goroutine panicrecover:用来做错误处理)
  5. recover 允许程序定义goroutinepanic动作
  6. real 返回complex的实部 complexreal imag:用于创建和操作复数)
  7. imag 返回complex的虚部
  8. make 用来分配内存,返回Type本身(只能应用于slice, map, channel)
  9. new 用来分配内存,主要用来分配值类型,比如intstruct。返回指向Type的指针
  10. cap capacity是容量的意思,用于返回某个类型的最大容量(只能用于切片和 map
  11. copy 用于复制和连接slice,返回复制的数目
  12. len 获取长度,比如stringarrayslicemapchannel ,返回长度
  13. printprintln 底层打印函数,在部署环境中建议使用 fmt

变量

全局变量与局部变量

  • 全局变量:在函数体外声明的变量称之为全局变量,全局变量可以在整个包甚至外部包(被导出后)使用。
  • 局部变量:在函数体内声明的变量称之为局部变量,它们的作用域只在函数体内,参数和返回值变量也是局部变量

大多数变量的默认值是其类型的零值。nil 是 interface、function、pointer、map、slice 和 channel 类型变量的默认初始值。

变量申明

一、一行声明一个变量

  1. var name sting
  2. var title sting = "Python编程时光"

二、多个变量一起声明

  1. var (
  2. name string
  3. age int
  4. gender string
  5. )

三、短变量申明(函数特有)

  1. name := "Python编程时光"

四、声明和初始化多个变量

  1. name, age := "wangbm", 28

常量申明

常量使用 const 关键字申明,注意:常量不能用 := 语法声明。

  1. const (
  2. A = "A"
  3. B = 1
  4. )
  5. const C = true

iota是常量的计数器,从0开始,组中每定义1个常量自动递增1,每遇到一个const关键字,iota就会重置为0。

  1. package main
  2. import (
  3. "fmt"
  4. )
  5. func main() {
  6. const (
  7. A = "A"
  8. B
  9. C = iota
  10. D
  11. )
  12. const E = iota
  13. fmt.Println(A, B, C, D, E) // A A 2 3 0
  14. }

数据类型转换

类型转换时一般需要显式的使用某个类型的表达式进行转换,例如表达式 T(v) 将值 v 转换为类型 T。

  1. package main
  2. import (
  3. "fmt"
  4. "reflect"
  5. "strconv"
  6. )
  7. func main() {
  8. //同类型转换,只能在两种相互兼容的类型之间
  9. f := 3.15
  10. int_f := int(f) //浮点变整形
  11. fmt.Println(int_f)
  12. bool := true
  13. fmt.Println(int(bool)) //报错,整型无法和bool类型兼容
  14. //int 和 string 转换
  15. num := 3
  16. str := strconv.Itoa(num) // 字符串 3
  17. num, _ = strconv.Atoi(str) // int 3
  18. fmt.Println(reflect.TypeOf(str), str, num)
  19. //ascii 转换
  20. zs := 65
  21. asc := string(zs)
  22. fmt.Println(asc) // A
  23. //浮点型精度处理
  24. a := 1690 // 表示1.69
  25. b := 1700 // 表示1.70
  26. c := a * b // 结果应该是2873000表示 2.873
  27. fmt.Println(c) // 2873000
  28. fmt.Println(float64(c) / 1000000) // 2.873
  29. }

不仅可以使用内置类型进行转换,我们自定义的类型也可以进行转换

  1. package main
  2. import (
  3. "fmt"
  4. )
  5. type tcl int
  6. func main() {
  7. num := 456
  8. p := (*tcl)(&num)
  9. val, isTcl := interface{}(p).(*tcl)
  10. fmt.Println(*val, isTcl) // 456, true
  11. }

byte 和 string 转换

  1. import "fmt"
  2. func main() {
  3. // 单个字符的类型可以申明为 byte
  4. var a byte = 'A'
  5. var b uint8 = 'B'
  6. fmt.Printf("a 的值: %c \nb 的值: %c", a, b)
  7. }

由于单个 byte 代表一个ACSII字符,那多个 byte 放在一起就组成了字符串,也就是 string 类型(一堆byte组成了字符串,即 byte[]);使用 byte[]来进行默认字符串处理 ,性能和扩展性都有照顾。

  1. func main() {
  2. // string 转 byte数组
  3. byteAB := []byte("AB") // [97 98]
  4. // byte数组 转 string
  5. stringAB := string(byteAB) // AB
  6. fmt.Println(byteAB, stringAB)
  7. }

要修改字符串,需要先将其转换成[]rune或[]byte,完成后再转换为string。因为一个 UTF8 编码的字符可能会占多个字节,因此更推荐使用 []rune 来处理字符

  1. s1 := "hello"
  2. //转换为字节数组
  3. byteS1 := []byte(s1)
  4. byteS1[0] = 'H'
  5. fmt.Println(string(byteS1))
  6. s2 := "博客"
  7. //转换为字节数组
  8. runeS2 := []rune(s2)
  9. runeS2[0] = '狗'
  10. fmt.Println(string(runeS2))

函数

函数申明

go 函数可以接受多个参数,并且在函数名之后可以指出返回的类型。

  1. package main
  2. import "fmt"
  3. func add(x int, y int) int {
  4. return x + y
  5. }
  6. func main() {
  7. fmt.Println(add(42, 13))
  8. }

可变参数

如果参数数量不确定,可以通过 ...type 来定义不定参数函数。

... type 只能是位于函数参数最后,如果你希望函数的参数传任意类型,可以指定类型为 interface{}。

  1. package main
  2. import "fmt"
  3. func myFunc(args ...int) {
  4. // 使用for循环获取每个输入的参数
  5. for _, arg := range args {
  6. fmt.Println(arg)
  7. }
  8. //在内部转发参数
  9. myFunc2(args ...)
  10. }
  11. func myFunc2(args ...int){
  12. fmt.Println(args)
  13. }
  14. func main() {
  15. myFunc(2, 3, 4)
  16. }

隐式返回

如果函数体中的变量名和返回值的变量名一样,你无需显式的 return 这个变量。

  1. func (manager *Manager) SessionStart() (session Session) {
  2. ...
  3. //生成 session
  4. session, _ = manager.provider.SessionInit(sid)
  5. ...
  6. return
  7. }

多返回值

函数还可以有多个返回值。

  1. package main
  2. import "fmt"
  3. func swap(x, y string) (string, string) {
  4. return y, x
  5. }
  6. func main() {
  7. a, b := swap("hello", "world")
  8. fmt.Println(a, b)
  9. }

返回nil

nil 是任意类型的0值,当一个函数你没有要求可返回的类型,那么你就返回 nil。

  1. func (provider MemoryDriver) SessionRead(sid string) (sessionManager.Session, error) {
  2. ...
  3. return nil, nil
  4. }

回调函数

函数可以用作另一函数的参数,也就是所谓的回调函数。

  1. package main
  2. import (
  3. "fmt"
  4. "math"
  5. )
  6. func compute(x,y float64,fn func(float64, float64) float64) float64 {
  7. return fn(x, y)
  8. }
  9. func main() {
  10. hypot := func(x, y float64) float64 {
  11. return math.Sqrt(x*x + y*y)
  12. }
  13. fmt.Println(compute(3, 4, hypot))
  14. }

闭包

函数的返回值类型如果是函数,这时候就形成了闭包

  1. package main
  2. import (
  3. "fmt"
  4. )
  5. //将 【func(int) int】 看作一个整体,意为 a() 函数的返回值类型为函数
  6. func a() (func(n int) int) {
  7. // 这里的func 没有函数名,我们称之为匿名函数
  8. return func(n int) int {
  9. return n+1
  10. }
  11. }
  12. func main() {
  13. b := a()
  14. fmt.Println(b(5)) // 6
  15. }

错误处理

对于大多数函数,如果要返回错误,将 error 作为多个返回值中的最后一个。

  1. func ListPosts(...) ([]Post, error) {
  2. conn, err := gorm.Open(...)
  3. if err != nil {
  4. return []Post{}, err
  5. }
  6. var posts []Post
  7. if err := conn.Find(&posts).Error; err != nil {
  8. return []Post{}, err
  9. }
  10. return posts, nil
  11. }

当然,我们还可以生成自定义 error 信息。

  1. var ErrTimeOut = errors.New("执行者执行超时")
  2. var ErrInterrupt = errors.New("执行者被中断")
  3. var fmtError = fmt.Errorf("借助fmt生成error信息")
  4. //函数调用判断是否为error
  5. ...
  6. err := r.run()
  7. if(err != nil){
  8. // 出错了
  9. return fmtError
  10. }

defer

defer 会在当前函数返回前执行传入的函数,它会经常被用于关闭文件描述符、关闭数据库连接以及解锁资源

defer 函数会按照后进先出的顺序调用(最早定义的最后执行)。

  1. package main
  2. import "fmt"
  3. func main() {
  4. fmt.Println("counting") // 立即输出
  5. // 最先输出的是9,符合最早定义,最后执行的特点
  6. for i := 0; i < 10; i++ {
  7. defer fmt.Println(i)
  8. }
  9. fmt.Println("done") // 立即输出
  10. }

外层函数中的 return 语句会等到 defer 函数执行完毕后才会返回。

  1. package main
  2. import "fmt"
  3. func main() {
  4. fmt.Println(testDefer()) // 2
  5. }
  6. func testDefer() (n int){
  7. i := 1
  8. // 这里通过匿名的自执行函数来求值
  9. defer func(i int) {
  10. n = i + 1
  11. }(i)
  12. return n
  13. }

在 panic 抛出异常之前,会先处理完当前协程上已经defer 的任务,执行完成后再抛出。

  1. func main() {
  2. defer func() {
  3. fmt.Println("defer func")
  4. }()
  5. arr := []int{1, 2, 3}
  6. fmt.Println(arr[4])
  7. }

异常处理 panic 和 recover

这两个函数是go的异常处理函数,panic() 相当于抛出异常,recover() 相当于捕获异常。

  • panic : 用来抛出错误,该函数接收任意类型的数据,比如整型,字符串,对象等,panic 退出前会执行 defer 指定的内容。
  • recover : 用来捕获由 panic 函数抛出的错误,recover 函数应放置于函数体的最前面,并且只有在 defer 调用的函数中有效。
  1. package main
  2. import (
  3. "errors"
  4. "fmt"
  5. )
  6. func main() {
  7. err := doSomeThing()
  8. if err != nil {
  9. fmt.Println(err)
  10. } else {
  11. fmt.Println("success")
  12. }
  13. }
  14. func doSomeThing() (err error) {
  15. defer func() {
  16. if r := recover(); r != nil {
  17. //通过类型断言,将捕获的错误值转换为 string 类型,并最终将捕获的错误转换成 error类型返回给调用者
  18. str , ok := r.(string)
  19. if ok {
  20. err = errors.New(str)
  21. } else {
  22. panic("new error")
  23. }
  24. }
  25. }()
  26. panic("some error")
  27. return err
  28. }

流程控制语句

for

go 只有 for 这一种循环结构,for 是 Go 中的 "while"。需要注意 for,if,switch 都具有块级作用域,即在语句外部无法在语句中定义的变量。

  1. package main
  2. import "fmt"
  3. func main() {
  4. // for循环
  5. sum := 0
  6. for i := 0; i < 10; i++ {
  7. sum += i
  8. }
  9. fmt.Println(sum)
  10. fmt.Println(i) //报错 undefined: i
  11. // while 循环
  12. sum := 1
  13. for sum < 1000 {
  14. sum += sum
  15. }
  16. fmt.Println(sum)
  17. }

可以使用 for range 对数组,切片,map,通道等迭代。

  1. package main
  2. import (
  3. "fmt"
  4. )
  5. func main() {
  6. slice := []int{1, 2, 3, 4, 5}
  7. // 普通遍历
  8. for i, v := range slice {
  9. fmt.Println(i , v)
  10. }
  11. // 如果不想要索引,可以使用_来忽略它
  12. for _, v := range slice {
  13. fmt.Println(v)
  14. }
  15. // 如果你用一个变量来接收的话,接收到的是索引
  16. for i := range slice {
  17. fmt.Println(i)
  18. }
  19. //range也可以用在map的键值对上。
  20. kvs := map[string]string{"a": "apple", "b": "banana"}
  21. for k, v := range kvs {
  22. fmt.Printf("%s -> %s\n", k, v)
  23. }
  24. //range还可以用来枚举字符串。
  25. for i2, c := range "go" {
  26. fmt.Println(i2, string(c)) //得出的c是ASCII值
  27. }
  28. }

if

if 语句与 for 循环类似,表达式外无需小括号 ( ) ,而大括号 { } 则是必须的。

  1. func main() {
  2. a := 10
  3. if a := 1; a > 0 {
  4. a++
  5. fmt.Println(a) // 2
  6. } else if a < 0 {
  7. a--
  8. fmt.Println(a)
  9. }
  10. fmt.Println(a) // 10,说明if的a只是在局部有用
  11. }

switch

不同于其他语言中的switch语句,go 中一旦条件符合则会自动终止,不需要 break,而如果你希望继续执行下一个case,需使用 fallthrough 语句。
G另一点重要的不同在于 switch 的 case 无需为常量,且取值不必为整数。

  1. package main
  2. import (
  3. "fmt"
  4. "runtime"
  5. "time"
  6. )
  7. func main() {
  8. fmt.Print("Go runs on ")
  9. os := runtime.GOOS
  10. switch os {
  11. case "darwin":
  12. fmt.Println("OS X.")
  13. case "linux":
  14. fmt.Println("Linux.")
  15. default:
  16. fmt.Printf("%s.\n", os)
  17. }
  18. // fallthrough 语句
  19. switch a := 200; {
  20. case a == 100:
  21. fmt.Println("a=100")
  22. case a == 200:
  23. fmt.Println("a=200")
  24. fallthrough // 不退出,进入下个case语句
  25. default:
  26. fmt.Println("a=" + strconv.Itoa(a+a))
  27. }
  28. // 没有没有条件的 switch 能将一长串 if-then-else 写得更加清晰
  29. t := time.Now()
  30. switch {
  31. case t.Hour() < 12:
  32. fmt.Println("Good morning!")
  33. case t.Hour() < 17:
  34. fmt.Println("Good afternoon.")
  35. default:
  36. fmt.Println("Good evening.")
  37. }
  38. }

空语句块

Go中支持空block{},这个大括号有自己的作用域,里面的代码只执行一次,退出大括号就退出作用域。

  1. func main() {
  2. {
  3. v := 1
  4. {
  5. v := 2
  6. fmt.Println(v) // 输出2
  7. }
  8. fmt.Println(v) // 输出1
  9. }
  10. }

复合数据类型

指针

Go语言中的指针操作非常简单,只需要记住两个符号:&(取地址)*(根据地址取值),取地址操作符&和取值操作符*是一对互补操作符,&取出地址,*根据地址取出地址指向的值。

  1. package main
  2. import (
  3. "fmt"
  4. )
  5. func main() {
  6. //================= 指向变量的指针 =====================
  7. var num = 3
  8. var p *int = &num // 指针类型的变量,值必须是用 & 标识的内存地址
  9. var np *int = 3 // 报错, cannot use 3 (type int) as type *int in assignment
  10. fmt.Println(&num) //0xc0420080c8
  11. fmt.Println(p); //0xc0420080c8,p和num指向了同一个内存地址
  12. fmt.Println(*p) //3
  13. //=============== 指向指针的指针 ======================
  14. var pp **int
  15. pp = &p
  16. fmt.Println(pp); //0xc042004028,获取指针 p 的内存地址,而指针 p 的值存的是它指向的内存地址
  17. fmt.Println(*pp); //0xc0420080c8,获取指针p 指向的内存地址
  18. fmt.Println(**pp) //3 ,** 获取指针p 指向的内存地址的值
  19. }

数组

类型 [n]T 表示拥有 n 个 T 类型的值的数组,数组用作函数参数是值传递。

  1. package main
  2. import "fmt"
  3. func main() {
  4. var a [2]string
  5. a[0] = "Hello"
  6. a[1] = "World"
  7. fmt.Println(a[0], a[1])
  8. fmt.Println(a)
  9. // 注意:{} 中的元素个数不能大于 [] 中的数字
  10. primes := [6]int{2, 3, 5, 7, 11, 13}
  11. fmt.Println(primes)
  12. }

Slice

切片是对数组一个连续片段的引用,没有设置元素个数的数组我们称之为切片, []T 表示一个元素类型为 T 的切片,切片用作函数参数是引用传递,切片比数组更常用,Go 中的大部分数组编程都是通过切片来完成的。

切片的零值是 nil,nil 切片的长度和容量为 0 且没有底层数组

  1. package main
  2. import "fmt"
  3. func main() {
  4. var s []int
  5. fmt.Println(s, len(s), cap(s))
  6. if s == nil {
  7. fmt.Println("nil!")
  8. }
  9. }

冒号分隔后的切片或数组,包括第一个元素,但排除最后一个元素。

  1. package main
  2. import "fmt"
  3. func main() {
  4. primes := [6]int{2, 3, 5, 7, 11, 13}
  5. var s []int = primes[1:4]
  6. fmt.Println(s) // [3 5 7]
  7. }

切片是对数组的引用,切片并不存储任何数据,它只是描述了底层数组中的一段,更改切片的元素会修改其底层数组中对应的元素。

  1. package main
  2. import "fmt"
  3. func main() {
  4. names := [4]string{
  5. "John",
  6. "Paul",
  7. "George",
  8. "Ringo",
  9. }
  10. fmt.Println(names) // [John Paul George Ringo]
  11. b := names[1:3]
  12. b[0] = "XXX"
  13. fmt.Println(names) // [John XXX George Ringo]
  14. }

相关函数

  • len() : 获取切片的元素个数。
  • cap() : 获取切片的容量。
  • make([]T, len, cap) : 创建切片,len 是数组的元素个数,cap 是数组的容量,如果cap 省略,则其值和 len 相同。
  • copy(dest, from) :复制切片,会在底层创建一个匿名的数组,这样就切断了与原切片间的引用关系,实现的效果是会覆盖相同索引处的元素,如果没有对应的索引,dest不变。
  • append() : 向数组/切片追加元素。

切片截取

  1. package main
  2. import "fmt"
  3. func main() {
  4. a := make([]int, 5)
  5. printSlice("a", a)
  6. b := make([]int, 0, 5)
  7. printSlice("b", b)
  8. c := b[:2]
  9. printSlice("c", c)
  10. d := c[2:5]
  11. printSlice("d", d)
  12. }
  13. func printSlice(s string, x []int) {
  14. fmt.Printf("%s len=%d cap=%d %v\n",
  15. s, len(x), cap(x), x)
  16. }

使用 append 追加元素

  1. package main
  2. import (
  3. "fmt"
  4. )
  5. func main() {
  6. s := []int {}
  7. s2 := []int {1,2,3}
  8. s = append(s, 4,5,6) // 添加元素到切片
  9. s2= append(s2, s...) // 通过...操作符合并切片
  10. fmt.Println(s, s2) // [4 5 6] [1 2 3 4 5 6]
  11. }

使用 copy 复制切片

  1. package main
  2. import (
  3. "fmt"
  4. )
  5. func main() {
  6. s := []int{1}
  7. arr := []int{4, 5, 6, 7, 8, 9}
  8. copy(arr, s)
  9. fmt.Println(arr, s) //[1 5 6 7 8 9] [1],arr和s有相同的索引(index=0),所以index=0处的4被覆盖成了1
  10. s[0] = 0
  11. fmt.Println(arr, s) //[1 5 6 7 8 9] [0], 切断了引用关系,即使修改了源切片也不会影响copy生成的切片
  12. //复制整个切片
  13. arr2 := []int{1, 2, 3, 4, 5}
  14. arr3 := make([]int, len(arr2))
  15. copy(arr3, arr2)
  16. arr3[0] = 0
  17. fmt.Println(arr2, arr3) // [1 2 3 4 5] [0 2 3 4 5]
  18. }

字符串的底层就是一个 byte 的数组,因此,也可以进行切片操作

  1. package main
  2. import (
  3. "fmt"
  4. "strings"
  5. )
  6. func main() {
  7. host := "www.baidu.com"
  8. pdIndex := strings.LastIndex(host, ".")
  9. fmt.Println(host[pdIndex:]) //.com
  10. s := []byte(host)
  11. s[0] = 'W'
  12. fmt.Println(string(s)) //Www.baidu.com
  13. }

Map

map 其实就是key-value形式的数据结构,在别的语言也叫hash表,字典或关联数组。

申明但未初始化的 Map 零值是 nil,既没有键,也不能添加键。

  1. package main
  2. import (
  3. "fmt"
  4. )
  5. func main(){
  6. var m1 map[string]string
  7. fmt.Println(m1, m1 == nil) // map[] true
  8. }

可以在申明时给 Map 赋值或者通过 make 函数来初始化 Map,当然可以用任意类型初始化 Map,例如 struct。

  1. package main
  2. import (
  3. "fmt"
  4. )
  5. type User struct {
  6. Name string
  7. Age int
  8. }
  9. func main(){
  10. m2 := map[string]string{"a":"a"}
  11. m3 := make(map[string]string)
  12. // 结构体作为 value
  13. m4 := map[string]User{
  14. "zhangsan": { "张三", 18},
  15. "lisi": {"李四", 19},
  16. }
  17. fmt.Println(m2, m3) // map[a:a] map[]
  18. fmt.Println(m4) // map[lisi:{李四 19} zhangsan:{张三 18}]
  19. }

对 Map 进行增删查改。

  1. package main
  2. import "fmt"
  3. func main() {
  4. m := make(map[string]int)
  5. // 插入元素
  6. m["Answer"] = 42
  7. fmt.Println("The value:", m["Answer"])
  8. // 修改元素
  9. m["Answer"] = 48
  10. fmt.Println("The value:", m["Answer"])
  11. // 元素数量
  12. fmt.Println("The count:", len(m))
  13. // 删除元素
  14. delete(m, "Answer")
  15. fmt.Println("The value:", m["Answer"])
  16. // 判断某个key是否存在
  17. // 若 key 在 m 中,isset 为 true,否则 isset 为 false。
  18. // 若 key 不在映射中,那么 v 是该映射元素类型的零值。
  19. v, isset := m["Answer"]
  20. if(isset) {
  21. fmt.Println("v value is", v)
  22. }else{
  23. fmt.Println("v not found ", v)
  24. }
  25. }

对 Map 进行迭代时,它是随机输出元素的,如果你想顺序迭代,可以先对Map中的键排序,然后遍历排好序的键,把对应的值取出来。

  1. package main
  2. import (
  3. "fmt"
  4. "sort"
  5. )
  6. func main() {
  7. m := map[string]string{"a": "A", "b": "B", "c": "C", "d": "D"}
  8. //===================随机取值=================
  9. for k, v := range m {
  10. fmt.Println(k, "=", v) // C D A b
  11. }
  12. //================= 顺序取值 ==================
  13. var keys []string
  14. for k := range m {
  15. keys = append(keys, k)
  16. }
  17. //对key进行排序
  18. sort.Strings(keys)
  19. for _, k := range keys {
  20. fmt.Println(k, "=", m[k]) // A b C D
  21. }
  22. }

new 和 make 的区别

new

  1. import "fmt"
  2. type Student struct {
  3. name string
  4. age int
  5. }
  6. func main() {
  7. // new 一个内建类型
  8. var a *int
  9. a = new(int)
  10. *a = 10
  11. fmt.Println(*a) //10
  12. // new 一个自定义类型
  13. s := new(Student)
  14. s.name = "bob"
  15. }

make

  1. package main
  2. import "fmt"
  3. func main() {
  4. var b = make(map[string]string)
  5. b["name"] = "bob"
  6. fmt.Println(b)
  7. }

面向对象

go 没有面向对象的概念,面向对象中我们使用 class 关键字来申明一个类,而在 go 中我们可以通过 type 关键词来申明一个类型。可以将类型看作是面向对象中的对象(相当于申明了一个类)。

自定义类型与类型别名

  • 自定义类型:自定义类型是基于内置的基本类型,使用 type 关键字定义的一种全新类型,和原类型是不同的两个类型。
  • 类型别名:类型别名和原类型完全一样,只不过是另一种叫法而已。
  1. package main
  2. type D = int // 类型别名,有等号
  3. type I int // 自定义类型,无等号
  4. func main() {
  5. v := 100
  6. var d D = v // 不报错
  7. var i I = v // 报错
  8. var K I = I(v) // 转换成 I 类型后就不报错了
  9. }

1.自定义类型
有这么一个闭包函数,参数类型和返回值类型都是函数,对于这样的代码其实是不好读的。

  1. func getValue(func(n int) int) func(n int) int {
  2. return func(n int) int {
  3. return c(n) + 1
  4. }
  5. }

我们可以借助自定义类型重构如下,这样一来这个函数就比较可读了。

  1. type IntCompute func(n int) int
  2. func getValue(c IntCompute) IntCompute {
  3. return func(n int) int {
  4. return c(n) + 1
  5. }
  6. }

2.类型别名

类型别名还可以为其它包中的类型定义别名,只要这个类型在其它包中是导出的:

  1. package main
  2. import (
  3. "fmt"
  4. "time"
  5. )
  6. type MyTime = time.Time
  7. func main() {
  8. var t MyTime = time.Now()
  9. fmt.Println(t)
  10. }

如果类型别名导出是的,那么别的包中就可以使用,它和原始类型是否是导出没关系:

  1. type t1 struct {
  2. S string
  3. }
  4. type T2 = t1

在switch中,你不能将原类型和类型别名作为两个分支,因为这是重复的case:

  1. package main
  2. import "fmt"
  3. type D = int
  4. func main() {
  5. var v interface{}
  6. var d D = 100
  7. v = d
  8. switch i := v.(type) {
  9. case int:
  10. fmt.Println("it is an int:", i)
  11. //报错: duplicate case int in type switch
  12. case D:
  13. fmt.Println("it is D type:", i)
  14. }
  15. }

既然类型别名和原始类型是相同的,那么它们的方法集也是相同的:

  1. package main
  2. type T1 struct{}
  3. type T3 = T1
  4. func (t1 T1) say() {}
  5. func (t3 *T3) greeting() {}
  6. func main() {
  7. var t1 T1
  8. var t3 T3
  9. t1.say()
  10. t1.greeting()
  11. t3.say()
  12. t3.greeting()
  13. }

类型别名建立的类型只能为本地类型添加方法,不能为内置类型添加方法:

  1. package main
  2. import (
  3. "fmt"
  4. "time"
  5. )
  6. type NTime = time.Time
  7. // 报错 :cannot define new methods on non-local type time.Time
  8. func (t NTime) Dida() {
  9. fmt.Println("嘀嗒嘀嗒嘀嗒嘀嗒搜索")
  10. }
  11. func main() {
  12. t := time.Now()
  13. t.Dida()
  14. }

对象定义三要素

  1. package main
  2. import "fmt"
  3. //对象定义
  4. type person struct {
  5. name string
  6. city string
  7. age int8
  8. }
  9. //构造函数
  10. func newPerson(name, city string, age int8) *person {
  11. return &person{
  12. name: name,
  13. city: city,
  14. age: age,
  15. }
  16. }
  17. //接收者
  18. func (p *person) SetAge(newAge int8) {
  19. p.age = newAge
  20. }
  21. func main() {
  22. p := newPerson("bob", "beijing", 20)
  23. p.SetAge(18)
  24. fmt.Println(p.name, (*p).age)
  25. }

Struct

Go语言中的基础数据类型可以表示一些事物的基本属性,但是当我们想表达一个事物的全部或部分属性时,这时候再用单一的基本数据类型明显就无法满足需求了,Go语言提供了一种自定义数据类型,可以封装多个基本数据类型,这种数据类型叫结构体(struct),通过 struct 可以实现面向对象。

结构体定义与实例化

  1. package main
  2. import (
  3. "fmt"
  4. )
  5. func main() {
  6. //============================结构体定义=========================
  7. type T_simple struct {
  8. id int
  9. name string
  10. phone string
  11. age int
  12. }
  13. //基本的实例化
  14. var t T_simple
  15. t.id = 10
  16. t.name = "tcl"
  17. fmt.Println(t) //{10 tcl 0}
  18. //new 函数实例化
  19. new_simple = new(T_simple) //类似其他语言 new 对象一样
  20. t.id = 10
  21. t.name = "tcl"
  22. fmt.Println(t) //{10 tcl 0}
  23. //键值对方式实例化
  24. t_simple := T_simple{ id:11, name:"tcl"}
  25. fmt.Println(t_simple) // {11 tcl 0}
  26. //定义并初始化
  27. t_default := struct {
  28. Name string
  29. Age int
  30. }{
  31. "tcl", 28,
  32. }
  33. fmt.Println(t_default) // tcl 28
  34. }

结构体中字段大写开头表示可公开访问,小写表示私有(仅在定义当前结构体的包中可访问),结构体成员使用 . 访问。

  1. package main
  2. import "fmt"
  3. type Vertex struct {
  4. X int
  5. Y int // 公开的
  6. z int // 私有的
  7. }
  8. func main() {
  9. v := Vertex{1, 2}
  10. v.X = 4
  11. fmt.Println(v.X) // 4
  12. }

指向结构体的指针 p,可以通过 (*p).X 来访问其字段 X,也可以通过 p.X 访问。p.X 的底层其实就是 (*p).X,这是go为了方便结构体使用帮我们实现的语法糖。

  1. package main
  2. import "fmt"
  3. type Vertex struct {
  4. X int
  5. Y int
  6. }
  7. func main() {
  8. v := Vertex{1, 2}
  9. p := &v
  10. (*p).X = 10
  11. p.Y = 20
  12. fmt.Println(v) // {10 20}
  13. }

方法

方法只是个带接收者参数的函数,接收者位于 func 关键字和方法名之间,接收者只能是通过 type 关键词申明的类型。接收者的类型定义和方法声明必须在同一包内。

  1. package main
  2. import (
  3. "fmt"
  4. )
  5. type Rect struct {
  6. width float64
  7. height float64
  8. }
  9. // Area 方法拥有一个名为 r,类型为 Rect 的接收者
  10. func (r *Rect) Area() float64 {
  11. return r.width * r.height
  12. }
  13. //在Go语言中没有构造函数的概念,通常以 NewXXX 命名的函数来表示【构造函数】
  14. func NewRect(width, height float64) *Rect {
  15. return &Rect{width, height}
  16. }
  17. func main() {
  18. r := NewRect(3,4)
  19. fmt.Println(r.Area()) // 12
  20. }

如果结构体比较复杂的话,值拷贝性能开销会比较大,因此构造函数推荐返回结构体的指针类型。

方法调用时没有严格要求接收者的类型,即方法的接收者为指针类型的话,也可以通过值类型来调用该方法,反之也是这样。

一般来说指针接收者比值接收者更常用

  • 方法经常需要修改它的接收者指向的值。
  • 可以避免在每次调用方法时复制该值,若值的类型为大型结构体时,这样做会更加高效。
  1. package main
  2. import (
  3. "fmt"
  4. )
  5. type person struct {
  6. name string
  7. }
  8. // 接收者为值
  9. func (p person) Call(){
  10. fmt.Println("value call")
  11. }
  12. // 接收者为指针
  13. func (p *person) Call2(){
  14. fmt.Println("point call")
  15. }
  16. func main() {
  17. p := person{}
  18. p.Call() // value call
  19. (&p).Call() // value call
  20. p.Call2() // point call
  21. (&p).Call2() // point call
  22. }

结构体嵌套

结构体嵌套

  1. package main
  2. import "fmt"
  3. //Address 地址结构体
  4. type Address struct {
  5. Province string
  6. City string
  7. }
  8. //User 用户结构体
  9. type User struct {
  10. Name string
  11. Gender string
  12. Address Address
  13. }
  14. func main() {
  15. user := User{
  16. Name: "bob",
  17. Gender: "man",
  18. Address: Address{
  19. Province: "广东",
  20. City: "深圳",
  21. },
  22. }
  23. fmt.Printf("user=%#v\n", user)
  24. }

匿名结构体嵌套

嵌套多个匿名机结构体时,要求字段名称必须唯一,如果有同名字段会有冲突,如果结构体内部有同名字段,就不要使用匿名结构体嵌套了。

  1. package main
  2. import "fmt"
  3. //Address 地址结构体
  4. type Address struct {
  5. Province string
  6. City string
  7. }
  8. //User 用户结构体
  9. type User struct {
  10. Name string
  11. Gender string
  12. Address //这里的
  13. }
  14. func main() {
  15. user := User{
  16. Name: "bob",
  17. Gender: "man",
  18. Address: Address{
  19. Province: "广东",
  20. City: "深圳",
  21. },
  22. }
  23. // 通过匿名结构体.字段名访问
  24. fmt.Println(user.Address.City)
  25. // 直接访问匿名结构体的字段名
  26. fmt.Println(user.Province)
  27. }

结构体标签

结构体标签是它是一个附属于字段的字符串,存储了字段的元数据(如字段映射,数据校验,对象关系映射等等)。

最常用的一个场景就是 json 的编解码,标签名会以结构体字段别名的方式出现在 json 中:

  1. package main
  2. import (
  3. "encoding/json"
  4. "fmt"
  5. )
  6. // 在编解码时 "omitempty" 会忽略 false,0,空指针,空接口,空数组,空切片,空映射,空字符。
  7. // "-" 会完全跳过该字段。
  8. // 小写的字段也会被忽略。
  9. type User struct{
  10. Name string `json:"name"`
  11. Age int `json:"age"`
  12. Phone string `json:"phone,omitempty"`
  13. Id int `json:"-"`
  14. }
  15. func main() {
  16. h := User{
  17. Name : "tcl",
  18. Age : 27,
  19. Phone: "",
  20. Id : 100,
  21. }
  22. jsonData, err := json.Marshal(h)
  23. if err != nil {
  24. fmt.Println(err)
  25. }
  26. fmt.Println(string(jsonData)) //{"name":"tcl","age":27}
  27. }

字段的标签可以有多个键值对,多个键值对使用空格分开:

  1. type User struct{
  2. Name string `json:"name"`
  3. Age int `json:"age"`
  4. Phone string `json:"phone,omitempty"`
  5. Id int `json:"-"`
  6. Area string `province:"广东" city:"shenzhen"`
  7. }

通过反射可以获取字段的标签信息:

  1. package main
  2. import (
  3. "fmt"
  4. "reflect"
  5. )
  6. type User struct{
  7. Name string `json:"name"`
  8. Age int `json:"age"`
  9. Phone string `json:"phone,omitempty"`
  10. Id int `json:"-"`
  11. Area string `province:"广东" city:"shenzhen"`
  12. }
  13. func main() {
  14. var u User
  15. t:=reflect.TypeOf(u)
  16. // 遍历字段
  17. for i:=0;i<t.NumField();i++{
  18. field:=t.Field(i)
  19. fmt.Println(field.Tag)
  20. }
  21. //获取特定字段的标签信息
  22. area, _ := t.FieldByName("Area")
  23. fmt.Println(area.Tag.Get("province")) //获取键值对中的某个 key
  24. fmt.Println(area.Tag.Lookup("city")) //查找标签中某个 key 是否存在
  25. }

Json 编解码

encoding/json 包中的 Marshal() 和 Unmarshal() 方法可以用来对json数据进行编解码, struct、slice、array、map都可以转换成json。

  1. `Marshal()`Go数据对象 -> json数据
  2. `UnMarshal()`Json数据 -> Go数据对象

首字母小写的字段是不会被编码输出的(未导出),但如果你就想编码后的字段名是小写的,那么这时候结构体标签(json:tag)就能帮你实现这种效果。

  1. package main
  2. import (
  3. "encoding/json"
  4. "fmt"
  5. "time"
  6. )
  7. /*
  8. ref 是 Id 字段编解码时的别名
  9. private 字段不会被编码,因为它是小写(未导出的)
  10. */
  11. type FruitBasket struct {
  12. Name string
  13. Fruit []string
  14. Id int64 `json:"ref"`
  15. private string
  16. Created time.Time
  17. }
  18. func main() {
  19. basket := FruitBasket{
  20. Name: "Standard",
  21. Fruit: []string{"Apple", "Banana", "Orange"},
  22. Id: 999,
  23. private: "Second-rate",
  24. Created: time.Now(),
  25. }
  26. jsonData, err := json.Marshal(basket)
  27. if err != nil {
  28. fmt.Println(err)
  29. }
  30. // json.Marshal 生成数据类型是字节数组([]byte),因此我们需要string()将其转换成字符串
  31. fmt.Println(string(jsonData))
  32. // "Name":"Standard","Fruit":["Apple","Banana","Orange"],"ref":999,"Created":"2019-07-27T15:13:5"}
  33. }

解码时,如果你明确知道待解码字符串的类型,你可以为 json 字符串指定一个解析模板,让其解析为特定的某个对象。

  1. package main
  2. import (
  3. "encoding/json"
  4. "fmt"
  5. "time"
  6. )
  7. type FruitBasket struct {
  8. Name string
  9. Fruit []string
  10. Id int64 `json:"ref"`
  11. private string
  12. Created time.Time
  13. }
  14. func main() {
  15. // json.Unmarshal 需要参数类型为字节数组, 需要把字符串转为 []byte 类型
  16. jsonStr := []byte(`
  17. {
  18. "Name": "Standard",
  19. "Fruit": [
  20. "Apple",
  21. "Banana",
  22. "Orange"
  23. ],
  24. "ref": 999
  25. }
  26. `)
  27. // 让 jsonStr 解析为对象 basketStruct
  28. var basketStruct FruitBasket
  29. err := json.Unmarshal(jsonStr, &basketStruct)
  30. if err != nil {
  31. fmt.Println(err)
  32. }
  33. fmt.Println(basketStruct)
  34. }

JSON解析的时候只会解析能找得到的字段,找不到的字段会被忽略,这样的一个好处是:当你接收到一个很大的JSON数据结构而你却只想获取其中的部分数据的时候,你只需将你想要的数据对应的字段名大写,即可轻松解决这个问题。

如果你不知道待解码字符串有哪些字段,可以定义一个 interface{} 类型,interface{} 在 go 表示任意类型,然后利用类型断言特性var.(T),把解码结果转换为 map[string]interface{} 类型来进行后续操作。

  1. package main
  2. import (
  3. "encoding/json"
  4. "fmt"
  5. "time"
  6. )
  7. type FruitBasket struct {
  8. Name string
  9. Fruit []string
  10. Id int64 `json:"ref"`
  11. private string
  12. Created time.Time
  13. }
  14. func main() {
  15. jsonStr := []byte(`
  16. {
  17. "Name": "Standard",
  18. "Fruit": [
  19. "Apple",
  20. "Banana",
  21. "Orange"
  22. ],
  23. "ref": 999
  24. }
  25. `)
  26. // 申明一个任意类型的对象
  27. var any interface{}
  28. var basketStruct FruitBasket
  29. err := json.Unmarshal(jsonStr, &basketStruct)
  30. err2 := json.Unmarshal(jsonStr, &any)
  31. if err != nil || err2 != nil {
  32. fmt.Println(err)
  33. }
  34. // interface{}()是类型转换的语法,因为类型断言要求变量类型必须接口类型,这里将 basketStruct 转换为接口类型
  35. value1, isBasketStruct := interface{}(basketStruct).(FruitBasket)
  36. fmt.Println(value1, isBasketStruct)
  37. // {Standard [Apple Banana Orange] 999 0001-01-01 00:00:00 +0000 UTC} true
  38. // 使用类型断言特性将结果转换为 map[string]interface{}
  39. value2, isMap := any.(map[string]interface{})
  40. fmt.Println(value2, isMap)
  41. //map[Fruit:[Apple Banana Orange] Name:Standard ref:999] true
  42. }

接口

接口 是由一组方法签名定义的集合,和传统面向对象语言中的接口一样,go 接口中的定义方法 只能是没有方法名的抽象方法,而通过接口中嵌入其它接口,就能实现传统面向对象中的接口继承。

接口类型的变量

接口也是一种类型,如果一个变量的类型是接口,我们就称该变量为接口型变量,可以用接口型变量保存任何实现了接口方法的对象。

  1. package main
  2. import (
  3. "fmt"
  4. )
  5. //=============== 接口申明 ============================
  6. type Abser interface {
  7. Abs() float64
  8. }
  9. //==============MyFloat 实现了接口 Abser==============
  10. type MyFloat float64
  11. func (f MyFloat) Abs() float64 {
  12. if f < 0 {
  13. return float64(-f)
  14. }
  15. return float64(f)
  16. }
  17. //==============Vertex 实现了接口 Abser ===================
  18. type Vertex struct {
  19. X, Y float64
  20. }
  21. func (v *Vertex) Abs() float64 {
  22. return v.X*v.X + v.Y*v.Y
  23. }
  24. func main() {
  25. //申明接口型变量
  26. var a Abser
  27. var b Abser
  28. // 接口类型的变量可以保存实现该接口的类型
  29. a = &Vertex{3, 4}
  30. b = MyFloat(-0.1)
  31. // 尽管方法名一样,但这两者的方法体行为完全不一样,这就是多态
  32. fmt.Println(a.Abs())
  33. fmt.Println(b.Abs())
  34. }

值接收者和指针接收者实现接口的区别

类型 Vertex 的方法 Abs(), 它的接收者是指针类型的 *Vertex ,不能把值类型的 Vertex 赋值给接口型变量因为值类型的 Vertex 并未实现 Abser 接口
但如果 Abs() 的接收者为值类型的 Vertex,那么指针类型的 *Vertex 是可以赋值给接口型变量

  1. func main() {
  2. var a Abser
  3. var c Abser
  4. a = &Vertex{3, 4}
  5. c = Vertex{10, 20} //报错:Vertex does not implement Abser (Abs method has pointer receiver)
  6. fmt.Println(a.Abs())
  7. fmt.Println(c.Abs())
  8. }

多个类型实现同一个接口

嵌入的类型如果实现了接口,也视为当前类型实现了接口,即接口的方法,不一定需要由一个类型完全实现。

  1. package main
  2. import (
  3. "fmt"
  4. )
  5. type Servicer interface {
  6. a()
  7. b()
  8. }
  9. type serviceA struct{}
  10. func (sa serviceA) a() {
  11. fmt.Println("a")
  12. }
  13. // serviceB 尽管只实现了接口中的 b() 方法,但由于嵌入了 serviceA,则视 ServiceB 实现了 Service 接口
  14. type serviceB struct {
  15. serviceA
  16. }
  17. func (sb serviceB) b() {
  18. fmt.Println("b")
  19. }
  20. func main() {
  21. sv := serviceB{}
  22. v, isService := interface{}(sv).(Servicer)
  23. fmt.Println(v, isService)
  24. }

接口嵌套

接口与接口间可以通过嵌套创造出新的接口。

  1. type Servicer interface {
  2. discover()
  3. Loger
  4. }
  5. type Loger interface {
  6. log(driver interface{})
  7. }

空接口

空接口(interface{})是特殊形式的接口类型,普通的接口都有方法,而空接口没有定义任何方法。我们可以说所有类型都至少实现了空接口,任何类型值都可以赋值给空接口变量。因此,空 interface{} 代表任意类型(Any)

  1. package main
  2. import (
  3. "fmt"
  4. )
  5. type Any interface {}
  6. // 接口作为函数参数和返回值
  7. func Ascll(anyParam Any) interface{}{
  8. if anyParam == "A" {
  9. return 65
  10. }else if anyParam == 65 {
  11. return "A"
  12. }
  13. // 任何类型值都可以赋值给空接口变量
  14. var anyValue interface{} = map[interface{}]Any{}
  15. return anyValue
  16. }
  17. func main() {
  18. fmt.Println(Ascll("A")) // 65
  19. fmt.Println(Ascll(65)) // A
  20. fmt.Println(Ascll(nil)) // map[]
  21. }

类型断言

类型断言

t, ok := obj.(T) 用来判断某个接口型变量是否属于某个类型并获取它的动态值,类型断言常用于对静态类型为空接口的对象进行断言。

某些情况下要断言的对象未必是接口类型,我们知道接口也是一种类型,因此可以通过 interface{}(obj)将其转换为接口型变量。

  1. t, ok := interface{}(obj).(T)
  1. package main
  2. import (
  3. "fmt"
  4. )
  5. type It interface {
  6. Test()
  7. }
  8. type Person struct {
  9. name string
  10. }
  11. func (p Person) Test() {
  12. // todo
  13. }
  14. func main() {
  15. var a It = Person{name:"tcl"}
  16. val,isPerson := a.(Person)
  17. fmt.Println(val, isPerson) // {tcl} true
  18. var b interface{} = nil
  19. val2,isPerson2 := b.(Person)
  20. fmt.Println(val2, isPerson2) // {} false
  21. }

类型选择

i.(type) 类型选择用在断言多次的场景,其实就是将多个if判断的断言使用switch语句来实现。有点像别的语言中 get_type() 之类的函数。

类型选择中的声明与类型断言 i.(T) 的语法相同,只是具体类型 T 被替换成了关键字 type

  1. package main
  2. import "fmt"
  3. func do(i interface{}) {
  4. switch v := i.(type) {
  5. case int:
  6. fmt.Printf("Twice %v is %v\n", v, v*2)
  7. case string:
  8. fmt.Printf("%q is %v bytes long\n", v, len(v))
  9. default:
  10. fmt.Printf("I don't know about type %T!\n", v)
  11. }
  12. }
  13. func main() {
  14. do(21)
  15. do("hello")
  16. do(true)
  17. }

类型转换

借助类型断言可以实现类型转换。

  1. if r := recover(); r != nil {
  2. //通过类型断言,将捕获的错误值 r 转换为 string 类型,
  3. //然后传入到 errors.New 中返回 error 类型的消息
  4. str , ok := r.(string)
  5. if ok {
  6. err = errors.New(str)
  7. }
  8. }

空接口可以代表任意类型,借助类型断言的特性,我们可以将任何空接口值转换为我们需要的类型。

  1. package main
  2. import (
  3. "fmt"
  4. )
  5. func main() {
  6. var i interface{} = 100
  7. val := i.(int)
  8. fmt.Println(val + 100) // 200
  9. }

如果你构造一个变量时,不确定它的类型,可以先定义为 interface{} 进行赋值,后面用到该变量时进行类型断言转化为对应类型就行了。

  1. var dbConfig = map[string]interface{}{
  2. "host" : "xxx.com:6379",
  3. "passwd": "xxx",
  4. "db" : 0,
  5. }
  6. func newPool(cfg interface{}) *redis.Pool {
  7. // dbConfig 中的每一项都是 interface{} 类型
  8. dbConfig := cfg.(map[string]interface{})
  9. host := dbConfig["host"].(string)
  10. passwd := redis.DialPassword(dbConfig["passwd"].(string))
  11. db := redis.DialDatabase(dbConfig["db"].(int))
  12. ...
  13. }

使用类型断言修改对象值,断言的类型必须是指针。

  1. package main
  2. import (
  3. "fmt"
  4. )
  5. type User struct {
  6. id int
  7. name string
  8. }
  9. func main() {
  10. u := User{1, "Tom"}
  11. var vi, pi interface{} = u, &u //copy u and assign to vi
  12. // vi.(User).name = "Jack" // Error: cannot assign to vi.(User).name
  13. pi.(*User).name = "Jack"
  14. fmt.Printf("%v\n", vi.(User)) //{1 Tom}
  15. fmt.Printf("%v\n", pi.(*User)) //&{1 Jack}
  16. }

最佳实践

以接口组织代码的方式在 Go 语言中非常常见,较大的接口定义,可以由多个小接口定义组合而成。

  1. type Reader interface {
  2. Read(p []byte) (n int, err error)
  3. }
  4. type Writer interface {
  5. Write(p []byte) (n int, err error)
  6. }
  7. type ReadWriter interface {
  8. Reader
  9. Writer
  10. }
  11. //只依赖于必要功能的最小接口
  12. func StoreData(reader Reader) error {
  13. ...
  14. }

面向接口是 Go 语言鼓励的开发方式,我们应该遵循固定的模式对外提供功能。

  1. 使用大写的 Service 对外暴露方法;
  2. 使用小写的 service 实现接口中定义的方法;
  3. 通过 func NewService(...) (Service, error) 函数初始化 Service 接口;
  1. package post
  2. type Service interface {
  3. ListPosts() ([]*Post, error)
  4. }
  5. type service struct {
  6. conn *grpc.ClientConn
  7. }
  8. func NewService(conn *grpc.ClientConn) Service {
  9. return &service{
  10. conn: conn,
  11. }
  12. }
  13. func (s *service) ListPosts() ([]*Post, error) {
  14. posts, err := s.conn.ListPosts(...)
  15. if err != nil {
  16. return []*Post{}, err
  17. }
  18. return posts, nil
  19. }

反射

为我们提供一种可以在运行时操作任意类型对象的能力。比如我们可以查看一个接口变量的具体类型,看看一个结构体有多少字段,如何修改某个字段的值等等.

TypeOf和ValueOf

go 的 reflect 为我们提供了两个方法用来获取对象的类型和实例:

  • reflect.TypeOf(): 获取任意对象的具体类型,如果为空则返回 nil。
  • reflect.ValueOf():获取对象的实例,如果为空则返回 0。
  1. package main
  2. import (
  3. "fmt"
  4. "reflect"
  5. )
  6. type User struct{
  7. Name string
  8. Age int
  9. }
  10. func main() {
  11. user := User{"张三",20}
  12. // TypeOf会返回目标数据的类型,比如上文我们通过 type 关键字定义的 User 类型
  13. reflectType := reflect.TypeOf(user)
  14. // valueOf返回目标数据的的值,比如上文的 {张三 20}
  15. reflectValue := reflect.ValueOf(user)
  16. fmt.Println("type: ", reflectType)
  17. fmt.Println("value: ", reflectValue)
  18. }

获取类型的底层类型

底层类型指的是基础的数据类型,比如上面我们申明的 User 类型,其底层基础类型是 struct,底层类型可以通过 Kind() 方法获取。

  1. package main
  2. import (
  3. "fmt"
  4. "reflect"
  5. )
  6. type User struct{
  7. Name string
  8. Age int
  9. }
  10. func main() {
  11. user := User{"张三",20}
  12. reflectType := reflect.TypeOf(user)
  13. concreteType := reflectType.Kind()
  14. if concreteType == reflect.Struct {
  15. fmt.Println("底层类型是 struct")
  16. }else {
  17. fmt.Println("类型不是 struct")
  18. }
  19. }

遍历字段和方法

NumField() 和 NumMethod() 可以获取目标对象的字段数量和方法数量。

  1. package main
  2. import (
  3. "fmt"
  4. "reflect"
  5. )
  6. type User struct{
  7. Name string
  8. Age int
  9. }
  10. func (u User) GetName() string{
  11. return u.Name
  12. }
  13. func main() {
  14. user := User{"张三",20}
  15. t := reflect.TypeOf(user)
  16. v := reflect.ValueOf(user)
  17. fmt.Println("类型名称:", t.Name())
  18. for i:=0;i<t.NumField();i++ {
  19. fmt.Println("字段名:",t.Field(i).Name)
  20. fmt.Println("字段值:",v.Field(i).Interface())
  21. }
  22. for i:=0;i<t.NumMethod() ;i++ {
  23. m := t.Method(i)
  24. fmt.Println("方法名:", m.Name)
  25. fmt.Println("方法类型:", m.Type)
  26. }
  27. }

修改字段的值

利用反射修改对象的字段值时需要注意:

  • 传入 ValueOf() 的是对象的地址,也就是对象的指针。
  • Elem() 方法可以找到这个指针指向的值。
  • FieldByName() 方法可以找到要修改的目标字段。
  1. package main
  2. import (
  3. "fmt"
  4. "reflect"
  5. )
  6. type User struct {
  7. Name string
  8. Age int
  9. }
  10. func main() {
  11. s := &User{Name: "张三", Age: 20}
  12. v := reflect.ValueOf(s)
  13. // 修改值必须是指针类型否则不可行
  14. if v.Kind() != reflect.Ptr {
  15. fmt.Println("不是指针类型,没法进行修改操作")
  16. return
  17. }
  18. // 获取指针所指向的元素
  19. v = v.Elem()
  20. if !v.CanSet() {
  21. fmt.Println("无法修改该对象")
  22. return
  23. }
  24. // 获取目标key的Value的封装
  25. name := v.FieldByName("Name")
  26. if name.Kind() == reflect.String {
  27. name.SetString("李四")
  28. }
  29. fmt.Printf("%#v \n", *s)
  30. // 如果是整型的话
  31. test := 888
  32. testV := reflect.ValueOf(&test)
  33. testV.Elem().SetInt(666)
  34. fmt.Println(test)
  35. }

动态调用方法

  1. package main
  2. import (
  3. "fmt"
  4. "reflect"
  5. )
  6. type User struct {
  7. Name string
  8. Age int
  9. }
  10. func (s User) EchoName(name string){
  11. fmt.Println("我的名字是:", name)
  12. }
  13. func main() {
  14. s := User{Name: "张三", Age:20}
  15. v := reflect.ValueOf(s)
  16. // 获取目标方法
  17. m := v.MethodByName("EchoName")
  18. // 构造参数
  19. args := []reflect.Value{reflect.ValueOf("李四")}
  20. // 调用函数
  21. m.Call(args) //我的名字是: 李四
  22. }

更多关于反射的使用见 这里

并发编程

单线程也可以实现并发,go 中的并发由 Go 自身的调度器实现,并行是和主机的CPU 核数有关的,多核就可以并行并发,单核只能并发了。

go 协程(goroutine)

协程本质上是一种用户态线程,不需要操作系统来进行抢占式调度,且在真正的实现中寄存于线程中,因此系统开销极小.

通过 go + func() 即可创建一个 goroutine,就这样一个简单的 go 关键字就将同步代码转为异步代码:

  1. import "fmt"
  2. func mytest() {
  3. fmt.Println("hello, go")
  4. }
  5. func main() {
  6. // 启动一个协程
  7. go mytest()
  8. fmt.Println("hello, world")
  9. }

main 函数本身相当于主线程,当 main 函数执行完成后,这个线程也就终结了, 它不会等待其下运行着的所有协程返回结果,因此上面的这段代码只会输出hello, world ,而不会输出 hello, go因为协程的创建需要时间,main 函数执行完后,它里面的协程可能还没来得及执行

因此如果 main 函数等待几秒,则会看到其他协程的输出:

  1. import (
  2. "fmt"
  3. "time"
  4. )
  5. func mytest() {
  6. fmt.Println("hello, go")
  7. }
  8. func main() {
  9. go mytest()
  10. fmt.Println("hello, world")
  11. time.Sleep(time.Second)
  12. }

sync.WaitGroup(等待组)

WaitGroup 的字面意思就是指等待一组协程执行完成后才会继续向下执行。细节见这里这里

sync.WaitGroup 的使用步骤:

WaitGroup 的局限:

其中等待的一组协程中有一个协程发生错误,则告诉协程组的其他协程,全部停止运行(本次任务失败)以免浪费系统资源。该场景WaitGroup是无法实现的,那么该场景该如何实现呢,就需要用到通知机制,其实也可以用channel来实现。

  1. func main() {
  2. var wg sync.WaitGroup
  3. go func(){
  4. wg.Add(1)
  5. defer wg.Done()
  6. for i:=1;i<100;i++ {
  7. fmt.Println("A:",i)
  8. }
  9. }()
  10. go func(){
  11. wg.Add(1)
  12. defer wg.Done()
  13. for i:=1;i<100;i++ {
  14. fmt.Println("B:",i)
  15. }
  16. }()
  17. wg.Wait()
  18. }

通道(channel)

通道用在多个协程间 同步数据。

注意:waitGoup 只是用来协程间同步,但不通信,而 channel 既能同步又能通信。

通道定义

通道是和 slice,map 一样的数据类型,可以使用内置的 make 函数声明初始化。

  1. # 定义通道
  2. ch:=make(chan int)
  3. # 读写通道
  4. ch <- 2
  5. <-ch
  6. # 关闭通道, 注意:通道只应该被发送者关闭。
  7. close(ch)
  8. # 检测通道是否关闭
  9. # data为通道的值,open 为通道的状态: 未关闭为 true, 已关闭则为 false
  10. data, open := <-pipline

通道读取有点像 php 的数组元素弹出函数 array_pop 一样,当你弹出一个元素后,源数组就少了一个,因此把通道读取也应看成弹出元素一样,比如说通道中只有一个元素,当你取完以后就不能再取了。

  1. package main
  2. import (
  3. "fmt"
  4. )
  5. func main() {
  6. ch := make(chan int, 1)
  7. go func() {
  8. ch <- 1
  9. }()
  10. fmt.Println(<-ch)
  11. fmt.Println(<-ch) //fatal error: all goroutines are asleep - deadlock!
  12. }

单项通道

只能读或只能写的通道称为单项通道,单项通道一般用作函数参数。

  • 只能写入的通道:ch chan <- int
  • 只能读取的通道:ch <- chan int
  1. // 只能写入
  2. func output(ch chan <- int) {
  3. //...
  4. }
  5. // 只能读取
  6. func input( ch <- chan int) {
  7. //...
  8. }

缓冲通道与非缓冲通道

创建通道的 make 函数可以接收第二个参数,为通道的容量。

  • 容量 = 0 的通道称为无缓冲通道。无缓冲通道要求接收者和发送者不能处于同一个协程中,否则将会造成死锁,在接收者未读取通道前,发送者的后续执行都是阻塞的。
  • 容量 > 0 的通道称为缓冲通道;对于缓冲通道,cap(ch)可以获取通道的容量,len(ch)可以获取通道中的元素数量,缓冲通道对接着者和发送者的顺序没有要求,并且二者可以出现在同一协程中。

无缓冲通道

  1. package main
  2. import (
  3. "fmt"
  4. "time"
  5. )
  6. func hello(pipline chan string) {
  7. pipline <- "hello world"
  8. }
  9. func main() {
  10. pipline := make(chan string)
  11. go hello(pipline) //发送者
  12. fmt.Print(<-pipline) //接收者
  13. time.Sleep(time.Second)
  14. }

需要注意,无缓冲通道会阻塞后续的执行,直到通道中的数据被读取。

  1. package main
  2. import (
  3. "fmt"
  4. "time"
  5. )
  6. func hello(ch chan string) {
  7. ch <- "hello world\n"
  8. //阻塞后续代码的执行,顺序输出 hello world 和 end
  9. fmt.Print("end\n")
  10. }
  11. func main() {
  12. //未设置容量的非缓冲通道
  13. ch := make(chan string)
  14. go hello(ch)
  15. time.Sleep(time.Millisecond * 100)
  16. fmt.Print(<-ch)
  17. //等待几秒以便子协程中的数据输出
  18. time.Sleep(time.Millisecond * 100)
  19. /*
  20. hello world
  21. end
  22. */
  23. }

缓冲通道

  1. package main
  2. import "fmt"
  3. func main() {
  4. pipline := make(chan string, 1)
  5. pipline <- "hello world"
  6. fmt.Println(<-pipline)
  7. //hello world
  8. }

缓冲通道不会阻塞后续代码的执行,你会看到后续代码会立马输出。

  1. package main
  2. import (
  3. "fmt"
  4. "time"
  5. )
  6. func hello(ch chan string) {
  7. ch <- "hello world\n"
  8. //不会阻塞后续代码执行,你会看到 end 先于 hello world 输出
  9. fmt.Print("end\n")
  10. }
  11. func main() {
  12. //设置了容量的缓冲通道
  13. ch := make(chan string, 1)
  14. go hello(ch)
  15. time.Sleep(time.Millisecond * 100)
  16. fmt.Print(<-ch)
  17. /*
  18. end
  19. hello world
  20. */
  21. }

通道遍历

通道遍历可以有三种方式: forfor-range任务数量;前两种遍历通道时,务必确保发送者已关闭(close)通道,否则会造成接收者一直在等待数据 ,造成死锁;最后一种则无需关闭通道, 但缺点是你必须知道任务数量是多少。

  1. package main
  2. import "fmt"
  3. func main() {
  4. taskNum := 10
  5. c := make(chan int, taskNum)
  6. //c := make(chan int)
  7. // 发送者
  8. go func() {
  9. for i := 0; i < taskNum; i++ {
  10. c <- i
  11. }
  12. //关闭channel
  13. close(c)
  14. }()
  15. //第一种:for循环遍历
  16. for {
  17. if data, ok := <-c; ok {
  18. fmt.Println("for:", data)
  19. } else {
  20. break
  21. }
  22. }
  23. //第二种:for-range遍历
  24. for i := range c {
  25. fmt.Println("for-range:", i)
  26. }
  27. //第三种:通过任务数量遍历,这种发送者无需关闭 channel
  28. for i := 0; i < taskNum; i++ {
  29. fmt.Println("taskNum:", <-c)
  30. }
  31. }

再看一个通道遍历的变体,在协程中遍历通道。

  1. package main
  2. import (
  3. "fmt"
  4. )
  5. func main() {
  6. c := make(chan int)
  7. execRes := make(chan string, 10)
  8. //协程1: for
  9. go func() {
  10. for {
  11. execRes <- fmt.Sprintf("task=%d exec by for", <-c)
  12. }
  13. }()
  14. //协程2: for-range
  15. go func() {
  16. for i := range c {
  17. execRes <- fmt.Sprintf("task=%d exec by for-range", i)
  18. }
  19. }()
  20. // 生成10个任务,分配给给两个协程去执行
  21. taskNum := 10
  22. for i := 1; i <= taskNum; i++ {
  23. c <- i
  24. }
  25. // 通过任务数量遍历通道,获取每一个任务的执行结果,每个协程最终能分配几个任务是随机的
  26. for i := 0; i < taskNum; i++ {
  27. fmt.Println(<-execRes)
  28. }
  29. }

通道 select

select 会阻塞到某个 channel 可以继续执行为止,当多个 channel 就绪时,select 会随机选择一个执行。

select 的语法类似 switch 的功能,可以使用 break 或 return 就会退出 select 语句。

  1. c := make(chan int)
  2. go func() {
  3. for v := range c {
  4. // 随机输出了0,1, 证明select 是随机读取/写入通道的
  5. fmt.Println(v)
  6. }
  7. }()
  8. for i := 0; i < 10; i++ {
  9. select {
  10. case c <- 0:
  11. case c <- 1:
  12. }
  13. }

为了防止长时间阻塞,可以为 select 设置超时时间(等待 channel 数据的时间)。

  1. c := make(chan bool)
  2. select {
  3. case v := <-c:
  4. fmt.Println(v)
  5. case t := <-time.After(3 * time.Second):
  6. fmt.Println(t)
  7. fmt.Println("Timeout")
  8. }

为了在发送或者接收时不发生阻塞,select 语句还支持定义一个 default 分支。

  1. c := make(chan int)
  2. select {
  3. case i := <-c:
  4. fmt.Println(i)
  5. default:
  6. fmt.Println("default")
  7. }

通道死锁案例

无论缓冲/非缓冲通道,如果接收者出现在发送者前面会造成死锁,因为接收者为了读取数据会一直阻塞后面的发送者往通道里写数据。

  1. //死锁
  2. func main() {
  3. // c := make(chan int, 1)
  4. c := make(chan int)
  5. //通道中没有数据会一直阻塞后续的执行
  6. fmt.Println(<-c)
  7. go func() {
  8. c <- 1
  9. }()
  10. //fatal error: all goroutines are asleep - deadlock!
  11. }
  12. //正常
  13. func main() {
  14. // c := make(chan int, 1)
  15. c := make(chan int)
  16. go func() {
  17. c <- 1
  18. }()
  19. //通道中能够读取数据
  20. fmt.Println(<-c)
  21. //1
  22. }

非缓冲通道,发送者出现在前面 也会造成死锁,因为通道无法被读取,导致发送者一直阻塞;但如果是 缓冲通道则正常,因为缓冲通道写入不会阻塞后续执行。

  1. //死锁(非缓冲通道)
  2. func main() {
  3. c := make(chan int)
  4. //通道无法被读取,会一直阻塞后续的执行
  5. c <- 1
  6. go func() {
  7. fmt.Println(<-c)
  8. }()
  9. time.Sleep(3 * time.Second)
  10. // fatal error: all goroutines are asleep - deadlock!
  11. }
  12. //正常(非缓冲通道)
  13. func main() {
  14. c := make(chan int)
  15. go func() {
  16. fmt.Println(<-c)
  17. }()
  18. //通道能被读取,不会阻塞后续执行
  19. c <- 1
  20. time.Sleep(3 * time.Second)
  21. //1
  22. }
  23. //正常(缓冲通道)
  24. func main() {
  25. c := make(chan int, 1)
  26. //缓冲通道不会阻塞后续执行
  27. c <- 1
  28. go func() {
  29. fmt.Println(<-c)
  30. }()
  31. time.Sleep(3 * time.Second)
  32. // 1
  33. }

无论缓冲/非缓冲通道,在同一协程中 通道写入超过通道容量限制会导致死锁;但如果不在同一协程,写入数据超过通道容量限制则不会造成死锁

  1. //正常:未超过容量限制
  2. func main() {
  3. pipline := make(chan string, 1)
  4. pipline <- "hello world"
  5. fmt.Println(<-pipline)
  6. }
  7. //死锁:超过容量限制
  8. func main() {
  9. ch1 := make(chan string, 1)
  10. ch1 <- "hello world"
  11. ch1 <- "hello China" //这条写入超过容量限制,导致死锁
  12. fmt.Println(<-ch1)
  13. //fatal error: all goroutines are asleep - deadlock!
  14. }
  15. //正常:不在同一协程写入超过容量限制
  16. package main
  17. import "fmt"
  18. func main() {
  19. ch := make(chan string, 1)
  20. go func() {
  21. ch <- "hello world"
  22. ch <- "hello China"
  23. }()
  24. fmt.Println(<-ch)
  25. //hello world
  26. }

无论缓冲/非缓冲通道,如果通道没有关闭,读取次数超过通道容量会造成死锁;但如果通道关闭的话,可以随意读取多次,如果值已取出,则读取的是类型的零值。

  1. package main
  2. import "fmt"
  3. func main() {
  4. UnDeadLocl()
  5. DeadLocl()
  6. }
  7. //死锁
  8. func DeadLocl() {
  9. ch := make(chan int, 1)
  10. go func() {
  11. ch <- 1
  12. //close(ch)
  13. }()
  14. fmt.Println(<-ch)
  15. fmt.Println(<-ch)
  16. }
  17. //正常
  18. func UnDeadLocl() {
  19. ch := make(chan int, 1)
  20. go func() {
  21. ch <- 1
  22. close(ch)
  23. }()
  24. fmt.Println(<-ch)
  25. fmt.Println(<-ch)
  26. }

遍历一个未关闭的缓冲/非缓冲通道,会造成死锁,为了避免这种情况,发送者在发送完数据后,需要调用 close() 来关闭通道。但如果你通过任务数量来遍历通道,则无需关闭通道

  1. package main
  2. import "fmt"
  3. func main() {
  4. DeadLocl()
  5. UnDeadLocl()
  6. }
  7. //死锁
  8. func DeadLocl() {
  9. pipline := make(chan string, 2)
  10. go func() {
  11. pipline <- "hello world"
  12. pipline <- "hello China"
  13. // close(pipline)
  14. }()
  15. for data := range pipline {
  16. fmt.Println(data)
  17. }
  18. }
  19. //正常(无需close通道)
  20. func UnDeadLocl() {
  21. taskNum := 2
  22. ch := make(chan string, taskNum)
  23. go func() {
  24. ch <- "hello world"
  25. ch <- "hello China"
  26. // close(pipline)
  27. }()
  28. for i := 0; i < taskNum; i++ {
  29. fmt.Println(<-ch)
  30. }
  31. }

通道阻塞案例

用通道来做锁 :使用容量为1的缓冲通道以达到共享变量的目的,当通道达到设定的容量时,此时再往里发送数据会阻塞整个程序。

  1. package main
  2. import (
  3. "fmt"
  4. "time"
  5. )
  6. func main() {
  7. // 注意要设置容量为 1 的缓冲信道以达到共享变量的目的
  8. ch := make(chan bool, 1)
  9. var x int
  10. for i := 0; i < 1000; i++ {
  11. go func(n int) {
  12. //如果使用非缓冲通道,在同一协程中将出现死锁(发送者和接收者相互等待),将导致永远执行不到x++ 这里
  13. ch <- true
  14. // 由于 x++ 不是原子操作,所以应避免多个协程对x进行操作
  15. // 使用容量为1的信道可以达到锁的效果
  16. x++
  17. <- ch
  18. }(x)
  19. }
  20. time.Sleep(time.Second)
  21. fmt.Println("x 的值:", x)
  22. }

通道注意事项

并发控制

有并发就有资源竞争,两个或者多个goroutine在没有相互同步的情况下,访问某个共享的资源,比如同时对该资源进行读写时,就会处于相互竞争的状态。

下面一个资源竞争的例子,我们可以多运行几次这个程序,会发现结果可能是2,也可以是3,也可能是4。因为共享资源count变量没有任何同步保护,所以两个goroutine都会对其进行读写,会导致对已经计算好的结果覆盖。

  1. package main
  2. import (
  3. "fmt"
  4. "runtime"
  5. "sync"
  6. )
  7. var (
  8. count int32
  9. wg sync.WaitGroup
  10. )
  11. func main() {
  12. wg.Add(2)
  13. go incCount()
  14. go incCount()
  15. wg.Wait()
  16. fmt.Println(count)
  17. }
  18. func incCount() {
  19. defer wg.Done()
  20. for i := 0; i < 2; i++ {
  21. value := count
  22. runtime.Gosched()
  23. value++
  24. count = value
  25. }
  26. }

竞态检测

借助 go build -race 我们可以详细的查看 goroutine 间的竞争情况。它会为我们在当前项目下生成一个可以执行文件,然后我们运行这个可执行文件,就可以看到打印出的检测信息。

  1. tcl@tcl:/data/goProject$ go build -race
  2. tcl@tcl:/data/goProject$ ./goProject
  3. ==================
  4. WARNING: DATA RACE
  5. Read at 0x0000005ef5ec by goroutine 7:
  6. main.incCount()
  7. /data/goProject/index.go:25 +0x6f
  8. Previous write at 0x0000005ef5ec by goroutine 6:
  9. main.incCount()
  10. /data/goProject/index.go:28 +0x8e
  11. Goroutine 7 (running) created at:
  12. main.main()
  13. /data/goProject/index.go:17 +0x77
  14. Goroutine 6 (running) created at:
  15. main.main()
  16. /data/goProject/index.go:16 +0x5f
  17. ==================
  18. 4
  19. Found 1 data race(s)

可以看到提示我们:

goroutine 7在代码25行读取共享资源value := count,而这时goroutine 6正在代码28行修改共享资源count = value,这就出现了资源竞争了。

面对并发类的资源竞争问题,传统解决的办法就是对资源加锁,那么接下来我们看下 go 中提供的几种资源竞争的处理方案。

原子函数(atomic)

atomic包中的原子函数提供了对资源加锁的功能。

atomic.LoadInt32和atomic.StoreInt32两个函数,一个读取int32类型变量的值,一个是修改int32类型变量的值,这两个都是原子性的操作,Go已经帮助我们在底层使用加锁机制,保证了共享资源的同步和安全。

  1. package main
  2. import (
  3. "fmt"
  4. "runtime"
  5. "sync"
  6. "sync/atomic"
  7. )
  8. var (
  9. count int32
  10. wg sync.WaitGroup
  11. )
  12. func main() {
  13. wg.Add(2)
  14. go incCount()
  15. go incCount()
  16. wg.Wait()
  17. fmt.Println(count)
  18. }
  19. func incCount() {
  20. defer wg.Done()
  21. for i := 0; i < 2; i++ {
  22. value := atomic.LoadInt32(&count)
  23. runtime.Gosched()
  24. value++
  25. atomic.StoreInt32(&count,value)
  26. }
  27. }

使用原子函数后,再次使用工具 go build -race 检查,发现不会提示有问题了。

  1. tcl@tcl:/data/goProject$ go build -race
  2. tcl@tcl:/data/goProject$ ./goProject
  3. 2

互斥锁(sync.Mutex)

atomic虽然可以解决资源竞争问题,但是比较都是比较简单的,支持的数据类型也有限,所以Go语言还提供了另一种类型的锁-互斥锁

互斥锁可以让我们自己灵活的控制哪些代码,同时只能有一个goroutine访问,被互斥锁控制的这段代码范围,被称之为临界区,临界区的代码,同一时间,只能又一个goroutine访问。

  1. package main
  2. import (
  3. "fmt"
  4. "runtime"
  5. "sync"
  6. )
  7. var (
  8. count int32
  9. wg sync.WaitGroup
  10. mutex sync.Mutex
  11. )
  12. func main() {
  13. wg.Add(2)
  14. go incCount()
  15. go incCount()
  16. wg.Wait()
  17. fmt.Println(count)
  18. }
  19. func incCount() {
  20. defer wg.Done()
  21. for i := 0; i < 2; i++ {
  22. mutex.Lock()
  23. value := count
  24. runtime.Gosched()
  25. value++
  26. count = value
  27. mutex.Unlock()
  28. }
  29. }

示例中我们先调用mutex.Lock()对有竞争资源的代码加锁,这样当一个goroutine进入这个区域的时候,其他goroutine就进不来了,只能等待,一直到调用mutex.Unlock() 释放这个锁为止。

读写锁(sync.RWMutex)

互斥锁有个问题:

当一个goroutine访问的时候,其他goroutine都不能访问,这样肯定保证了资源的同步,避免了竞争,不过也降低了性能。

而我们希望是像 mysql 中的共享锁一样,当前进程读数据的时候,不要互斥其他的进程的读操作,因为数据不会被修改,那么其实是不存在资源竞争的问题的,不管怎么读取,多少goroutine同时读取,都是可以的那么。

所以这就延伸出来 go 中另外一种锁-读写锁

读写锁可以让多个读操作同时并发,同时读取,但是对于写操作是完全互斥的。也就是说,当一个goroutine进行写操作的时候,其他goroutine既不能进行读操作,也不能进行写操作。

  1. var count int
  2. var wg sync.WaitGroup
  3. var rw sync.RWMutex
  4. func main() {
  5. wg.Add(10)
  6. for i:=0;i<5;i++ {
  7. go read(i)
  8. }
  9. for i:=0;i<5;i++ {
  10. go write(i);
  11. }
  12. wg.Wait()
  13. }
  14. func read(n int) {
  15. rw.RLock()
  16. fmt.Printf("读goroutine %d 正在读取...\n",n)
  17. v := count
  18. fmt.Printf("读goroutine %d 读取结束,值为:%d\n", n,v)
  19. wg.Done()
  20. rw.RUnlock()
  21. }
  22. func write(n int) {
  23. rw.Lock()
  24. fmt.Printf("写goroutine %d 正在写入...\n",n)
  25. v := rand.Intn(1000)
  26. count = v
  27. fmt.Printf("写goroutine %d 写入结束,新值为:%d\n", n,v)
  28. wg.Done()
  29. rw.Unlock()
  30. }

我们在read里使用读锁,也就是RLock和RUnlock,写锁的方法名和我们平时使用的一样Lock和Unlock,这样我们就使用了读写锁,可以并发的读,但是同时只能有一个写,并且写的时候不能进行读操作。

条件变量(sync.Cond)

sync.Cond 经常用在多个 goroutine 等待,一个 goroutine 通知(事件发生)的场景。

sync.Cond的作用并不是保证在同一时刻仅有一个线程访问某一个共享数据,而是在对应的共享数据的状态发生变化时,通知其他因此而被阻塞的协程。

sync.Cond 需要与锁(互斥锁,或者读写锁)一起使用, NewCond 函数可以返回 *sync.Cond 的对象指针,成员变量 L 代表与条件变量搭配使用的锁。

  1. func NewCond(l Locker) *Cond {
  2. return &Cond{L: l}
  3. }
  4. // A Locker represents an object that can be locked and unlocked.
  5. type Locker interface {
  6. Lock()
  7. Unlock()
  8. }

该对象主要有三个方法

  • wait():阻塞等待通知,如源码所示Wait()方法会阻塞所在协程,直到收到其他协程的通知结束阻塞时,会重新给 c.L 加锁,并且继续执行 Wait 后面的代码。因此调用 Wait 方法前,必须加锁。
  • Signal():单发通知,给一个正等待(阻塞)在该条件变量上的 goroutine 发送通知。
  • Broadcast():广播通知,给正在等待(阻塞)在该条件变量上的所有 goroutine 发送通知。
  1. func (c *Cond) Wait() {
  2. c.checker.check()
  3. t := runtime_notifyListAdd(&c.notify) // 等待的goruntine数+1
  4. c.L.Unlock() // 释放锁资源
  5. runtime_notifyListWait(&c.notify, t) // 阻塞,等待其他goruntine唤醒
  6. c.L.Lock() // 获取资源
  7. }
  8. func (c *Cond) Signal() {
  9. c.checker.check()
  10. runtime_notifyListNotifyOne(&c.notify) // 唤醒最早被阻塞的goruntine
  11. }
  12. func (c *Cond) Broadcast() {
  13. c.checker.check()
  14. runtime_notifyListNotifyAll(&c.notify) // 唤醒所有goruntine
  15. }

示例一

  1. package main
  2. import (
  3. "fmt"
  4. "sync"
  5. "time"
  6. )
  7. var (
  8. lock sync.Mutex //互斥锁
  9. cond = sync.NewCond(&lock) //条件变量
  10. )
  11. func main() {
  12. //调用 Wait 方法前必须加锁
  13. cond.L.Lock()
  14. fmt.Println("main thread got lock ")
  15. go doSomething() //子线程
  16. fmt.Println("main thread begin wait ")
  17. //main 线程执行到 wait 释放锁后,这时子线程会获取锁成功
  18. cond.Wait()
  19. fmt.Println("main thread end wait ")
  20. cond.L.Unlock()
  21. fmt.Println("main thread release lock ")
  22. /*
  23. main thread got lock
  24. main thread begin wait
  25. sub thread begin get lock
  26. sub thread got lock
  27. sub thread release lock
  28. main thread end wait
  29. main thread release lock
  30. */
  31. }
  32. func doSomething() {
  33. time.Sleep(time.Second * 2)
  34. fmt.Println("sub thread begin get lock ")
  35. //调用条件变量的signal方法之前,加锁这个并不是必须的,可以注释获取和释放锁代码,也是OK的
  36. //cond.L.Lock()
  37. //defer cond.L.Unlock()
  38. fmt.Println("sub thread got lock ")
  39. time.Sleep(5 * time.Second)
  40. cond.Signal() //这时候main线程则会重新获取到锁,然后从wait返回
  41. fmt.Println("sub thread release lock")
  42. }

示例二
假设我们需要循环处理某队列中的任务,但是我们又想在没有任务时,能够出让CPU的空闲给别的进程。

未使用条件变量前的代码:

  1. func addTask(t *Task) {
  2. mutex.Lock()
  3. tasks[t.Id] = t
  4. mutex.UnLock()
  5. }
  6. func Loop() {
  7. for {
  8. mutex.Lock()
  9. for taskId, task := range tasks {
  10. // handle code
  11. }
  12. lenght := len(task)
  13. mutex.UnLock()
  14. // 为了减少cpu空转,当队列为空的时候sleep 2秒
  15. if length == 0 {
  16. time.Sleep(time.Second * 2)
  17. }
  18. }
  19. }

使用条件变量后的代码:

  1. func addTask(t *Task) {
  2. mutex.Lock()
  3. tasks[t.Id] = t
  4. if len(task) == 1 {
  5. cond.Signal()
  6. }
  7. mutex.UnLock()
  8. }
  9. func Loop() {
  10. for {
  11. mutex.Lock()
  12. // 如果当前任务数为0 调用Wait()等待新任务,直到添加任务后被唤醒
  13. if len(tasks) == 0 {
  14. cond.Wait()
  15. }
  16. for taskId, task := range tasks {
  17. // handle code
  18. }
  19. mutex.UnLock()
  20. }
  21. }

只执行一次(sync.Once)

sync.Once 典型应用场景就是仅需执行一次的任务,例如数据库连接池的建立,全局变量的延迟初始化等。

sync.Once 类型的 Do() 可以实现只执行一次的效果,该方法接收一个无参数,无结果的函数作为参数,该方法一旦被调用,就会去调用作为参数的那个函数。

  1. package main
  2. import (
  3. "fmt"
  4. "math/rand"
  5. "sync"
  6. )
  7. func main() {
  8. var count = 0
  9. var once sync.Once
  10. max := rand.Intn(100)
  11. for i:=0 ; i<max; i++{
  12. once.Do(func() {
  13. count++
  14. })
  15. }
  16. fmt.Println("Count:",count) // Count: 1
  17. }

可以看到无论运行了多少次,始终输出的是第一次运行的结果 Count: 1

临时对象池(sync.Pool)

一句话总结:保存和复用临时对象,减少内存分配,降低 GC 压力。

sync.Pool 只有一个公开的字段 New,该字段的类型是一个函数类型,该函数一般仅在池中无可用对象的时候才调用,使用 New 生成的对象是持久的。

  1. type Pool struct {
  2. noCopy noCopy
  3. local unsafe.Pointer // 本地P缓存池指针
  4. localSize uintptr // 本地P缓存池大小
  5. // 当池中没有可能对象时
  6. // 会调用 New 函数构造构造一个对象
  7. New func() interface{}
  8. }

sync.Pool 还有两个方法 PutGet ,前者用来向对象池中写入对象,后者用来从对象池中读取对象。

Put 方法写入的对象是临时的,因为这种对象通过 Get 方法访问时,从对象池中返回的同时并删除,并且这种临时对象也会受垃圾回收的影响。

对于 Get 方法,如果没有临时对象,那么就会访问 New 函数生成的持久对象,如果你在初始化对象池的时候没有设置 New 函数,那么 Get 方法只能返回 nil 了。

注意:

Get 方法只会删除 Put 方法写入的临时对象,并不会删除 New 函数创建的持久对象。

示例

  1. package main
  2. import (
  3. "fmt"
  4. "runtime"
  5. "runtime/debug"
  6. "sync"
  7. "sync/atomic"
  8. )
  9. func main() {
  10. // 禁用GC,并保证在main函数执行结束前恢复GC。
  11. defer debug.SetGCPercent(debug.SetGCPercent(-1))
  12. var count int32
  13. newFunc := func() interface{} {
  14. return atomic.AddInt32(&count, 1)
  15. }
  16. pool := sync.Pool{New: newFunc}
  17. // 访问使用 New 函数创建的持久对象
  18. v1 := pool.Get()
  19. fmt.Printf("Value 1: %v\n", v1) //1
  20. // 临时对象池的存取
  21. pool.Put(10)
  22. pool.Put(11)
  23. pool.Put(12)
  24. v2 := pool.Get()
  25. fmt.Printf("Value 2: %v\n", v2) // 10
  26. // 手动触发垃圾回收,导致之前使用 put 写入的三个值被回收掉
  27. debug.SetGCPercent(100)
  28. runtime.GC()
  29. // 这时再访问对象池,就会返回 New 函数创建的对象
  30. v3 := pool.Get()
  31. fmt.Printf("Value 3: %v\n", v3) // 2
  32. //置空对象池,这时再获取对象就会发现返回为 nil
  33. pool.New = nil
  34. v4 := pool.Get()
  35. fmt.Printf("Value 4: %v\n", v4) // nil
  36. }

Context

Context 是一个可以帮助我们实现多 goroutine 协作流程的同步工具,从设计的角度来看,Context 提供了一种 父goroutine子goroutine 的管理功能。

我们为某个 HTTP 请求创建一个 goroutine 以并发地处理业务,同时这个 goroutine 也可能会创建更多的 goroutine 来访问数据库或者 RPC 服务。当上层的 goroutine 超时或者被终止的时候,我们希望其他的 子goroutine 及时停掉无用的工作减少额外资源的消耗。

这其实就是Context的最大作用,如果没有Context,当上层执行的操作出现错误时,下层其实不会收到错误而是会继续执行下去。每一个 Context 都会从最顶层的 goroutine 一层一层传递到最下层,这也是 Context 最常见的使用方式,此外 Context 还能携带键值对信息传递给下层 goroutine。

Context 是 context 包对外暴露的接口,该接口定义了四个需要实现的方法:

  1. type Context interface {
  2. Deadline() (deadline time.Time, ok bool)
  3. Done() <-chan struct{}
  4. Err() error
  5. Value(key interface{}) interface{}
  6. }

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 :

  1. func WithCancel(parent Context) (ctx Context, cancel CancelFunc)
  2. func WithDeadline(parent Context, deadline time.Time) (Context, CancelFunc)
  3. func WithTimeout(parent Context, timeout time.Duration) (Context, CancelFunc)
  4. 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

  1. package main
  2. import (
  3. "context"
  4. "fmt"
  5. "time"
  6. )
  7. func main() {
  8. ctx, cancel := context.WithCancel(context.Background())
  9. go func(ctx context.Context) {
  10. for {
  11. select {
  12. case <-ctx.Done():
  13. fmt.Println("收到取消任务的通知了...")
  14. return
  15. default:
  16. fmt.Println("执行任务中...")
  17. time.Sleep(1 * time.Second)
  18. }
  19. }
  20. }(ctx)
  21. time.Sleep(3 * time.Second)
  22. fmt.Println("任务执行出错,通知其他 goroutine 取消任务")
  23. cancel()
  24. time.Sleep(3 * time.Second)
  25. fmt.Println("完毕...")
  26. /*
  27. 执行任务中...
  28. 执行任务中...
  29. 执行任务中...
  30. 任务执行出错,通知其他 goroutine 取消任务
  31. 收到取消任务的通知了...
  32. 完毕...
  33. */
  34. }

示例 WithTimeout

  1. package main
  2. import (
  3. "context"
  4. "fmt"
  5. "time"
  6. )
  7. func main() {
  8. timeOut := 1 * time.Second
  9. ch := make(chan string)
  10. ctx, _ := context.WithTimeout(context.Background(), timeOut)
  11. go handle(ctx, ch)
  12. fmt.Println(<-ch)
  13. /*
  14. 开始运行...
  15. 超时退出...
  16. */
  17. }
  18. func handle(ctx context.Context, ch chan string) {
  19. fmt.Println("开始运行...")
  20. time.Sleep(1 * time.Second)
  21. select {
  22. case <-ctx.Done():
  23. ch <- "超时退出..."
  24. default:
  25. ch <- "结束运行..."
  26. }
  27. }

上述示例中如果你将 timeOut 换成2s后你会发现它是正常输出的.

示例 WithValue

  1. package main
  2. import (
  3. "context"
  4. "fmt"
  5. "time"
  6. )
  7. var key string = "name"
  8. func main() {
  9. ctx, cancel := context.WithCancel(context.Background())
  10. //这样就生成了一个新的 Context,这个新的 Context 会带有键值对
  11. valueCtx:=context.WithValue(ctx,key,"custom-value")
  12. go watch(valueCtx)
  13. //sleep 2s 以便等待协程输出后再输出
  14. time.Sleep(2 * time.Second)
  15. fmt.Println("任务执行出错,通知其他 goroutine 取消任务")
  16. //取消父Context
  17. cancel()
  18. //sleep 2s 以便等待协程输出后再输出
  19. time.Sleep(2 * time.Second)
  20. fmt.Println("完毕...")
  21. }
  22. func watch(ctx context.Context) {
  23. for {
  24. select {
  25. case <-ctx.Done():
  26. //取出值
  27. fmt.Println("收到取消任务的通知了,value:", ctx.Value(key))
  28. return
  29. default:
  30. //取出值
  31. fmt.Println("执行任务中,value:",ctx.Value(key))
  32. time.Sleep(2 * time.Second)
  33. }
  34. }
  35. }
  36. /*
  37. 执行任务中,value: custom-value
  38. 任务执行出错,通知其他 goroutine 取消任务
  39. 收到取消任务的通知了,value: custom-value
  40. 完毕...
  41. */
添加新批注
在作者公开此批注前,只有你和作者可见。
回复批注