跟着产品自学Go - 3_基本用法

1.1 变量常量、基本数据类型

变量命名规范:

  • 变量名只能包含字母、数字和下划线(_),不能包含其他字符。
  • 变量名的第一个字符必须是字母或下划线,不能是数字。
  • 变量名区分大小写。
  • 变量名不能是Go语言的关键字,例如if、else、for等。keywords
  • 变量名应该具有一定的描述性,可以通过变量名来表达变量的含义。

变量:

go 复制代码
var name string = "xiaoming"
// 或
name := "xiaoming"

常量:

go 复制代码
const hello string = "nihao"

基本数据类型:

  1. 整数类型(Integer Types):

    • int:根据平台可能是32位或64位整数。
    • int8int16int32int64:分别是8位、16位、32位和64位整数。
    • uint:无符号整数,大小与平台相关。
    • uint8uint16uint32uint64:分别是8位、16位、32位和64位无符号整数。
    go 复制代码
    var age int = 30
    var count int64 = 1000000
    var distance uint = 500
  2. 浮点数类型(Floating-Point Types):

    • float32:32位浮点数。
    • float64:64位浮点数(也称为float)。
    go 复制代码
    var pi float32 = 3.1415926
    var price float64 = 19.99
  3. 复数类型(Complex Types):

    • complex64:由两个float32表示的复数。
    • complex128:由两个float64表示的复数。
    go 复制代码
    var complexNum complex64 = 3 + 4i
    var anotherComplex complex128 = -1.5 + 2.5i
  4. 布尔类型(Boolean Type):

    • bool:表示真(true)或假(false)值。
    go 复制代码
    var isTrue bool = true
    var isFalse bool = false
  5. 字符串类型(String Type):

    • string:用于存储文本。
    go 复制代码
    var name string = "John Mary 猫"
    var message string = "Hello, World!"
  6. 字节类型(Byte Type):

    • byte:与uint8相同,表示一个字节的整数值,通常用于处理二进制数据。
    go 复制代码
    var byteValue byte = 65 // ASCII code for 'A'
    var binaryData []byte = []byte{0x48, 0x65, 0x6C, 0x6C, 0x6F} // "Hello"
  7. Unicode 字符类型(Unicode Character Type):

    • rune:与int32相同,表示一个Unicode码点。
    go 复制代码
    var runeValue rune = 'Ω' // Unicode character Omega

1.2 类型转换

  1. 整数类型转换

    go 复制代码
    var num1 int = 42
    var floatNum float64 = float64(num1) // 将整数转换为浮点数
    fmt.Println(floatNum)               // 输出: 42
  2. 浮点数类型转换

    go 复制代码
    var num2 float64 = 3.14
    var intNum int = int(num2) // 将浮点数转换为整数(小数部分会被截断)
    fmt.Println(intNum)        // 输出: 3
  3. 字符串和字节切片之间的转换

    go 复制代码
    str := "Hello, Golang!"
    bytes := []byte(str) // 将字符串转换为字节切片
    fmt.Println(bytes) // 输出: [72 101 108 108 111 44 32 71 111 108 97 110 103 33]
  4. 字符串和整数之间的转换

    go 复制代码
    numStr := "123"
    num, _ := strconv.Atoi(numStr) // 将字符串转换为整数
    fmt.Println(num)               // 输出: 123
  5. 整数和字符串之间的转换

    go 复制代码
    num3 := 456
    numStr2 := strconv.Itoa(num3) // 将整数转换为字符串
    fmt.Println(numStr2)          // 输出: "456"
  6. 布尔类型和整型之间的转换

    go 复制代码
    var b = true
    var i2 int
    if b {
      i2 = 1
    } else {
      i2 = 0
    }
    fmt.Println(i2)
  7. 布尔类型和字符串之间的转换

    go 复制代码
    var b2 = true
    var str2 string
    if b2 {
      str2 = "true"
    } else {
      str2 = "false"
    }
    fmt.Println(str2)

1.3 指针

什么是指针?

指针是一个变量,其值为另一个变量的内存地址。你可以想象它是一个索引,指向计算机内存的某个位置。通过这个索引,你可以直接进行读取或者修改其它变量的值。

为什么需要指针?

  • 直接访问内存,改变变量的值。
  • 在函数调用中,改变函数外部变量的值。
  • 优化内存利用,例如,数据结构如树和链表的实现。
  • 提高程序性能,避免大量数据的复制。
go 复制代码
var p *int      // 指针类型 ==> 地址
var i3 = 123 
p = &i3         // & 取地址 
fmt.Println(p)  // 0x14000112040
fmt.Println(*p) // 123
*p = 456        // 修改了他们共同地址(0x14000112040)里面的值
fmt.Println(i3) // 456

通过使用指针,可以直接访问和修改变量在内存中的值,而不必直接操作变量本身。这在某些情况下非常有用,例如函数传递参数、动态分配内存等。

以下是一些关于 Go 语言中指针的重要概念和操作:

  1. 声明指针: 可以通过在变量类型前加上 * 来声明一个指针变量,例如 var ptr *int 声明了一个指向整数的指针。
  2. 获取地址: 使用 & 运算符可以获取一个变量的内存地址,例如 x := 10; ptr := &xptr 设置为变量 x 的地址。
  3. 解引用指针: 使用 * 运算符可以解引用指针,访问存储在指针指向的地址上的值,例如 val := *ptr 将会将指针 ptr 指向的地址上的值赋给 val
  4. 传递指针给函数: 在函数参数中使用指针,可以使函数修改传递的变量。当您想要在函数内部修改一个变量并且希望这个变化在函数外部也可见时,可以传递指针作为参数。

当然,也可以通过函数去修改内存的值,此时,我们传递的应该是一个地址,比如:

go 复制代码
...
p = &i3         // & 取地址 
...
func AddPointer(p *int) int {
  return *p + 1
}
...
result2 := AddPointer(p)

1.4 引入其他文件的函数

假设项目结构如下:

go 复制代码
myproject/
├── main.go
└── utils/
    └── math.go

math.go文件内容如下:

go 复制代码
// utils/math.go
package utils
​
func Add(a, b int) int {
    return a + b
}

main.go 文件内容如下:

go 复制代码
// main.go
package main
​
import (
    "fmt"
    "myproject/utils"
)
​
func main() {
    result := utils.Add(3, 5)
    fmt.Println("Result:", result)
}

main.go 中的 import "myproject/utils" 语句导入了 utils 包,使 Add 函数在 main 函数中可用。

注意:

Go 语言不允许在当前软件包外部访问未导出的标识符,包括函数、变量、常量等。这是 Go 语言的可见性规则之一。

在 Go 语言中,标识符(如函数、变量名)的可见性是由标识符的首字母的大小写来决定的:

  • 以大写字母开头的标识符是可导出的(public),可以在其他包中使用。
  • 以小写字母开头的标识符是不可导出的(private),只能在当前包内部使用。

1.5 数组、slice、数组转切片

1.5.1 数组

在 Go 语言中,数组是一种固定长度、同类型元素的集合。数组在内存中是连续存储的,这使得数组在某些情况下非常高效,但它们的长度是固定的,不可动态改变。

  1. 声明数组: 声明一个数组需要指定数组的类型和长度。语法如下:

    go 复制代码
    var arrayName [length]Type

    例如:

    go 复制代码
    var numbers [5]int
  2. 初始化数组: 数组可以在声明时进行初始化,也可以在后续的代码中赋值。初始化数组时,可以使用花括号 {} 括起来的元素列表。例如:

    go 复制代码
    numbers := [5]int{1, 2, 3, 4, 5}

    如果初始化时不指定数组长度,Go 会根据提供的元素数量自动计算数组长度。

  3. 访问数组元素: 使用索引来访问数组中的元素,索引从 0 开始。例如:

    go 复制代码
    x := numbers[2] // 获取第三个元素(索引为2)
  4. 数组长度: 使用 len 函数可以获取数组的长度。例如:

    go 复制代码
    length := len(numbers)
  5. 多维数组: Go 支持多维数组,即数组的数组。例如:

    go 复制代码
    var matrix [3][3]int
  6. 数组是值类型: 当将一个数组赋值给另一个数组时,会发生数据的拷贝。这意味着修改一个数组的副本不会影响原始数组。

  7. 注意事项: 数组的长度是固定的,一旦声明后就无法更改。这可能在某些场景下限制了数组的灵活性。为了避免这种限制,通常会使用切片(Slice)这种动态长度的数据结构。

以下是一个使用数组的简单示例:

go 复制代码
package main

import "fmt"

func main() {
    numbers := [5]int{1, 2, 3, 4, 5}

    fmt.Println("Array:", numbers)
    fmt.Println("Third element:", numbers[2])
    fmt.Println("Array length:", len(numbers))
}

总之,数组是 Go 语言中一种用于存储固定长度、同类型元素的数据结构。但由于其固定长度的限制,对于更灵活的数据管理,常常使用切片来替代数组。

在 Go 语言中,数组是值类型,传递数组给函数时会进行一次拷贝,因此在函数内部修改数组的值不会影响原始数组。要在函数内部修改数组并使修改对原始数组可见,可以通过传递指向数组的指针来实现。

go 复制代码
package main

import "fmt"

// 接受一个指向数组的指针作为参数,将数组中的所有元素加倍
func doubleArray(arr *[5]int) {
    for i := 0; i < len(arr); i++ {
        (*arr)[i] *= 2
    }
}

func main() {
    numbers := [5]int{1, 2, 3, 4, 5}

    fmt.Println("Original array:", numbers)

    // 将指向 numbers 的指针传递给 doubleArray 函数
    doubleArray(&numbers)

    fmt.Println("Doubled array:", numbers)
}

在这个示例中,doubleArray 函数接受一个指向数组的指针作为参数,并将数组中的每个元素加倍。在 main 函数中,我们声明了一个整数数组 numbers,然后通过 doubleArray(&numbers) 将指向 numbers 的指针传递给 doubleArray 函数。在函数内部,通过解引用指针并操作数组元素,修改了原始的 numbers 数组。

1.5.2 slice切片

在 Go 语言中,切片(Slice)是一个动态长度的数据结构,它基于数组并提供了更灵活的方式来管理一系列元素。切片不同于数组,它的长度是可变的,允许动态地添加、删除和修改元素。

  1. 创建切片: 使用 make 函数来创建切片,同时指定切片的类型、长度和容量。或者,您可以直接使用数组或其他切片创建切片。

    go 复制代码
    // 使用 make 创建切片
    slice := make([]Type, length, capacity)
    
    // 从数组创建切片
    array := [5]int{1, 2, 3, 4, 5}
    slice := array[start:end] // 包含 start 索引,不包含 end 索引
  2. 切片操作: 您可以使用切片操作来获取、添加和删除元素。

    • 获取元素:通过索引访问切片中的元素,索引从 0 开始。
    • 添加元素:使用 append 函数将元素添加到切片的末尾。
    • 删除元素:通过切片操作或者 append 函数删除切片中的元素。
  3. 切片长度和容量: 切片的长度表示其中的元素个数,而容量表示切片的底层数组的大小。容量不会超过底层数组的长度。

  4. 传递切片: 切片是引用类型,当您将切片传递给函数时,函数中对切片的修改会影响到原始切片。

以下是一个简单的切片示例:

go 复制代码
package main

import "fmt"

func main() {
    // 创建切片
    numbers := []int{1, 2, 3, 4, 5}

    fmt.Println("Original slice:", numbers)

    // 添加元素到切片
    numbers = append(numbers, 6)

    fmt.Println("After append:", numbers)

    // 删除切片中的元素
    numbers = append(numbers[:2], numbers[3:]...)

    fmt.Println("After deletion:", numbers)
}

在这个示例中,我们创建一个切片 numbers,然后使用 append 函数在切片末尾添加一个元素。

接下来,使用切片操作删除了索引为 2 的元素。请注意,删除切片元素时,我们使用了 ... 运算符,将切片元素展开传递给 append 函数。

如果,扩容呢?

go 复制代码
// main.go
...
number2 := make([]int, 10)
for i := 0; i < 10; i++ {
  number2 = append(number2, i)
}
fmt.Println(number2)
utils.SliceAdd(&number2)
fmt.Println(number2)
...
// utils/sliceAdd.go
func SliceAdd(arr *[]int) {
  for i := 0; i < 10; i++ {
    *arr = append(*arr, i)
  }
}

1.5.3 数组转切片

在 Go 语言中,可以将数组转换为切片,从而可以在切片上更加灵活地操作。切片在数组的基础上提供了动态长度的特性。要将数组转换为切片,只需提供数组的一个或多个元素的索引范围。

go 复制代码
package main
​
import "fmt"
​
func main() {
    // 创建一个数组
    array := [5]int{1, 2, 3, 4, 5}
​
    // 将数组转换为切片
    // 使用 array[start:end],其中 start 是起始索引,end 是结束索引(不包含)
    slice := array[1:4]
​
    fmt.Println("Array:", array)
    fmt.Println("Slice:", slice)
}

在这个示例中,我们首先创建了一个包含 5 个整数的数组 array。然后,通过使用 array[1:4],我们将数组从索引 1 到 3 转换为切片 slice。请注意,切片的结束索引是不包含的,因此切片中包含了索引 1、2、3 对应的元素。

主要作用于:在函数内部,影响到实际数组的值

go 复制代码
// main.go
array10 := [10]int{1, 2, 3, 4, 5, 6, 7, 8, 9, 10}
fmt.Println(array10)
utils.ArrayToSlice(array10[:])
fmt.Println(array10)
​
// utils/arrayToSlice.go
func ArrayToSlice(arr []int) {
  for i := range arr {
    arr[i] += 1
  }
}

1.6 map

map是一种内置的数据结构,用于表示无序的键值对集合。它类似于其他编程语言中的字典(dictionary)。map常用于基于键快速高效地检索数据。

golang中map的特点:

  1. 映射关系 map使用哈希表实现,可以存储键值对数据,通过键快速查找对应的值。
  2. 无序性 map中的元素不是按顺序排序的,也不能通过索引访问,只能通过键访问值。
  3. 键的唯一性 map的每个键对应一个值,键不能重复,重复的键会覆盖前一个值。
  4. 变长 map可以动态增删键值对,长度可以任意扩容和收缩。
  5. nil map 没有初始化的map的值是nil。nil map不能用来存储键值对。
  6. 引用类型 map是引用类型,作为函数参数时遵循引用传递,可以直接修改原始map。
  7. 线程不安全 同时读写map需要加锁同步。
  8. 优化查找 基于哈希表实现,查找速度很快,近似O(1)复杂度。
  9. 按遍历顺序返回 Go 1.12之后,遍历map的元素顺序是确定的,不再随机。

1.6.1 map基础操作

Go中使用map的基本概述:

  1. 声明和初始化: 使用map关键字声明一个map,需要指定键和值的类型。使用make函数创建map实例。

    go 复制代码
    var myMap map[string]int // 声明
    myMap = make(map[string]int) // 初始化

    也可以使用短变量声明进行声明和初始化:

    go 复制代码
    myMap := make(map[string]int) // 声明并初始化
  2. 添加和获取元素: 使用键来添加和获取元素。

    go 复制代码
    myMap["one"] = 1      // 增加
    myMap["two"] = 2
    ​
    value := myMap["one"] // 获取值
  3. 删除元素: 使用delete函数来删除指定键的元素。

    go 复制代码
    delete(myMap, "two") // 删除键为"two"的元素
  4. 检查键是否存在: 使用多重赋值方式,可以判断键是否存在以及对应的值是什么。

    go 复制代码
    value, exists := myMap["three"]
    if exists {
        // 键存在,可以使用value
    } else {
        // 键不存在
    }
  5. 迭代: 使用for range循环来遍历map中的所有键值对。

    go 复制代码
    for key, value := range myMap {
        fmt.Println(key, value)
    }

1.6.2 if、switch、for、break、continue

if 语句

在 Go 中,if 语句用于根据条件执行代码块。示例:

go 复制代码
package main
​
import "fmt"
​
func main() {
    age := 20
    if age >= 18 {
        fmt.Println("You are an adult")
    } else {
        fmt.Println("You are a minor")
    }
}

switch 语句

switch 语句用于根据一个表达式的值进行多路分支判断。示例:

go 复制代码
package main
​
import "fmt"
​
func main() {
    day := "Tuesday"
    switch day {
    case "Monday":
        fmt.Println("It's the start of the week")
    case "Friday":
        fmt.Println("It's almost the weekend")
    default:
        fmt.Println("It's a regular day")
    }
}

for 循环

for 循环用于重复执行一段代码块。以下是不同类型的 for 循环示例:

  1. 基本的 for 循环:

    go 复制代码
    package main
    ​
    import "fmt"
    ​
    func main() {
        for i := 0; i < 5; i++ {
            fmt.Println(i)
        }
    }
  2. 类似 while 的循环:

    go 复制代码
    package main
    ​
    import "fmt"
    ​
    func main() {
        num := 0
        for num < 5 {
            fmt.Println(num)
            num++
        }
    }
  3. 无限循环:

    go 复制代码
    package main
    ​
    import "fmt"
    ​
    func main() {
        count := 0
        for {
            if count >= 5 {
                break
            }
            fmt.Println(count)
            count++
        }
    }

breakcontinue

break 用于终止循环,而 continue 用于跳过当前迭代并继续下一次迭代。

go 复制代码
package main
​
import "fmt"
​
func main() {
    // 使用 break 终止循环
    for i := 0; i < 10; i++ {
        if i == 5 {
            break
        }
        fmt.Println(i)
    }
​
    // 使用 continue 跳过特定迭代
    for i := 0; i < 5; i++ {
        if i == 2 {
            continue
        }
        fmt.Println(i)
    }
}

上述示例中,break 将在 i 等于 5 时终止循环,而 continue 将在 i 等于 2 时跳过当前迭代。

1.6.3 多重循环

在 Go 中,可以使用多重循环(嵌套循环)来处理复杂的迭代需求。这意味着可以在一个循环内包含另一个循环。以下是一些示例展示如何使用多重循环:

  1. 嵌套 for 循环

    可以在一个 for 循环内嵌套另一个或多个 for 循环,以处理多维数组、矩阵等复杂结构。

    go 复制代码
    package main
    ​
    import "fmt"
    ​
    func main() {
        // 乘法表示例
        for i := 1; i <= 5; i++ {
            for j := 1; j <= 5; j++ {
                fmt.Printf("%d * %d = %d\t", i, j, i*j)
            }
            fmt.Println()
        }
    }
  2. 使用 breakcontinue 在多重循环中

    可以在多重循环中使用 breakcontinue 语句来控制内层或外层循环的行为。

    go 复制代码
    package main
    ​
    import "fmt"
    ​
    func main() {
        // 使用 break 结束外层循环
        for i := 1; i <= 3; i++ {
            for j := 1; j <= 3; j++ {
                fmt.Println(i, j)
                if i*j > 4 {
                    break
                }
            }
        }
    ​
        // 使用 continue 跳过内层循环的某次迭代
        for i := 1; i <= 3; i++ {
            for j := 1; j <= 3; j++ {
                if j == 2 {
                    continue
                }
                fmt.Println(i, j)
            }
        }
    }
  3. 标签(Label)和多重循环

    可以使用标签(label)来标识循环,然后在需要的地方使用 breakcontinue 搭配标签来控制多重循环的流程。

    go 复制代码
    package main
    ​
    import "fmt"
    ​
    func main() {
        outerLoop:
        for i := 1; i <= 3; i++ {
            for j := 1; j <= 3; j++ {
                fmt.Println(i, j)
                if i*j > 4 {
                    break outerLoop // 使用标签退出外层循环
                }
            }
        }
    }

    标签 outerLoop 用于标识外层循环,在内层循环中使用 break outerLoop 可以跳出外层循环。

1.6.4 for 循环遍历slice和map

  1. 遍历切片(slice)

    切片是有序的集合,可以使用 for 循环来遍历其中的元素。

    go 复制代码
    package main
    ​
    import "fmt"
    ​
    func main() {
        numbers := []int{1, 2, 3, 4, 5}
    ​
        // 遍历切片
        for index, value := range numbers {
            fmt.Printf("Index: %d, Value: %d\n", index, value)
        }
    }

    在上面的示例中,range 关键字用于迭代切片 numbers 中的元素,index 表示元素的索引,value 表示元素的值。

  2. 遍历映射(map)

    映射是一种键-值对的集合,同样可以使用 for 循环来遍历其中的键和值。

    go 复制代码
    package main
    ​
    import "fmt"
    ​
    func main() {
        colors := map[string]string{
            "red":    "#FF0000",
            "green":  "#00FF00",
            "blue":   "#0000FF",
        }
    ​
        // 遍历映射
        for key, value := range colors {
            fmt.Printf("Key: %s, Value: %s\n", key, value)
        }
    }

    在上面的示例中,range 关键字用于迭代映射 colors 中的键和对应的值。

请注意 ,如果只对切片或映射的值进行迭代而不需要索引,可以使用下划线 _ 来忽略索引变量。例如:

go 复制代码
package main
​
import "fmt"
​
func main() {
    numbers := []int{1, 2, 3, 4, 5}
​
    // 遍历切片,忽略索引
    for _, value := range numbers {
        fmt.Println(value)
    }
}

同样,也可以只迭代映射的键而忽略值,只需在 for 循环中使用一个变量即可。

总之,使用 for 循环结合 range 关键字,可以方便地遍历切片和映射中的元素。

1.6.5 for循环的循环变量问题

在 Go 中,for 循环的循环变量在每次循环迭代中都会被重新赋值。这是由于 Go 的语法规定,每次循环迭代都会创建一个新的循环变量副本,而不是在同一个变量上进行修改。这可能会导致一些意外行为,特别是在循环迭代过程中使用闭包或引用类型时。

以下是一个可能会引起问题的🌰:

go 复制代码
package main
​
import "fmt"
​
func main() {
    numbers := []int{1, 2, 3, 4, 5}
    var funcs []func()
​
    for _, num := range numbers {
        funcs = append(funcs, func() {
            fmt.Println(num)
        })
    }
​
    for _, f := range funcs {
        f()
    }
}

在这个示例中,我们期望输出 1, 2, 3, 4, 5,但实际上输出的是 5, 5, 5, 5, 5。这是因为闭包捕获的是循环变量 num 的地址,而不是它的值。由于循环变量在每次迭代中都会被重新赋值,所以在闭包被调用时,num 的值已经变为了最后一个迭代的值 5

为了解决这个问题,可以通过在每次迭代中创建一个新的变量副本来实现预期的行为:

go 复制代码
package main
​
import "fmt"
​
func main() {
    numbers := []int{1, 2, 3, 4, 5}
    var funcs []func()
​
    for _, num := range numbers {
        numCopy := num // 创建 num 的副本
        funcs = append(funcs, func() {
            fmt.Println(numCopy)
        })
    }
​
    for _, f := range funcs {
        f()
    }
}

在每次迭代中,创建一个新的变量 numCopy,并将其用作闭包内的值。这样就避免了在闭包中共享同一个变量,并获得了预期的输出。

1.7 函数

1.7.1 函数的定义和使用

go 复制代码
func functionName(parameter1 type1, parameter2 type2, ...) returnType {
    // 函数体
    // 在这里执行操作
    // 返回结果
}
  • func:这是定义函数的关键字。
  • 函数名functionName:函数的名称,遵循标识符命名规则。
  • 参数列表(parameter1,parameter2):函数的参数列表,多个参数之间用逗号分隔,每个参数都包括参数名和类型。
  • 返回值列表returnType:函数的返回值列表,多个返回值之间用逗号分隔,每个返回值都包括返回值名和类型。
  • 函数体:函数执行的代码块。
go 复制代码
func Add(a, b int) (ans int) {
  ans = a + b
  return
}
​
func main {
  i := Add(4, 5)
  fmt.Println(i)
}

1.7.2 可变函数参数

在Go中,可以使用可变函数参数来编写能够接受可变数量的参数的函数。这种功能通常用于编写通用的函数,允许你传递不同数量的参数给函数。可变函数参数通过省略号 ... 表示。

go 复制代码
package main
​
import (
    "fmt"
)
​
// add 函数接受可变数量的整数参数并返回它们的总和
func add(numbers ...int) int {
    sum := 0
    for _, num := range numbers {
        sum += num
    }
    return sum
}
​
func main() {
    result1 := add(1, 2, 3)
    result2 := add(4, 5, 6, 7, 8)
    fmt.Println("Result 1:", result1) // 输出: 6
    fmt.Println("Result 2:", result2) // 输出: 30
}

add 函数接受可变数量的整数参数,通过使用省略号 ... 表示参数类型,这使得你可以传递不同数量的整数给函数。在函数体内,我们使用一个 for 循环来计算所有传递的整数的总和。

注意事项:

  1. 可变参数必须是最后一个参数,不能在参数列表的中间。
  2. 可变参数在函数内部被当作一个切片(slice)处理,你可以使用循环或其他切片操作来处理它们。
  3. 如果不传递任何参数给可变参数函数,它将接收一个空的切片。

1.7.3 匿名函数和闭包

匿名函数是一种没有名字的函数,它可以直接在代码中声明和使用,通常用于实现一些简单的功能或在函数内部定义其他函数。匿名函数也称为闭包(closures),因为它们可以捕获并访问其周围的变量。

go 复制代码
package main
​
import "fmt"
​
func main() {
    // 声明并调用匿名函数
    add := func(a, b int) int {
        return a + b
    }
​
    result := add(3, 5)
    fmt.Println(result) // 输出: 8
}

在上面的示例中,我们声明了一个匿名函数并将其分配给变量 add。然后,我们可以像调用常规函数一样调用它。

匿名函数可以访问其外部作用域的变量,这意味着它们可以捕获和使用周围的变量。

而在Go语言中,闭包(closures)是指函数可以访问其外部作用域的变量,并且在函数内部持有对这些变量的引用。这使得函数可以在其声明的范围之外保持对这些变量的状态。闭包通常用于需要在函数内部保存状态或数据的情况。

例如:

go 复制代码
package main
​
import "fmt"
​
func main() {
    x := 10
​
    // 声明并调用匿名函数,它可以访问外部变量 x
    increment := func() int {
        x++
        return x
    }
​
    fmt.Println(increment()) // 输出: 11
    fmt.Println(increment()) // 输出: 12
}

匿名函数 increment 捕获了外部变量 x,并且每次调用时都会递增 x 的值。

1.7.4 函数作为参数

在Go语言中,函数可以作为参数传递给其他函数,这是一种常见的编程模式,被称为高阶函数

go 复制代码
package main
​
import "fmt"
​
// 定义一个高阶函数,接受一个函数作为参数
func apply(numbers []int, f func(int) int) []int {
    result := []int{}
    for _, num := range numbers {
        result = append(result, f(num))
    }
    return result
}
​
// 一个示例函数,将整数加倍
func double(x int) int {
    return x * 2
}
​
// 另一个示例函数,将整数平方
func square(x int) int {
    return x * x
}
​
func main() {
    numbers := []int{1, 2, 3, 4, 5}
​
    // 将 double 函数作为参数传递给 apply
    doubled := apply(numbers, double)
    fmt.Println(doubled) // 输出: [2 4 6 8 10]
​
    // 将 square 函数作为参数传递给 apply
    squared := apply(numbers, square)
    fmt.Println(squared) // 输出: [1 4 9 16 25]
}

我们定义了一个名为 apply 的高阶函数,它接受一个整数切片和一个函数作为参数。apply 函数使用传递的函数对切片中的每个元素进行操作,并返回一个新的整数切片。

然后,再定义了两个示例函数 doublesquare,它们分别将整数加倍和平方。我们通过将这些函数作为参数传递给 apply 函数来使用它们。

再举一个🌰:

go 复制代码
package main
​
import (
  "fmt"
  "time"
)
​
func ExpensiveFuc(key string) string {
  time.Sleep(3 * time.Second)
  return "result for 1 " + key
}
​
func Cache(f func(string) string) func(string) string {
  cache := make(map[string]string)
  return func(key string) string {
    fmt.Println("key = ", key)
    // 如果存在就直接返回
    if v, ok := cache[key]; ok {
      fmt.Println("val = ", v)
      fmt.Println(ok)
      return v
    }
    // 不存在就调用函数
    v := f(key)
    cache[key] = v
    return v
  }
}
​
func main() {
  pro := Cache(ExpensiveFuc)
  result := pro("hahaha")
  fmt.Println(result)
  result2 := pro("hahaha")
  result3 := pro("hahaha")
  fmt.Println(result2, result3)
}

Cache 过程是怎么样的?

  1. 首先调用Cache函数,传入的参数是ExpensiveFuc函数
  2. 然后Cache函数返回一个匿名函数,这个匿名函数的参数是string类型,返回值也是string类型
  3. 然后调用匿名函数,传入的参数是hahaha,返回值是result
  4. 匿名函数内部首先判断cache中是否存在hahaha这个key,如果存在就直接返回,如果不存在就调用ExpensiveFuc函数
  5. 调用ExpensiveFuc函数,传入的参数是hahaha,返回值是result for 1 hahaha
  6. 将result for 1 hahaha存入cache中,然后返回result for 1 hahaha
  7. 将result for 1 hahaha赋值给result
  8. 打印result

1.7.5 defer

在Go语言中,defer 语句用于安排一个函数调用在当前函数执行结束后再执行,即使在发生错误或return语句之后也会执行。defer 通常用于确保一些清理工作或资源释放操作在函数结束时被执行,以保持代码的可维护性和可靠性。

以下是一些关于defer的重要特性和示例用法:

  1. 执行顺序 :如果有多个defer语句,它们会按照后进先出(LIFO)的顺序执行,即最后一个defer语句会最先执行,依此类推。
  2. 用途defer常用于关闭文件、释放资源、解锁互斥锁、记录日志、恢复恐慌(panic)等场景。
go 复制代码
package main
​
import "fmt"
​
func main() {
    defer fmt.Println("World") // 这个语句会在 main 函数结束之前执行
    fmt.Println("Hello")       // 这个语句会在上面的 defer 之前执行
}

在这个示例中,fmt.Println("World")被推迟执行,因此它会在main函数结束之前执行,即使它出现在fmt.Println("Hello")之后。

另一个示例,演示了defer用于关闭文件的情况:

go 复制代码
package main
​
import (
    "fmt"
    "os"
)
​
func main() {
    file, err := os.Create("example.txt")
    if err != nil {
        fmt.Println("Error:", err)
        return
    }
    defer file.Close() // 确保文件在函数结束前关闭
​
    fmt.Fprintln(file, "Hello, Go!") // 写入文件
}

在上述示例中,defer file.Close()确保文件在main函数结束之前关闭,即使在函数中出现了错误也会执行关闭操作。

1.7.6 error

在Go语言中,错误处理是一种重要的编程概念,用于处理和报告函数运行中的错误。Go使用内置的error类型来表示错误,通常通过返回一个error值来表示函数是否成功执行,以及在出现错误时提供错误信息。

go 复制代码
package main
​
import (
    "errors"
    "fmt"
)
​
func divide(a, b float64) (float64, error) {
    if b == 0 {
        return 0, errors.New("division by zero") 
    }
    return a / b, nil
}
​
func main() {
    result, err := divide(6, 2)
    if err != nil {
        fmt.Println("Error:", err)
    } else {
        fmt.Println("Result:", result)
    }
​
    result, err = divide(3, 0)
    if err != nil {
        fmt.Println("Error:", err) // 输出: Error: division by zero
    } else {
        fmt.Println("Result:", result)
    }
}
go 复制代码
return 0, errors.New("division by zero")
// 替代成具有值的错误
return 0, fmt.Errorf("can not divide by zero a = %v, b = %v", a, b)

1.7.7 panic和recover

在Go语言中,panicrecover 是用于处理程序中的异常情况(例如运行时错误或恐慌)的两个关键机制。它们允许你在程序出现问题时执行一些特定的操作,以确保程序可以优雅地恢复或进行必要的清理。

  1. panic:

    • panic 是一个内置函数,用于引发运行时恐慌(panic)。

    • 当程序遇到无法处理的错误或不可恢复的情况时,可以使用 panic 来立即停止程序的执行,同时调用堆栈回溯并打印错误消息。

    • panic 可以接受任何类型的参数,通常是字符串,用于描述恐慌的原因。

    • panic 会导致程序立即停止执行,函数调用栈被展开,执行 defer 语句,然后程序终止。

    go 复制代码
    package main
    ​
    import "fmt"
    ​
    func main() {
        fmt.Println("Start of main")
    ​
        // 引发恐慌
        panic("Something went wrong!")
    ​
        fmt.Println("End of main") // 这行代码不会被执行
    }
  2. recover:

    • recover 也是一个内置函数,用于在发生恐慌后进行恢复。

    • 通常,recover 用于在 defer 语句中调用,以捕获并处理 panic 引发的错误。

    • 如果在 defer 中调用 recover,并且在发生恐慌时,它会停止恐慌的传播并返回 panic 的值。

    • 如果没有发生恐慌,recover 返回 nil

    • 通过检查 recover 的返回值,你可以决定是否采取恢复操作,例如记录错误并继续执行。

    go 复制代码
    package main
    ​
    import "fmt"
    ​
    func main() {
        fmt.Println("Start of main")
    ​
        // 在 defer 中使用 recover 来捕获 panic
        defer func() {
            if r := recover(); r != nil {
                fmt.Println("Recovered from panic:", r)
            }
        }()
    ​
        // 引发恐慌
        panic("Something went wrong!")
    ​
        fmt.Println("End of main") // 这行代码不会被执行
    }
        ```

在上述示例中,recover 函数在 defer 中用于捕获 panic 引发的错误,并记录该错误。这可以防止程序因恐慌而崩溃,使其能够进行适当的清理操作或错误处理。

需要注意的是,recover 只在 defer 中有效,且只能捕获当前协程(goroutine)中的 panic。不能用于捕获其他协程中的恐慌。

什么情况下使用panic?

初始化:

go 复制代码
package main
​
import (
    "database/sql"
    "fmt"
    "log"
​
    _ "github.com/go-sql-driver/mysql"
)
​
func main() {
    db, err := sql.Open("mysql", "user:password@tcp(127.0.0.1:3306)/database_name")
    if err != nil {
        panic(fmt.Errorf("无法连接数据库:%w", err))
    }
    defer db.Close()
​
    // ...
}

总之:

  • 尽量避免使用 panic。
  • 在 panic 之前,应该尽可能多地记录信息, 多打log。
  • 不要在 defer 中使用 panic。如果函数中有多个 defer 语句,并且其中一个 defer 中使用了 panic,那么所有的 defer 都将被执行,但是 panic 将在其他 defer 执行完成之后才会被触发。

1.7.8 errors包下面的方法

go 复制代码
import "errors"
​
func doSomething() error {
    return errors.New("something went wrong")
}
1.7.8.1 Is

Is 函数用于判断一个错误对象 err 是否是某个特定错误对象 target。如果 errtarget 或者 err 的一个包装错误对象,那么 Is 函数返回 true,否则返回 false

go 复制代码
import (
    "errors"
    "os"
)
​
func readFile(filename string) error {
    file, err := os.Open(filename)
    if err != nil {
        if errors.Is(err, os.ErrNotExist) {
            return errors.New("file does not exist")
        }
        return err
    }
    defer file.Close()
​
    // do something with file
    return nil
}
1.7.8.2 As

As 函数用于将一个错误对象 err 转换为某个特定的类型,并将其赋值给 target。如果 err 可以被转换为 target 的类型,那么 As 函数返回 true,否则返回 false

go 复制代码
type MyError struct {
    message string
}
​
func (e *MyError) Error() string {
    return e.message
}
​
func doSomething() error {
    return &MyError{"something went wrong"}
}
func main() {
    err := doSomething()
​
    var myErr *MyError
    if errors.As(err, &myErr) {
        // handle myErr
    } else {
        // handle other errors
    }
}
1.7.8.3 Wrap

直接使用Errorf即可:

go 复制代码
import "fmt"
​
func doSomething() error {
    err := doSomethingElse()
    if err != nil {
        return fmt.Errorf("failed to do something: %w", err)
    }
    return nil
}
1.7.8.4 Unwarp

Unwrap 函数用于返回一个错误对象的底层错误对象,该底层错误对象是通过 Wrap 函数包装而成的。如果该错误对象没有被包装过,Unwrap 函数返回 nil

go 复制代码
func main() {
    err := doSomething()
    if err != nil {
        fmt.Printf("error: %s\n", err.Error())
​
        if underlyingErr := errors.Unwrap(err); underlyingErr != nil {
            fmt.Printf("underlying error: %s\n", underlyingErr.Error())
        }
    }
}
func main() {  
   err := fmt.Errorf("main error: %w", errors.New("unknown error"))  
   fmt.Println(err)  
​
   underlyingErr := errors.Unwrap(err)  
   fmt.Println(underlyingErr)  
}

例子:

go 复制代码
func wrapError(err error) error {  
   return fmt.Errorf("err: %w\n%v", err, string(debug.Stack()))  
}  
​
func add(a, b int) (int, error) {  
   if a < 0 || b < 0 {  
      return 0, wrapError(errors.New("negative number"))  
   }  
   return a + b, nil  
}  
​
func addApi() (int, error) {  
   res, err := add(-1, 2)  
   if err != nil {  
      log.Printf("add failed, err: %v", err)  
      return res, errors.Unwrap(err)  
   } else {  
      log.Printf("add success, res: %v", res)  
      return res, nil  
   }  
}  
​
func main() {  
   addApi()  
}

1.7.9 package

一个Go程序可以包含多个源文件,但是每个源文件只能属于一个包。一个包可以由多个文件组成,但是这些文件必须在同一个目录下,并且文件名必须以.go为后缀。在一个包中,可以定义变量、常量、函数、类型等。这些定义可以被其他包引用和使用。

Go语言中的包名必须是唯一的,且建议使用小写字母来命名包名。

go 复制代码
import (
    "fmt"
    rand1 "math/rand"
    _ "xxxxx/driver"
)

在Go语言中,package之间是不允许循环引用的。

init 函数

每个package都可以定义一个或多个名为init的函数。这些函数的作用是在程序启动时自动执行一些初始化操作,例如初始化配置信息、注册一些变量等。

init函数的定义必须满足以下两个条件:

  1. 函数名必须为init
  2. 函数没有参数和返回值。

在一个package中,可以定义多个init函数,这些函数会按照定义的顺序自动执行。如果一个package中定义了多个init函数,会按照它们在文件中出现的顺序依次执行。

go 复制代码
// main.go
package main

import (
  "fmt"
  _ "practice/initPack1"
)


func init() {
  fmt.Println("main init()")
}
go 复制代码
// practice/initPack1

package initPack1

import (
  "fmt"
  _ "practice/initPack2"
)

func init() {
  fmt.Println("initPack1.go init()")
}
go 复制代码
// practice/initPack2

package initPack2

import (
  "fmt"
)

func init() {
  fmt.Println("initPack2.go init()")
}

1.7.10 go module

Go mod是一个用于管理Go语言模块依赖性的工具,它是Go 1.11版本中引入的。通过使用Go mod,可以更轻松地管理其代码库中的依赖项,同时还可以更加精细地控制依赖性的版本。

Go mod使用一个文件名为go.mod的文件来管理依赖项。运行go build、go test或go run等命令时,Go mod会自动下载所有必需的依赖项并将其存储在$GOPATH/pkg/mod目录中。通过使用go.mod文件,可以指定特定的依赖版本或版本范围,并通过执行go mod tidy命令来自动升级依赖项。

go 复制代码
go mod init:在当前目录中初始化go.mod文件,用于管理依赖项。
go mod tidy:根据go.mod文件中的依赖关系清理模块,移除未使用的依赖项,并添加缺失的依赖项。
go mod download:下载go.mod文件中列出的所有依赖项,但不安装它们。
go mod verify:验证下载的依赖项的完整性和正确性。
go mod graph:以图形化形式显示模块之间的依赖关系。
go mod why:显示特定模块为什么需要特定依赖项。

1.8 面向对象编程

1.8.1 struct

在Go语言中,struct(结构体)是一种复合数据类型,用于组合不同类型的字段以创建自定义的数据结构。struct允许你将多个变量(字段)组合在一起,以便更灵活地表示和操作数据。

1.8.1.1 struct结构体的定义及初始化
  1. 定义结构体:

    可以使用 type 关键字来定义一个结构体类型。结构体的定义通常包含字段名和字段类型。例如:

    go 复制代码
    type Person struct {
        FirstName string
        LastName  string
        Age       int
    }

    在这个示例中,我们定义了一个名为 Person 的结构体,它包含三个字段:FirstName(字符串类型)、LastName(字符串类型)和 Age(整数类型)。

  2. 初始化struct:

    go 复制代码
    p := Person{
        FirstName: "John",
        LastName:  "Doe",
        Age:       30,
    }
  3. 访问结构体字段:

    要访问结构体的字段,可以使用.操作符。例如:

    go 复制代码
    fmt.Println(p.FirstName) // 输出: John
    fmt.Println(p.Age)       // 输出: 30
  4. 修改结构体字段:

    同样使用.操作符。例如:

    go 复制代码
    p.Age = 31
1.8.1.2 结构体指针

为什么需要结构体指针?

不妨,先思考一个问题。如果,我们有一个想法:

  1. 首先,将结构体初始化的值赋给别的变量
  2. 其次,改变结构体中已经初始化的值

那么,打印这两个变量会发生什么呢?会不会改变原来的值呢?

go 复制代码
p := Person{
  FirstName: "John",
  LastName:  "Doe",
  Age:       30,
}
p1 := p
p1.FirstName = "Jane"
fmt.Println(p1)
fmt.Println(p)

结果,我们会看到,p1的值已经发生改变了,但是原来的值并没有发生改变。修改的只是p1的数据,并没有与p发生关联。

而我们想要的是,使用一块数据,那么该如何去写?

很简单,直接用结构体指针即可:

go 复制代码
p := &Person{
  FirstName: "John",
  LastName:  "Doe",
  Age:       30,
}
1.8.1.3 method

在Go语言中,方法(Method)是与结构体(Struct)或其他类型关联的函数。方法允许你为某种类型定义特定的行为。在Go中,方法是函数,但它们与特定类型关联,因此可以通过该类型的实例来调用。

go 复制代码
// 方法定义的语法为
func (receiver Type) methodName(parameters) returnType

其中receiver是方法关联的类型,methodName是方法的名称,parameters是方法的参数列表,returnType是方法的返回类型。

举个🌰

go 复制代码
package main
​
import "fmt"
​
// 定义一个结构体
type Rect struct {
    Width  float64
    Height float64
}
​
// 定义Rect结构体的方法,计算面积
func (r Rect) Area() float64 {
    return r.Width * r.Height
}
​
func main() {
    // 创建一个Rect类型的实例
    rectangle := Rect{Width: 5.0, Height: 3.0}
​
    // 调用Rect类型的方法来计算面积
    area := rectangle.Area()
​
    // 打印面积
    fmt.Println("矩形的面积是:", area)
}

在上面的示例中,我们首先定义了一个名为Rect的结构体,它有两个字段WidthHeight

然后,我们为Rect类型定义了一个名为Area的方法,该方法用于计算矩形的面积。

main函数中,我们创建了一个Rect类型的实例,并调用Area方法来计算并打印矩形的面积。

1.8.1.4 结构体的嵌套

在Go语言中,可以在一个类型中嵌套其他类型,并且可以为嵌套的类型定义方法。这种嵌套类型的方法称为嵌套方法。嵌套方法允许在一个类型中包含另一个类型的行为,从而实现组合和复用代码的目的。

go 复制代码
package main
​
import "fmt"
​
// 定义一个Person结构体
type Person struct {
    FirstName string
    LastName  string
}
​
// 定义一个Address结构体
type Address struct {
    Street     string
    City       string
    PostalCode string
}
​
// 在Person结构体中嵌套Address结构体,并为Person定义一个方法
type Contact struct {
    Person
    Email   string
    Address // 嵌套Address结构体
}
​
// 为Contact类型定义一个方法
func (c Contact) FullName() string {
    return c.FirstName + " " + c.LastName
}
​
func main() {
    // 创建一个Contact类型的实例
    contact := Contact{
        Person: Person{
            FirstName: "John",
            LastName:  "Doe",
        },
        Email: "john.doe@example.com",
        Address: Address{
            Street:     "123 Main St",
            City:       "Anytown",
            PostalCode: "12345",
        },
    }
​
    // 调用Contact类型的方法
    fullName := contact.FullName()
​
    // 打印完整姓名
    fmt.Println("完整姓名:", fullName)
​
    // 访问嵌套的Address结构体字段
    fmt.Println("地址:", contact.Street, contact.City, contact.PostalCode)
}

在上面的示例中,我们定义了三个结构体类型:PersonAddressContact。然后,我们在Contact结构体中嵌套了Person结构体,这意味着Contact类型将继承Person类型的字段和方法。我们为Contact类型定义了一个名为FullName的方法,该方法可以访问Person类型的字段,并返回完整的姓名。

main函数中,我们创建了一个Contact类型的实例,并调用了FullName方法,以及访问了嵌套的Person结构体字段。

1.8.2 interface

1.8.2.1 interface嵌套

在Go中,可以嵌套接口,类似于嵌套结构体,以构建更复杂的接口。嵌套接口使你能够在一个接口中包含另一个或多个接口的所有方法。这可以用于组合和扩展接口的行为。

go 复制代码
package main
​
import (
    "fmt"
)
​
// 定义一个基本的接口
type Animal interface {
    Speak() string
}
​
// 定义一个嵌套的接口,包含 Animal 接口和一个额外的方法
type Pet interface {
    Animal
    Name() string
}
​
// 实现 Animal 接口的结构体
type Dog struct{}
​
func (d Dog) Speak() string {
    return "Woof!"
}
​
// 实现 Pet 接口,满足 Pet 接口和 Animal 接口的要求
func (d Dog) Name() string {
    return "Fido"
}
​
func main() {
    // 创建一个 Dog 实例
    dog := Dog{}
​
    // 使用 Pet 接口来调用 Speak 和 Name 方法
    pet := Pet(dog) // 将 Dog 类型强制转换为 Pet 接口
    fmt.Println("宠物的名字:", pet.Name())
    fmt.Println("宠物的声音:", pet.Speak())
}

我们创建了一个 Dog 实例,并将其强制转换为 Pet 接口,然后使用 Pet 接口来调用Dog结构体中的 SpeakName 方法。这样,我们使用了 Pet 接口,并成功调用了 Dog 结构体中实现的方法。

1.8.2.2 接口的类型转换和类型断言

类型转换(Type Conversion)

类型转换用于将一个接口类型的值转换为另一种具体类型的值。

go 复制代码
package main
​
import "fmt"
​
type Eater interface {
  Eat()
}
​
type Animal interface {
  Eater
  Sleep()
}
​
type Dog struct {
  Name string
}
​
func (d Dog) Eat() {
  fmt.Printf("%s is eating\n", d.Name)
}
​
func (d Dog) Sleep() {
  fmt.Printf("%s is sleeping\n", d.Name)
}
​
type Cat struct {
  Name string
}
​
func (c Cat) Eat() {
  fmt.Printf("%s is eating\n", c.Name)
}
​
func (c Cat) Sleep() {
  fmt.Printf("%s is sleeping\n", c.Name)
}
​
func AnimalEat(a Eater) {
  // 这里的a只是Eater类型,也就是说只有Eater的Eat函数
  // 那么,我还想使用Sleep函数怎么办,那么就得需要类型转换
  a.Eat()
  a.(Animal).Sleep()
}
​
func main() {
  d := Dog{"dog"}
  AnimalEat(d)
  c := Cat{"cat"}
  AnimalEat(c)
}

类型断言(Type Assertion)

类型断言是在运行时将接口值转换为其底层具体类型的过程,并同时检查是否成功。如果底层类型匹配,那么转换就成功了,否则它会返回一个panic。

go 复制代码
value, ok := interfaceValue.(ConcreteType)
  • interfaceValue 是要转换的接口值。
  • ConcreteType 是目标具体类型。
  • value 是转换后的底层具体类型值。
  • ok 是一个布尔值,指示是否成功进行了转换。

如果类型断言成功,ok 将为 true,并且value 将包含底层具体类型的值。如果类型断言失败,ok 将为 false,并且value 将为零值(zero value)。

go 复制代码
package main
​
import (
  "fmt"
)
​
type Shape interface {
  Area() float64
}
​
type Circle struct {
  Radius float64
}
​
func (c Circle) Area() float64 {
  return 3.14 * c.Radius * c.Radius
}
​
func main() {
  var shape Shape
  shape = Circle{Radius: 5.0}
​
  // 尝试将接口值 shape 转换为 Circle 类型,并检查是否成功
  if circle, ok := shape.(Circle); ok {
    fmt.Printf("圆的半径:%f\n", circle.Radius)
    fmt.Printf("圆的面积:%f\n", circle.Area())
  } else {
    fmt.Println("类型断言失败,不是 Circle 类型")
  }
}

断言失败:

go 复制代码
package main
​
import "fmt"
​
type Shape interface {
  Area() float64
}
​
type Circle struct {
  Radius float64
}
​
func (c Circle) Area() float64 {
  return 3.14 * c.Radius * c.Radius
}
​
type Square struct {
  SideLength float64
}
​
func (s Square) Area() float64 {
  return s.SideLength * s.SideLength
}
​
func main() {
  // 创建一个 Circle 实例
  circle := Circle{Radius: 5.0}
​
  // 将 Circle 实例分配给 Shape 接口变量
  var shape Shape
  shape = circle
​
  // 尝试将 Shape 接口值断言为 Square 类型,这将失败
  square, ok := shape.(Square)
​
  if ok {
    // 断言成功,但不会执行到这里
    fmt.Println("这是一个正方形")
  } else {
    // 断言失败,会执行到这里
    fmt.Println("这不是一个正方形")
  }
}

在示例中我们定义了一个 Square 结构体和它的 Area 方法,以及 Circle 结构体和它的 Area 方法。在 main 函数中,我们创建了一个 Circle 实例,并尝试将 shape 接口值断言为 Square 类型,但实际上,它不是 Square 类型。因此,断言失败,并且程序会执行到 else 分支,打印 "这不是一个正方形"。

1.8.2.3 判断interface是否为nil

在Go语言中,判断一个接口是否为nil要注意几点:

  1. 如果接口类型变量未初始化,即尚未赋值为具体值或nil,那么它将是nil
  2. 如果一个接口类型变量已被赋值为一个具体的非nil值,即使它的底层值是nil,这个接口变量仍然不是nil
go 复制代码
package main
​
import "fmt"
​
type MyInterface interface {
    DoSomething()
}
​
type MyStruct struct {
    // 一些字段
}
​
func (s MyStruct) DoSomething() {
    fmt.Println("正在做一些事情")
}
​
func main() {
    // 示例1: 未初始化的接口变量是nil
    var myInterface1 MyInterface
    if myInterface1 == nil {
        fmt.Println("myInterface1 是 nil")
    } else {
        fmt.Println("myInterface1 不是 nil")
    }
​
    // 示例2: 初始化的接口变量,底层值为nil,但接口不是nil
    var myStruct MyStruct
    var myInterface2 MyInterface
    myInterface2 = myStruct
    if myInterface2 == nil {
        fmt.Println("myInterface2 是 nil")
    } else {
        fmt.Println("myInterface2 不是 nil")
    }
​
    // 示例3: 初始化的接口变量,底层值不为nil
    var myInterface3 MyInterface
    myStructWithValue := MyStruct{}
    myInterface3 = myStructWithValue
    if myInterface3 == nil {
        fmt.Println("myInterface3 是 nil")
    } else {
        fmt.Println("myInterface3 不是 nil")
    }
}
1.8.2.4 接口嵌入到结构体

在 Go 语言中,可以将一个接口嵌入到结构体中,这样结构体就会继承该接口的方法。这种方式称为接口的嵌入。嵌入接口可以让结构体实现接口的方法,而无需显式地声明这些方法。

go 复制代码
package main
​
import "fmt"
​
type Worker interface {
  doWork()
  Start()
}
​
type BaseWorker struct {
  // 抽象类
  Worker
}
​
func (b *BaseWorker) Start() {
  fmt.Println("before start")
  b.doWork()
  fmt.Println("finished")
}
​
type NormalWorker struct {
  BaseWorker
}
​
func (n *NormalWorker) doWork() {
  fmt.Println("do work")
}
​
func NewNormalWorker() Worker {
  n := &NormalWorker{BaseWorker{}}
  n.Worker = n
  return n
}
​
func main() {
  w := NewNormalWorker()
  w.doWork()
  w.Start()
}

定义了一个接口 Worker,包含了 doWork()Start() 两个方法。

然后,创建了一个名为 BaseWorker 的结构体,它嵌入了 Worker 接口,并实现了 Start() 方法。

接着,创建了一个名为 NormalWorker 的结构体,它继承了 BaseWorker 结构体并覆盖了 doWork() 方法。

NewNormalWorker 函数中,创建了一个 NormalWorker 实例,并将其赋值给 Worker 接口,然后返回该接口。

最后在 main 函数中,创建了一个 NormalWorker 实例 w,并通过 doWork()Start() 方法来调用它们。

注意点:

  1. n := &NormalWorker{BaseWorker{}}:这一行代码创建了一个新的 NormalWorker 类型的实例,并使用复合字面量创建了一个 BaseWorker 类型的实例,然后将该 BaseWorker 实例嵌入到 NormalWorker 中。

    • NormalWorker 嵌入了 BaseWorker,这意味着 NormalWorker 包含了 BaseWorker 的字段和方法。通过这种方式,NormalWorker 继承了 BaseWorker 的方法,包括 Start 方法。
  2. n.Worker = n:接下来,将 nNormalWorker 类型的实例)赋值给 n.Worker。这是因为 Worker 接口需要 doWorkStart 方法,而 NormalWorker 本身已经实现了 doWork 方法,但需要通过 BaseWorker 嵌入的 Start 方法来满足 Worker 接口的要求。

  3. return n:最后,将 n 返回,以便作为实现了 Worker 接口的对象使用。

整个过程的目的是确保 NormalWorker 类型的实例可以被视为 Worker 接口的实现。这种做法是一种结构型继承和接口实现的方式,允许通过复合和继承来构建更复杂的类型,同时确保它们满足接口的需求。

1.8.3 interface和struct的区别

1. struct(结构体):

  • 用途:结构体是一种用户自定义的数据类型,用于组织和存储数据。你可以在结构体中定义字段(成员变量),这些字段可以是不同的数据类型,用于表示对象的属性。

  • 特点:结构体是值类型(value type),它们在内存中占据一定的空间,可以存储数据。结构体可以有自己的方法,但是这些方法与特定的结构体关联。

  • 示例:用于创建自定义的数据结构,如用户、商品、汽车等。

    go 复制代码
    package main
    ​
    import "fmt"
    ​
    type Person struct {
        FirstName string
        LastName  string
        Age       int
    }
    ​
    func (p Person) FullName() string {
        return p.FirstName + " " + p.LastName
    }
    ​
    func main() {
        person := Person{
            FirstName: "John",
            LastName:  "Doe",
            Age:       30,
        }
    ​
        fmt.Println("Name:", person.FullName())
        fmt.Println("Age:", person.Age)
    }

    在这个示例中,我们定义了一个Person结构体,它包含了一个人的基本信息(名字和年龄),并定义了一个FullName方法来返回完整的名字。在main函数中,我们创建了一个Person对象并调用了它的方法。

2. struct(结构体):

  • 用途:接口是一种抽象类型,它定义了一组方法的签名,但没有实际的实现。接口用于描述对象的行为,而不关心对象的具体类型。

  • 特点:接口是隐式实现的,类型不需要显式声明它实现了某个接口。只要一个类型定义了接口中的所有方法,它就被认为是实现了该接口。一个类型可以同时实现多个接口。

  • 示例:用于实现多态、组合、依赖注入等。

    创建一个接口Shape,表示可以计算面积的形状:

    go 复制代码
    package main
    ​
    import "fmt"
    ​
    type Shape interface {
        Area() float64
    }
    ​
    type Circle struct {
        Radius float64
    }
    ​
    func (c Circle) Area() float64 {
        return 3.14 * c.Radius * c.Radius
    }
    ​
    type Rectangle struct {
        Width  float64
        Height float64
    }
    ​
    func (r Rectangle) Area() float64 {
        return r.Width * r.Height
    }
    ​
    func printArea(s Shape) {
        fmt.Println("Area:", s.Area())
    }
    ​
    func main() {
        circle := Circle{Radius: 5}
        rectangle := Rectangle{Width: 4, Height: 6}
    ​
        printArea(circle)
        printArea(rectangle)
    }

    在这个示例中,我们定义了一个Shape接口,它包含一个Area方法,表示可以计算面积的形状。然后,我们创建了两个结构体CircleRectangle,它们都实现了Area方法。在main函数中,我们创建了一个圆和一个矩形,并将它们传递给printArea函数,该函数接受一个Shape接口作为参数,并打印出形状的面积。

最终可以看到:

struct用于表示具体的数据结构,而interface用于定义通用的行为约定,可以由不同的类型实现。这就是它们的区别。

1.9 并发编程

1.9.1 goroutine

1.9.1.1 goroutine的基本使用方法

在 Go 语言中,"goroutine"(协程)是一种轻量级的线程,它允许在同一个程序中并发执行多个任务,而不需要显式地创建和管理操作系统线程。Goroutines 是 Go 语言并发模型的核心组成部分,它们可以用来执行异步任务,提高程序的并发性能。

先看一段花费10s执行的代码:

go 复制代码
package main
​
import (
  "fmt"
  "log"
  "time"
)
​
type User struct {
  Name     string
  Phone    string
  Position string
}
​
func GetBaseUserInfo(id string) (*User, error) {
  time.Sleep(5 * time.Second)
  return &User{
    Name:  "xiaoming",
    Phone: "123456789",
  }, nil
}
​
func GetPositionInfo(id string) (string, error) {
  time.Sleep(5 * time.Second)
  return "developer", nil
}
​
func GetUser(id string) (*User, error) {
  user, err := GetBaseUserInfo(id)
  if err != nil {
    return nil, err
  }
  position, err := GetPositionInfo(id)
  if err != nil {
    return nil, err
  }
  //fmt.Println(user, position)
  user.Position = position
  return user, nil
}
​
func main() {
  now := time.Now()
  user, err := GetUser("1")
  if err != nil {
    log.Fatalf("GetUser failed, %v ", err)
    return
  }
  //fmt.Println(time.Since(now))
  fmt.Println(time.Now().Sub(now).Seconds())
  fmt.Println(user)
}

在上方代码中,同时在GetBaseUserInfoGetPositionInfo函数中,都延迟了5s,结果导致了整体运行10s左右。

本质上,我们可以同时进行这两个函数,总共仅花费5s:这就用到了goroutine协程

goroutine的启动非常简单,只需要在函数或方法调用前加上关键字go即可:

go 复制代码
package main
​
import (
  "fmt"
  "log"
  "sync"
  "time"
)
​
type User struct {
  Name     string
  Phone    string
  Position string
}
​
func GetBaseUserInfo(id string) (*User, error) {
  time.Sleep(5 * time.Second)
  return &User{
    Name:  "xiaoming",
    Phone: "123456789",
  }, nil
}
​
func GetPositionInfo(id string) (string, error) {
  time.Sleep(5 * time.Second)
  return "developer", nil
}
​
func GetUser(id string) (*User, error) {
  var baseUserInfoErr error
  var user *User
  var wg sync.WaitGroup
  wg.Add(1)
  go func() {
    defer func() {
      wg.Done()
    }()
    user, baseUserInfoErr = GetBaseUserInfo(id)
  }()
​
  var positionInfoErr error
  var position string
  wg.Add(1)
  go func() {
    defer func() {
      wg.Done()
    }()
    position, positionInfoErr = GetPositionInfo(id)
  }()
  wg.Wait()
  if baseUserInfoErr != nil {
    return nil, baseUserInfoErr
  }
  if positionInfoErr != nil {
    return nil, positionInfoErr
  }
  user.Position = position
  return user, nil
}
​
func main() {
  now := time.Now()
  user, err := GetUser("1")
  if err != nil {
    log.Fatalf("GetUser failed, %v ", err)
    return
  }
  //fmt.Println(time.Since(now))
  fmt.Println(time.Now().Sub(now).Seconds())
  fmt.Println(user)
}
1.9.1.2 等待goroutine问题

等待Goroutine 结束是为了确保程序在主函数返回之前,所有的 Goroutines 都已经完成了它们的任务。如果不等待 Goroutine 结束,主函数可能会在 Goroutines 运行之前退出,导致 Goroutines 被提前终止,这可能会导致未完成的工作或资源泄漏。

等待 Goroutine 结束的主要方式是使用 sync.WaitGroup,这是 Go 标准库提供的一种同步机制。以下是为什么等待 Goroutine 结束很重要的一些原因:

  1. 确保所有任务完成:在某些情况下,你可能需要确保所有的并发任务都已完成,然后再继续后续操作。等待 Goroutine 结束是一种有效的方式来实现这一点。
  2. 资源清理:如果 Goroutine 在工作完成后需要释放资源(例如关闭文件、数据库连接等),等待 Goroutine 结束是确保资源被正确释放的关键步骤。
  3. 错误处理:等待 Goroutine 结束可以让你收集和处理 Goroutine 中可能发生的错误。如果 Goroutine 发生错误,你可以在主函数中获知,并采取相应的措施。
  4. 协作和同步:等待 Goroutine 结束也用于协调和同步不同任务之间的操作。你可能希望在某个 Goroutine 执行完成后才能开始另一个 Goroutine。

如果不等待goroutine结束的话...举个🌰:

go 复制代码
package main
​
import (
  "fmt"
  "time"
)
​
func WaitGoRoutine() {
  time.Sleep(5 * time.Second)
  fmt.Println("WaitGoRoutine finsihsed")
}
​
func main() {
  go WaitGoRoutine()
  fmt.Println("main finished")
}

可以看到,最终结果只打印了fmt.Println("main finished")由于不等待 Goroutine 结束,主函数就会在 Goroutines 运行之前退出,导致 Goroutines 被提前终止

所以,一定要结束Goroutine

go 复制代码
package main
​
import (
  "fmt"
  "sync"
  "time"
)
​
func WaitGoRoutine(wg *sync.WaitGroup) {
  defer func() {
    wg.Done()
  }()
  time.Sleep(5 * time.Second)
  fmt.Println("WaitGoRoutine finsihsed")
}
​
func main() {
  var wg sync.WaitGroup
  wg.Add(1)
  go WaitGoRoutine(&wg)
  wg.Wait()
  fmt.Println("main finished")
}
1.9.1.3 goruntine循环变量问题

在使用 Goroutines 时,循环变量问题是一个常见的陷阱。问题通常出现在循环中启动 Goroutines,其中 Goroutines 会访问循环变量的值。由于 Goroutines 是并发执行的,可能会导致 Goroutines 访问到不正确的变量值。

这个问题的根本原因是循环变量的作用域和生命周期。在循环中启动的多个 Goroutines 共享相同的循环变量,而且这些 Goroutines 的执行速度可能会不同,因此它们可能会在不同的时间点访问循环变量。这可能导致 Goroutines 访问到已经被修改的变量值,或者在某些情况下,所有 Goroutines 都访问到相同的最后一个值。

为了解决循环变量问题,可以采取以下方法:(本质跟map那节循环变量问题是一样的)

传递循环变量的副本:在启动 Goroutine 时,将循环变量的值复制到一个新的局部变量中,然后将该局部变量传递给 Goroutine。这样每个 Goroutine 都有自己的副本,不会共享相同的变量。

go 复制代码
for i := 0; i < 10; i++ {
    // 传递 i 的副本给 Goroutine
    go func(x int) {
        fmt.Println(x)
    }(i)
}
1.9.1.4 竞态条件

竞态条件(Race Condition)是多线程或多协程并发执行时可能发生的一种问题,它会导致程序的行为不确定性和错误。竟态条件通常涉及多个线程或协程同时访问和修改共享的资源,并且操作的顺序不受控制,因此可能会导致不一致或不正确的结果。

ini 复制代码
Goroutine A                          Goroutine B

1. read counter = 10                  2. read counter = 10
3. add 1 to counter = 11             4. add 1 to counter = 11
5. write counter = 11                  6. write counter = 11

Counter value: 11 (应该为12)

竞态条件的常见例子包括:

  1. 数据竞态:多个线程或协程同时读取和写入共享的变量,而没有足够的同步机制来确保操作的顺序。这可能导致意外的值写入共享变量,或者读取到不一致的数据。
  2. 死锁:多个线程或协程相互等待对方释放资源,导致程序无法继续执行。
  3. 活锁:线程或协程在竞争资源时不断重试,导致一直无法取得成功。
  4. 饥饿:某些线程或协程一直无法获得所需的资源,因为其他线程或协程一直占用资源。
go 复制代码
package main
​
import (
  "fmt"
  "sync"
  "time"
)
​
var counter int64
​
func main() {
  var wg sync.WaitGroup
  for i := 0; i < 1000; i++ {
    wg.Add(1)
    go func() {
      counter++
      wg.Done()
    }()
  }
  wg.Wait()
  fmt.Println("counter = ", counter)
}

避免竟态条件的常见方法包括:

  1. 互斥锁:使用互斥锁(Mutex)或其他同步机制来保护共享资源,确保一次只有一个线程或协程可以访问该资源。
  2. 条件变量:使用条件变量来实现线程或协程之间的协调和通信,以避免竞争条件。
  3. 原子操作:使用原子操作来执行针对共享变量的不可分割操作,从而避免竞态条件。
  4. 不可变数据:使用不可变数据结构,这些结构不会被修改,从而避免并发写入问题。
  5. 并发安全的数据结构:使用并发安全的数据结构,这些数据结构已经实现了适当的同步机制,可以在并发环境中安全使用。
  6. 良好的设计:设计程序时要考虑并发问题,避免共享状态和共享资源的过度使用,尽量减少竞争条件的发生。

解决方式:

  1. var counter atomic.Int64 以及 atomic.AddInt64(&counter, 1)原子

    go 复制代码
    var counter atomic.Int64
    ​
    func main() {
      var wg sync.WaitGroup
      for i := 0; i < 1000; i++ {
        wg.Add(1)
        go func() {
          counter.Add(1)
          wg.Done()
        }()
      }
      wg.Wait()
      fmt.Println("counter = ", counter.Load())
    }
    go 复制代码
    var counter int64
    ​
    func main() {
      var wg sync.WaitGroup
      for i := 0; i < 1000; i++ {
        wg.Add(1)
        go func() {
          atomic.AddInt64(&counter, 1)
          wg.Done()
        }()
      }
      wg.Wait()
      fmt.Println("counter = ", counter)
    }
  2. CAS

    本质上也是一个原子操作。

    go 复制代码
    package main
    ​
    import (
      "fmt"
      "sync"
      "sync/atomic"
      "time"
    )
    ​
    var counter int64
    ​
    func main() {
      var wg sync.WaitGroup
      for i := 0; i < 1000; i++ {
        wg.Add(1)
        go func() {
    ​
          for {
            old := atomic.LoadInt64(&counter)
            newNum := old + 1
            if atomic.CompareAndSwapInt64(&counter, old, newNum) {
              break
            }
          }
          wg.Done()
        }()
      }
      wg.Wait()
      fmt.Println("counter = ", counter)
    }
  3. 互斥锁

    go 复制代码
    package main
    ​
    import (
      "fmt"
      "sync"
      "time"
    )
    ​
    var counter int64
    var mutex sync.Mutex
    ​
    func main() {
      
    var wg sync.WaitGroup
      for i := 0; i < 1000; i++ {
        wg.Add(1)
        go func() {
          mutex.Lock()
          counter++
          mutex.Unlock()
          wg.Done()
        }()
      }
      wg.Wait()
      fmt.Println("counter = ", counter)
    }

1.9.2 sync包

1.9.2.1 Once

sync.Once 是 Go 语言标准库中的一个同步工具,它用于确保某个操作只会执行一次,无论有多少个 Goroutines 尝试执行该操作。

sync.Once 类型有一个 Do 方法,该方法接受一个函数作为参数。当 Do 方法被第一次调用时,它会执行传入的函数,并记住该函数已经被调用过。之后,无论多少次调用 Do 方法,传入的函数都不会再次执行。

go 复制代码
package main
​
import (
  "fmt"
  "sync"
  "time"
)
​
var (
  once sync.Once
  data []string
)
​
func loadData() {
  fmt.Println("Loading data...")
  data = []string{"data1", "data2", "data3"}
}
​
func getData() []string {
  time.Sleep(3 * time.Second)
  // 只有第一次调用 Do 时,传入的函数会被执行
  once.Do(loadData)
  once.Do(loadData)
  once.Do(loadData)
  return data
}
​
func main() {
  var wg sync.WaitGroup
  wg.Add(2)
  go func() {
    fmt.Println(getData())
    wg.Done()
  }()
  go func() {
    fmt.Println(getData())
    wg.Done()
  }()
  wg.Wait()
}
1.9.2.2 WaitGroup

sync.WaitGroup 是 Go 语言标准库中的一个同步工具,它用于等待一组 Goroutines 完成它们的工作,然后再继续执行主线程或其他操作。WaitGroup 是通过一个计数器来实现的,初始计数器值为 0,可以通过 Add 方法增加计数器的值,通过 Done 方法减少计数器的值,通过 Wait 方法等待计数器值达到零。

go 复制代码
package main
​
import (
  "fmt"
  "sync"
  "time"
)
​
func worker(id int, wg *sync.WaitGroup) {
  defer wg.Done() // 在 Goroutine 完成时减少计数器的值
​
  fmt.Printf("Worker %d is working...\n", id)
  time.Sleep(time.Second) // 模拟工作
  fmt.Printf("Worker %d has finished.\n", id)
}
​
func main() {
  var wg sync.WaitGroup
​
  // 启动多个 Goroutines
  for i := 1; i <= 5; i++ {
    wg.Add(1) // 增加计数器的值
    go worker(i, &wg)
  }
​
  // 等待所有 Goroutines 完成
  wg.Wait()
​
  fmt.Println("All workers have finished.")
}

worker 函数接收 *sync.WaitGroup 类型的参数 wg,并在函数内部使用 wg.Done() 来减少计数器的值。在主函数中,我们传递了 &wg,即 WaitGroup 的指针,以便在 worker 函数中能够修改同一个 WaitGroup 的计数器。

1.9.2.3 Mutex

sync.Mutex 是 Go 语言标准库中的一种互斥锁,用于在多个 Goroutines 之间保护共享资源,确保只有一个 Goroutine 能够访问该资源,从而避免竞态条件(Race Condition)和数据竞态(Data Race)。

互斥锁的基本操作包括两个方法:

  1. Lock():用于锁定互斥锁。当一个 Goroutine 调用 Lock() 时,如果锁已经被其他 Goroutine 锁定,它将会被阻塞,直到锁被解锁为止。一旦锁被成功锁定,其他尝试锁定的 Goroutine 将被阻塞。
  2. Unlock():用于解锁互斥锁。一旦 Goroutine 完成了对共享资源的操作,应该调用 Unlock() 来释放锁,以便其他 Goroutine 可以获得锁并访问共享资源。

互斥锁的典型用法是将它包含在一个结构体中,然后在结构体的方法中使用锁来保护共享资源。这确保了在同一时间只有一个 Goroutine 可以访问共享资源,从而避免了并发冲突。

go 复制代码
package main
​
import (
  "fmt"
  "sync"
)
​
type Counter struct {
  count int
  mu    sync.Mutex
}
​
func (c *Counter) Increment() {
  c.mu.Lock()
  defer c.mu.Unlock()
  c.count++
}
​
func (c *Counter) Value() int {
  c.mu.Lock()
  defer c.mu.Unlock()
  return c.count
}
​
func main() {
  var wg sync.WaitGroup
  c := &Counter{}
  for i := 1; i < 100; i++ {
    wg.Add(1)
    go func() {
      defer wg.Done()
      c.Increment()
    }()
  }
  wg.Wait()
  fmt.Println(c.Value())
}

Counter 结构体包含一个 count 字段和一个 sync.Mutex 类型的互斥锁。Increment 方法和 Value 方法分别用于增加计数器的值和读取计数器的值,并在操作之前和之后使用锁来保护共享资源。这确保了在并发情况下对计数器的操作是安全的。

1.9.2.4 Cond

sync.Cond(条件变量)是 Go 语言标准库中的一种同步原语,用于在多个 Goroutines 之间进行线程间通信和协调。条件变量允许一个 Goroutine 等待另一个 Goroutine 发送信号以进行某种操作或等待某种条件的发生。

sync.Cond 的主要方法包括:

  1. NewCond(l Locker):创建一个新的条件变量,并与给定的互斥锁 l 关联。条件变量需要与互斥锁一起使用,以确保在等待和发送信号时共享的数据是线程安全的。
  2. cond.Wait():在条件变量上等待,会阻塞当前 Goroutine,直到其他 Goroutine 调用 SignalBroadcast 来通知条件的变化。
  3. cond.Signal():发送信号给等待在条件变量上的一个 Goroutine,唤醒其中一个等待的 Goroutine。如果没有 Goroutine 在等待,信号会被丢弃。
  4. cond.Broadcast():发送信号给等待在条件变量上的所有 Goroutines,唤醒所有等待的 Goroutine。

使用条件变量通常需要结合互斥锁一起使用,以确保在发送和接收信号时数据的一致性和线程安全性。

go 复制代码
package main
​
import (
  "sync"
  "time"
)
​
func main() {
  lock := sync.Mutex{}
  cond := sync.NewCond(&lock)
​
  ready := false
  go func() {
    for !ready {
      cond.L.Lock()
      cond.Wait()
      cond.L.Unlock()
    }
​
    println("ready1")
  }()
​
  go func() {
    for !ready {
      cond.L.Lock()
      cond.Wait()
      cond.L.Unlock()
    }
​
    println("ready2")
  }()
​
  go func() {
    time.Sleep(time.Second)
    ready = true
    cond.Broadcast()
  }()
​
  time.Sleep(time.Second * 4)
​
}

有两个 Goroutines ("ready1" 和 "ready2")在等待条件变量 cond 发送信号,并在信号到达时打印消息。此外,还有一个 Goroutine 在等待一段时间后发送广播信号,通知所有等待的 Goroutines。

尽管可能期望两个 Goroutines 同时接收到广播信号并打印消息,但实际情况可能不是这样。在 Go 中,Goroutines 的执行顺序是不确定的,因此无法保证哪个 Goroutine 会先被唤醒。

在代码中,第一个接收到广播信号的 Goroutine("ready1" 或 "ready2")将立即获得互斥锁 cond.L 并打印消息,而另一个 Goroutine 将继续等待下一个机会。

总之,在多个 Goroutines 等待条件变量时,不能依赖于特定的 Goroutine 先被唤醒,因为 Goroutines 的执行顺序是不确定的。这是 Go 并发编程的一个重要概念,需要在设计中考虑到这一点。如果需要确保某个 Goroutine 先被唤醒,可以使用其他同步机制,例如 sync.Mutexsync.WaitGroup 来明确定序。

再举一个🌰:

go 复制代码
package main
​
import (
    "fmt"
    "sync"
    "time"
)
​
func main() {
    var lock sync.Mutex
    cond := sync.NewCond(&lock)
    ready := false
​
    for i := 1; i < 10; i++ {
        i := i // 创建局部变量 i,以便在闭包中使用
        go func() {
            fmt.Printf("goroutine%d start\n", i)
​
            // 等待条件满足或被唤醒
            for {
                // 使用互斥锁保护对 ready 的访问
                lock.Lock()
                if ready {
                    lock.Unlock() // 如果条件满足,释放锁
                    break         // 跳出循环
                }
                // 如果条件不满足,等待条件变量的信号
                cond.Wait()
                lock.Unlock() // 释放锁以允许其他 Goroutine 获取锁并发送信号
            }
​
            // 当条件满足时,打印完成信息
            fmt.Printf("goroutine%d done\n", i)
        }()
    }
​
    // 主线程休眠 2 秒
    time.Sleep(2 * time.Second)
​
    // 设置条件为满足,并发送信号给所有等待的 Goroutines
    lock.Lock()
    ready = true
    cond.Broadcast()
    lock.Unlock()
​
    // 主线程休眠 10 秒,以确保所有 Goroutines 有足够的时间完成工作
    time.Sleep(10 * time.Second)
}
  • sync.Mutex 用于创建互斥锁,以确保对 ready 变量的访问是线程安全的。
  • sync.NewCond(&lock) 用于创建一个条件变量 cond,它与互斥锁 lock 关联。
  • 循环中的每个 Goroutine 使用匿名函数来执行任务,其中包含了等待条件满足的逻辑。
  • 等待条件的循环中,使用互斥锁来保护对 ready 变量的读取。如果条件满足,Goroutine 释放锁并跳出循环,否则它等待条件变量的信号。
  • 主线程休眠 2 秒,然后设置 ready 变量为 true 并发送信号给所有等待的 Goroutines。
  • 主线程再次休眠 10 秒,以确保所有 Goroutines 有足够的时间完成工作。
步骤 执行内容 执行 Goroutine 执行结果
1 主函数 main() 开始执行 - -
2 创建互斥锁 lock 和条件变量 cond,以及布尔变量 ready - -
3 启动 9 个 Goroutines,每个 Goroutine 执行一个匿名函数 - -
4 打印 Goroutine 的开始信息,例如 "goroutine1 start" Goroutine 1 -
5 进入无限循环,等待 ready 变为 true 或被唤醒 Goroutine 1 -
6 主函数执行 time.Sleep(2 * time.Second) 使主线程休眠 2 秒 - -
7 主线程休眠结束,设置 ready 变量为 true,然后调用 cond.Broadcast() 发送信号 - -
8 所有等待的 Goroutines 竞争获取互斥锁 lock 并检查 ready 变量,然后退出等待循环 Goroutines 1-9 打印完成信息,例如 "goroutine1 done"
9 主线程再次休眠 10 秒,以确保所有 Goroutines 有足够的时间完成工作 - -
1.9.2.5 Pool

sync.Pool 是 Go 标准库中的一个并发安全的对象池,用于存储和复用临时对象,以减少对象的创建和垃圾回收开销。它通常用于缓存需要频繁创建和销毁的对象,例如临时的大内存分配或数据库连接。

sync.Pool 具有以下特点:

  1. 并发安全:sync.Pool 可以在多个 Goroutine 中并发使用,它内部使用互斥锁来确保安全的访问。
  2. 自动缓存和回收:sync.Pool 会自动管理对象的缓存和回收,无需手动操作。
  3. 无法控制对象生命周期:sync.Pool 并不保证对象的生命周期,对象可能会在任何时候被垃圾回收。因此,不能依赖于对象的持久性。
  4. 提供 New 函数:通过 New 函数,可以指定如何创建新的对象。当池中没有可用对象时,会调用 New 函数创建一个新的对象。
  5. 用途广泛:sync.Pool 可以用于缓存各种类型的对象,包括结构体、连接、临时数据等。
go 复制代码
package main
​
import (
  "fmt"
  "sync"
)
​
type MyStruct struct {
  Name string
  age  int
}
​
func main() {
  pool := sync.Pool{
    New: func() interface{} {
      return &MyStruct{}
    },
  }
  // Get()返回的是一个interface,所以我们要进行强制类型转换,因为返回的值是一个指针,所以我们也得用指针
  // 那么,obj返回的也是一个指针
  obj := pool.Get().(*MyStruct)
  obj.Name = "小明"
  obj.age = 18
  pool.Put(obj)
  obj2 := pool.Get().(*MyStruct)
​
  fmt.Println(obj2.Name)
  fmt.Println(obj2.age)
}
步骤 执行内容 执行结果 说明
1 创建 sync.Pool - 创建一个空的对象池。
2 pool.Get() - 从对象池中获取一个对象,返回值类型为 interface{}
3 强制类型转换:obj := pool.Get().(*MyStruct) - 对获取的对象进行类型断言,转换为 *MyStruct 指针类型。
4 设置对象属性:obj.Name = "小明"obj.age = 18 - 在获取的对象上设置 Nameage 属性。
5 pool.Put(obj) - 将对象放回对象池。
6 pool.Get() - 再次从对象池中获取一个对象。
7 强制类型转换:obj2 := pool.Get().(*MyStruct) - 对获取的对象进行类型断言,转换为 *MyStruct 指针类型。
8 打印对象属性:fmt.Println(obj2.Name)fmt.Println(obj2.age) 小明 和 18 打印从对象池中获取的对象的属性值。

注意事项:

  • 步骤 3 和步骤 7 中的类型断言是必要的,因为 sync.Pool 返回的对象类型是 interface{},需要将其强制转换为原始对象类型。
  • 步骤 5 中的 pool.Put(obj) 将对象放回对象池,以便后续重用。
  • 由于 sync.Pool 的对象生命周期不受控制,因此在获取对象后,需要小心处理,确保不会在对象被回收后再次使用。
  • 在实际应用中,sync.Pool 主要用于临时对象的复用,以减少对象分配和垃圾回收的开销,提高性能。
  • sync.Pool 并不保证池中的对象一直可用。它可以在任何时候清空池,或者从池中移除对象。当从池中获取对象时,需要在使用之前将对象状态重置为默认值,以确保对象处于正确的状态。
1.9.2.6 Map

sync.Map 是 Go 语言标准库中的一种并发安全的键值对存储结构,用于在多个 Goroutine 中安全地存储和检索数据。相对于 map 类型,它具有更好的并发性能,可以在多个 Goroutine 中同时读写而无需额外的锁。

sync.Map 具有以下特点和用法:

  1. 并发安全:sync.Map 内部使用了锁机制来保证在并发环境下对数据的安全访问。多个 Goroutine 可以同时读取数据,但写入数据时会进行加锁操作。
  2. 无需额外的锁:相对于传统的 mapsync.Map 不需要额外的读写锁,因此更加高效,特别适合在高并发环境中使用。
  3. 动态键值对存储:sync.Map 可以动态地存储键值对,键和值的类型可以是任意类型,不需要提前定义。
  4. 安全的并发操作:通过 Load(读取)、Store(写入)、Delete(删除)等方法来操作键值对,这些操作都是并发安全的。
  5. 遍历键值对:可以使用 Range 方法来遍历 sync.Map 中的所有键值对。
  6. 适用于读多写少的场景:sync.Map 在读多写少的场景下性能较好,如果写入操作较频繁,可能会导致性能下降。
go 复制代码
package main
​
import (
  "fmt"
  "sync"
)
​
func main() {
  // 创建
  var m sync.Map
  // 添加
  m.Store("name", "小明")
  m.Store("age", 18)
  m.Store("country", "China")
  // 获取
  val, ok := m.Load("name")
  if ok {
    fmt.Println(val, "name的值存在")
  }
  // 删除
  m.Delete("country")
  // 遍历
  m.Range(func(key, value interface{}) bool {
    fmt.Println(key, value)
    return true
  })
}
步骤 执行内容 执行结果 说明
1 创建 sync.Map - 创建一个空的 sync.Map 对象。
2 使用 m.Store("name", "小明")m.Store("age", 18)m.Store("country", "China") 添加键值对 - sync.Map 中添加了三个键值对。
3 使用 m.Load("name") 获取键为 "name" 的值 "小明" 和 true 通过 Load 方法获取键为 "name" 的值,并返回 true 表示键存在。
4 使用 m.Delete("country") 删除键为 "country" 的键值对 - 删除了键为 "country" 的键值对。
5 使用 m.Range 遍历 sync.Map 中的所有键值对 - 遍历 sync.Map 中的所有键值对,并打印它们。

在这个示例中,创建了一个 sync.Map,并使用 Store 方法添加了三个键值对。然后,使用 Load 方法获取了 "name" 键的值,并使用 Delete 方法删除了 "country" 键的键值对。最后,通过 Range 方法遍历了 sync.Map 中的所有键值对,并打印它们。

1.9.2.7 RWMutex

sync.RWMutex(读写互斥锁)是 Go 语言标准库中的一种并发控制机制,用于管理多个 Goroutine 对共享资源的并发访问。RWMutex 支持多个 Goroutine 并发读取共享资源,但只能有一个 Goroutine 写入共享资源,以确保数据的一致性和安全性。

sync.RWMutex 具有以下特点和用法:

  1. 并发读取:多个 Goroutine 可以同时获取读锁(共享锁),并发读取共享资源,不会阻塞其他读取操作。
  2. 互斥写入:只有一个 Goroutine 可以获取写锁(排他锁),进行写入操作。当有 Goroutine 持有写锁时,其他任何读取或写入操作都会被阻塞,直到写锁被释放。
  3. 写锁优先:写锁优先于读锁,即如果有 Goroutine 持有写锁,则读锁和其他写锁都会被阻塞,直到写锁被释放。
  4. 适用于读多写少的场景:RWMutex 适用于读多写少的共享资源场景,因为并发读取不会相互阻塞,但写入需要独占资源。
go 复制代码
package main
​
import (
  "fmt"
  "sync"
  "time"
)
​
var counter int
​
func main() {
  lock := sync.RWMutex{}
  for i := 0; i < 4; i++ {
    go func() {
      for i := 0; i < 4; i++ {
        // 读锁
        lock.RLock()
        fmt.Printf("data: %d\n", counter)
        time.Sleep(1 * time.Second)
        lock.RUnlock()
      }
    }()
  }
  go func() {
    for i := 0; i < 6; i++ {
      lock.Lock()
      fmt.Printf("write data\n")
      counter++
      time.Sleep(4 * time.Second)
      lock.Unlock()
    }
  }()
  time.Sleep(20 * time.Second)
}
  1. 创建了一个名为 counter 的整数变量,用于模拟共享资源。

  2. 创建了一个名为 lock 的读写互斥锁(sync.RWMutex)。

  3. 启动了四个 Goroutine,每个 Goroutine 执行以下操作:

    • 获取读锁(lock.RLock()
    • 打印 counter 的值
    • 睡眠 1 秒钟
    • 释放读锁(lock.RUnlock()
  4. 启动了一个额外的 Goroutine,该 Goroutine 执行以下操作:

    • 获取写锁(lock.Lock()
    • 打印 "write data"
    • 增加 counter 的值
    • 睡眠 4 秒钟
    • 释放写锁(lock.Unlock()
  5. 主函数 main 睡眠 20 秒钟,以确保所有 Goroutine 有足够的时间运行。

下面是每个 Goroutine 的执行顺序:

Goroutine ID 执行操作 结果 锁状态
1 获取读锁 - 读锁
2 获取读锁 - 读锁
3 获取读锁 - 读锁
4 获取读锁 - 读锁
5 获取写锁 - 写锁
1 打印 counter data: 0 读锁
2 打印 counter data: 0 读锁
3 打印 counter data: 0 读锁
4 打印 counter data: 0 读锁
1 释放读锁 - -
2 释放读锁 - -
3 释放读锁 - -
4 释放读锁 - -
5 打印 "write data" write data 写锁
5 增加 counter - 写锁
5 睡眠 4 秒钟 - 写锁
1 获取读锁 - 读锁
2 获取读锁 - 读锁
3 获取读锁 - 读锁
4 获取读锁 - 读锁
5 释放写锁 - -
1 打印 counter data: 1 读锁
2 打印 counter data: 1 读锁
3 打印 counter data: 1 读锁
4 打印 counter data: 1 读锁
1 释放读锁 - -
2 释放读锁 - -
3 释放读锁 - -
4 释放读锁 - -
... 重复上述操作 ... ...

请注意,读锁(RLock)允许多个 Goroutine 并发读取,而写锁(Lock)会独占资源,因此当写锁被持有时,其他读锁和写锁都会被阻塞,直到写锁被释放。这就是为什么在读锁持有期间,多个 Goroutine 仍然可以并发读取共享资源(counter 的值),但在写锁持有期间,其他操作都会被阻塞。

1.9.2.8 Mutex以及RWMutex的区别

sync.Mutexsync.RWMutex 都是 Go 语言标准库中用于控制并发访问的锁机制,它们的主要区别在于对并发读取和写入的支持程度。

sync.Mutex(互斥锁)

sync.Mutex 是一种互斥锁,也称为排他锁。它具有以下特点和用法:

  • 互斥性:sync.Mutex 用于在多个 Goroutine 之间互斥地访问共享资源。一次只有一个 Goroutine 可以获得锁,其他 Goroutine 需要等待锁释放后才能获取。

  • 锁的基本操作:

    • Lock():用于获取锁,如果锁已被其他 Goroutine 占用,则当前 Goroutine 会被阻塞,直到锁被释放。
    • Unlock():用于释放锁,允许其他 Goroutine 获取锁。
  • 应用场景:适用于对共享资源进行写操作的场景,例如更新变量或数据结构。

基本用法示例:

go 复制代码
import (
    "sync"
)
​
var mu sync.Mutex
var counter int
​
func main() {
    mu.Lock()         // 获取锁
    counter = 42      // 修改共享资源
    mu.Unlock()       // 释放锁
}

sync.RWMutex(读写互斥锁)

sync.RWMutex 是一种读写互斥锁,它在互斥性上与 sync.Mutex 相似,但支持更灵活的读取操作。它具有以下特点和用法:

  • 读锁共享:sync.RWMutex 允许多个 Goroutine 同时获取读锁,以进行并发读取,不会阻塞其他读取操作。

  • 写锁互斥:只有一个 Goroutine 可以获取写锁,进行写入操作。当有 Goroutine 持有写锁时,其他任何读取或写入操作都会被阻塞,直到写锁被释放。

  • 写锁优先:写锁优先于读锁,即如果有 Goroutine 持有写锁,则读锁和其他写锁都会被阻塞,直到写锁被释放。

  • 锁的基本操作:

    • RLock():获取读锁,允许并发读取操作。
    • RUnlock():释放读锁。
    • Lock():获取写锁,独占资源进行写操作。
    • Unlock():释放写锁。
  • 应用场景:适用于读多写少的共享资源场景,其中读取操作可以并发进行,而写入操作需要独占资源。

基本用法示例:

go 复制代码
import (
    "sync"
)
​
var rwMu sync.RWMutex
var data map[string]int
​
func main() {
    rwMu.Lock()         // 获取写锁
    data["key"] = 42    // 修改共享资源
    rwMu.Unlock()       // 释放写锁
​
    rwMu.RLock()        // 获取读锁
    value := data["key"] // 读取共享资源
    rwMu.RUnlock()      // 释放读锁
}

总结:

  • sync.Mutex 适用于写多读少的场景,对写操作进行互斥保护。
  • sync.RWMutex 适用于读多写少的场景,允许并发读取,但写操作会独占资源。

1.9.3 channel

1.9.3.1 channel简介

在Go语言中,通道(channel) 是一种用于在不同的goroutines之间安全传递数据的机制。通道提供了一种通信的方式,允许一个goroutine向通道发送数据,另一个goroutine从通道接收数据。这种通信方式确保了数据传递的同步性和安全性,避免了多个goroutines之间发生竞态条件(race condition)和数据竞争(data race)等并发问题。

通道的特性包括:

  1. 安全性: 通道是并发安全的,多个goroutines可以同时向通道发送(send)或接收(receive)数据而不会发生冲突,确保了数据的安全传递。
  2. 同步性: 通道可以用来在不同的goroutines之间同步操作,一个goroutine可以等待另一个goroutine发送或接收数据,从而协调它们的执行顺序。
  3. 阻塞: 当一个goroutine试图向通道发送数据时,如果通道已满,发送操作将阻塞,直到有其他goroutine从通道中接收数据。同样,如果一个goroutine试图从空通道接收数据,接收操作也会阻塞,直到有其他goroutine向通道发送数据。

通道的声明方式如下:

go 复制代码
var ch chan DataType  // 创建一个通道,用于传递 DataType 类型的数据

或者使用make()函数来创建通道:(常用)

go 复制代码
ch := make(chan DataType)  // 创建一个传递 DataType 类型数据的通道

在通道上进行发送和接收操作的语法分别是:

  • 发送操作: ch <- data,将 data 发送到通道 ch 中。
  • 接收操作: data := <-ch,从通道 ch 中接收数据,并将其赋值给变量 data

通过通道,可以实现不同goroutines之间的数据传递和同步,确保程序的并发执行是安全和可靠的。

1.9.3.2 无buf channel和有buf channel

在Go语言中,通道(channels)可以分为两种类型:无缓冲通道(unbuffered channels)有缓冲通道(buffered channels)。简单点就是可以说无buf的和有buf的。

  1. 无缓冲通道(Unbuffered Channels):

    无缓冲通道是最基本的类型,它的特点是不保存任何数据。当一个goroutine发送数据到无缓冲通道时,它会被阻塞,直到另一个goroutine准备好接收数据。同样,当一个goroutine从无缓冲通道接收数据时,它也会被阻塞,直到另一个goroutine发送数据。

    无缓冲通道确保了数据的同步传递,发送和接收操作是同步的。这种特性使得goroutines能够在通道上进行安全的数据交换。

    go 复制代码
    ch := make(chan int) // 创建无缓冲通道
  2. 有缓冲通道(Buffered Channels):

    有缓冲通道在创建时可以指定一个缓冲区的大小。缓冲通道可以存储一定数量的元素,当通道中的元素达到缓冲区大小时,发送操作会阻塞,直到有其他goroutine从通道中接收数据释放空间。同样,当通道中有数据时,接收操作不会阻塞,直到缓冲区为空。

    有缓冲通道适用于在发送和接收之间存在时间上的异步性的情况,即发送和接收的goroutines的执行速度不一致。

    go 复制代码
    ch := make(chan int, 5) // 创建大小为5的有缓冲通道

    在这个例子中,make(chan int, 5) 创建了一个可以存储5个整数的有缓冲通道。

    通过选择无缓冲通道或有缓冲通道,可以根据程序的需求来控制goroutines之间的同步和异步操作。

go 复制代码
// 无buf的 (1)
package main
​
import (
  "fmt"
  "sync"
  "time"
)
​
func main() {
  var wg sync.WaitGroup
  ch := make(chan int)
  wg.Add(1)
  go func() {
    time.Sleep(5 * time.Second)
    defer wg.Done()
    ch <- 1
    fmt.Println("go routine done")
  }()
  num := <-ch
  fmt.Printf("num:%d\n", num)
  wg.Wait()
}

由于子go routine中延时了5s,则一直没有向ch中写入数据,也就导致了num无法接收到这个值,导致阻塞。5s后,同时打印结果。

go 复制代码
// 无buf的 (2)
package main
​
import (
  "fmt"
  "sync"
  "time"
)
​
func main() {
  var wg sync.WaitGroup
  ch := make(chan int)
  wg.Add(1)
  go func() {
    defer wg.Done()
    ch <- 1
    fmt.Println("go routine done")
  }()
  time.Sleep(5 * time.Second)
  num := <-ch
  fmt.Printf("num:%d\n", num)
  wg.Wait()
}

同样道理,子go routine往里面写入数据,但是主go routine没有读数据,导致代码阻塞,包括 ch <- 1也会阻塞5s。

go 复制代码
// 有buf的 
package main
​
import (
  "fmt"
  "sync"
  "time"
)
​
func main() {
  var wg sync.WaitGroup
  ch := make(chan int, 10)
  wg.Add(1)
  go func() {
    defer wg.Done()
    ch <- 1
    fmt.Println("go routine done")
  }()
  time.Sleep(5 * time.Second)
  num := <-ch
  fmt.Printf("num:%d\n", num)
  wg.Wait()
}

因为,定义了一个buf为10的一个channel,所以,往里面写数据的时候是有buf的,能够直接写入数据。以至于子go routine不会被阻塞5s,直接执行done。主的go routine阻塞5s后,可以直接拿到这个值,因为已经保存到了channel中的buf内。

1.9.3.3 channel循环

在Go语言中,循环中使用通道时需要特别小心,以避免出现死锁(deadlock)或其他并发问题。如下,就造成了死锁问题,自动panic。

同时,在Go语言中,range可以用于迭代通道(channel)中的数据。当你在通道上使用range时,它会持续迭代通道,直到通道被关闭。在每次迭代中,range会返回通道发送的值,并且当通道被关闭时,range会自动退出循环。

go 复制代码
package main
​
import (
  "fmt"
  "sync"
)
​
func work(ch chan int, wg *sync.WaitGroup) {
  defer wg.Done()
  for i := 0; i < 99; i++ {
    ch <- i * i
  }
  ch <- 1
  fmt.Println("go routine done")
}
​
func main() {
  var wg sync.WaitGroup
  ch := make(chan int, 10)
  wg.Add(1)
  go work(ch, &wg)
  for num := range ch {
    fmt.Printf("num:%d\n", num)
  }
  wg.Wait()
}

那么,我们就需要 --- 关闭通道close(channel),通知接收方没有更多数据:

go 复制代码
package main
​
import (
  "fmt"
  "sync"
)
​
func work(ch chan int, wg *sync.WaitGroup) {
  defer wg.Done()
  defer close(ch)  // 新增的
  for i := 0; i < 99; i++ {
    ch <- i * i
  }
  ch <- 1
  fmt.Println("go routine done")
}
​
func main() {
  var wg sync.WaitGroup
  ch := make(chan int, 10)
  wg.Add(1)
  go work(ch, &wg)
  for num := range ch {
    fmt.Printf("num:%d\n", num)
  }
  wg.Wait()
}
go 复制代码
ch := make(chan int)
​
go func() {
    for i := 0; i < 5; i++ {
        ch <- i // 发送数据到通道
    }
    close(ch) // 关闭通道
}()
​
for {
    data, ok := <-ch
    if !ok {
        break // 通道已关闭,退出循环
    }
    // 处理接收到的数据
    fmt.Println(data)
}

goroutine发送完数据后,关闭了通道。在接收数据时,使用ok变量来检查通道是否已关闭,如果通道已关闭,则退出循环。

总之:

如果在一个没有关闭的通道上使用range来循环接收数据,当通道中没有数据可接收时,range语句会阻塞等待新的数据,而且由于通道没有被关闭,没有其他的goroutine会再向通道中发送数据,因此range语句会一直等待下去,导致程序陷入死锁状态。

简单点说:其他的子go routine都完事了,但主go routine还在等着呢,所以就会陷入死锁。

1.9.3.4 使用channel需要注意的问题

使用Go语言的通道(channel)时,需要注意一些常见的问题,以确保并发程序的正确性和稳定性。以下是一些需要注意的问题:

不要关闭一个已经关闭的通道:

在Go语言中,通道是可以被关闭的。但是,在关闭一个已经关闭的通道时会引发panic。因此,在关闭通道之前,通常需要确保它并没有被关闭。

go 复制代码
ch := make(chan int)
​
close(ch) // 关闭通道
​
// 再次关闭通道会引发panic
// close(ch) 

避免从未关闭的通道接收数据:

如果在一个未关闭的通道上进行接收操作,并且通道中没有数据可接收时,接收操作会一直阻塞。确保在发送方确定不再发送数据时,关闭通道,避免接收方无法退出的情况。

go 复制代码
ch := make(chan int)
​
// 这里会永远阻塞,因为通道没有被关闭且没有数据可接收
// data := <-ch

避免在未初始化的通道上进行操作:

未初始化的通道值是nil,在nil通道上进行发送和接收操作会永远阻塞,导致程序无法继续执行。

go 复制代码
var ch chan int
​
// 这里会永远阻塞,因为通道没有被初始化
// ch <- 42
// data := <-ch

避免在缓冲区已满的通道上发送数据:

如果在一个没有空闲缓冲区的通道上发送数据,发送操作会阻塞。使用select语句可以避免阻塞,或者在缓冲区满的情况下选择等待或放弃发送操作。

go 复制代码
ch := make(chan int, 1) // 创建缓冲区大小为1的通道
​
ch <- 42   // 发送数据,通道不会阻塞
// ch <- 24 // 缓冲区已满,发送操作会阻塞
​
select {
case ch <- 24:
    fmt.Println("Sent successfully")
default:
    fmt.Println("Channel is full, skipping send operation.")
}

在编写并发程序时,要注意处理好通道的关闭、阻塞、以及未初始化等情况,以避免出现死锁和其他并发问题。使用select语句可以更好地控制通道操作,确保程序的正确性和稳定性。

1.9.3.5 select

select 是 Go 语言中的一种控制结构,用于处理一个或多个 channel 操作。它类似于 switch 语句,但是专门用于处理通信操作。select 语句会等待多个通信操作中的一个完成,并且会随机选择一个可用的通信操作执行。

以下是 select 语句的基本语法:

go 复制代码
select {
case communicationClause1:
    // 当 communicationClause1 的通信操作可以进行时,执行此处的代码
case communicationClause2:
    // 当 communicationClause2 的通信操作可以进行时,执行此处的代码
// 可以有更多的 case 语句
default:
    // 如果没有任何 case 的通信操作可以进行时,执行 default 语句
}

communicationClause 可以是发送(chan <-)、接收(<-chan)或关闭(close(chan))操作。

以下是一个示例,演示了如何使用 select 语句处理多个 channel 操作:

go 复制代码
package main
​
import (
  "fmt"
  "time"
)
​
func main() {
  // 创建两个通道
  ch1 := make(chan string)
  ch2 := make(chan string)
​
  // 匿名goroutine,向ch1和ch2发送消息
  go func() {
    time.Sleep(2 * time.Second)
    ch1 <- "Message from channel 1"
  }()
​
  go func() {
    time.Sleep(1 * time.Second)
    ch2 <- "Message from channel 2"
  }()
​
  // 使用select同时等待ch1和ch2的消息
  select {
  case msg1 := <-ch1:
    fmt.Println("Received", msg1)
  case msg2 := <-ch2:
    fmt.Println("Received", msg2)
  case <-time.After(3 * time.Second):
    fmt.Println("Timeout. No message received.")
  }
}

在这个例子中,我们创建了两个通道 ch1ch2,并在两个匿名的 goroutine 中分别向它们发送了消息。然后,在 select 语句中,我们同时等待 ch1ch2 的消息,以及一个 3 秒的超时时间。第一个能够发送消息的通道将触发对应的 case 分支,最终将其消息打印出来。如果在超时之前没有任何消息被接收,就会执行 default 分支,输出超时信息。

1.9.3.5.1 time.NewTicker

在Go语言中,time.NewTicker 函数用于创建一个定时触发的Ticker,它会在固定的时间间隔内重复执行一个操作。time.NewTicker 函数接受一个time.Duration类型的参数,表示每次触发的时间间隔。当创建了一个Ticker之后,它会定期地发送当前时间到一个通道(Ticker的C属性),你可以通过该通道来获取这些时间信息。

下面是time.NewTicker 函数的定义:

go 复制代码
func NewTicker(d Duration) *Ticker

这里的参数 d 表示时间间隔,可以是纳秒(time.Nanosecond)、微秒(time.Microsecond)、毫秒(time.Millisecond)、秒(time.Second)等单位。

以下是一个使用time.NewTicker的简单例子,每隔1秒输出一次当前时间:

go 复制代码
package main
​
import (
  "fmt"
  "time"
)
​
func main() {
  // 创建一个每隔1秒触发一次的Ticker
  ticker := time.NewTicker(1 * time.Second)
​
  // 在goroutine中监听Ticker的触发事件
  go func() {
    for {
      // 通过Ticker的C通道获取当前时间
      <-ticker.C
      fmt.Println("Tick at", time.Now())
    }
  }()
​
  // 等待10秒,让Ticker触发10次
  time.Sleep(10 * time.Second)
​
  // 停止Ticker
  ticker.Stop()
  fmt.Println("Ticker stopped")
}

在这个例子中,程序创建了一个每隔1秒触发一次的Ticker,并在一个单独的goroutine中监听Ticker的触发事件。在主goroutine中,程序等待10秒后停止了Ticker,然后输出"Ticker stopped"。在监听的goroutine中,每次Ticker触发时,会输出当前的时间信息。

请注意,当不再需要使用Ticker时,应该调用它的Stop方法来释放相关资源,以防止内存泄漏。

1.9.3.5.2 time.NewTicker

time.After 函数是 Go 语言中 time 包提供的一个函数,它返回一个通道(<-chan time.Time),该通道在指定的时间间隔之后会接收到一个当前时间值。

函数定义如下:

go 复制代码
func After(d Duration) <-chan Time

这里,Duration 表示时间间隔,可以是纳秒(time.Nanosecond)、微秒(time.Microsecond)、毫秒(time.Millisecond)、秒(time.Second)等单位。

time.After 函数通常用于在 select 语句中,作为一个 case 来设置超时。例如,在一个需要等待某个操作完成的情况下,你可以使用 time.After 来设置一个超时,确保不会无限期地等待某个操作。

以下是一个🌰,演示了如何在 select 语句中使用 time.After 设置超时:

go 复制代码
package main
​
import (
  "fmt"
  "time"
)
​
func main() {
  // 创建一个通道,用于接收数据
  dataChan := make(chan int)
​
  // 启动一个goroutine,模拟一些耗时的操作,然后将结果发送到dataChan
  go func() {
    time.Sleep(2 * time.Second)
    dataChan <- 42
  }()
​
  // 使用select同时等待dataChan和2秒的超时时间
  select {
  case data := <-dataChan:
    fmt.Println("Received data:", data)
  case <-time.After(3 * time.Second):
    fmt.Println("Timeout. No data received in 3 seconds.")
  }
}

在这个例子中,我们创建了一个名为 dataChan 的通道,并在一个匿名的 goroutine 中模拟了一个耗时的操作。然后,在 select 语句中,我们同时等待 dataChan 的消息和 3 秒的超时时间。如果 dataChan 在 3 秒内没有收到数据,time.After 将会触发,执行相应的 case 分支,输出超时信息。

请注意,time.After 返回的是一个只读通道,你不能关闭它。当超时发生时,通道将会被自动关闭。

1.9.3.5.3 select例子
go 复制代码
package main
​
import (
  "fmt"
  "sync"
  "time"
)
​
func main() {
  ch1 := make(chan int, 10)
  ch2 := make(chan int, 10)
  var wg sync.WaitGroup
  wg.Add(2)
  go func() {
    for i := 0; i < 10; i++ {
      ch1 <- i
      time.Sleep(1 * time.Second)
    }
    defer wg.Done()
    defer close(ch1)
  }()
​
  go func() {
    for i := 0; i < 10; i++ {
      ch2 <- i
      time.Sleep(500 * time.Millisecond)
    }
    defer wg.Done()
    defer close(ch2)
  }()
  timeout := time.After(7 * time.Second)
loop:
  for {
    select {
    case num, ok := <-ch1:
      if !ok {
        ch1 = nil
        break
      }
      fmt.Printf("receive num from ch1:%d\n", num)
    case num, ok := <-ch2:
      if !ok {
        ch2 = nil
        break
      }
      fmt.Printf("receive num from ch2:%d\n", num)
    case <-timeout:
      fmt.Println("no data, time out!!!!!!!!!")
      break loop
    }
    if ch1 == nil && ch2 == nil {
      break
    }
  }
  wg.Wait()
}

创建了两个带有缓冲通道(ch1ch2),并使用两个独立的 goroutine 向这两个通道发送数据。然后,主 goroutine 中使用 select 语句同时等待 ch1ch2 的数据和 7 秒的超时时间。如果在 7 秒内没有从 ch1ch2 中接收到数据,就会打印超时消息并结束程序。

同时使用了 defer 关键字来确保在 goroutine 执行结束后关闭通道,并且使用了 sync.WaitGroup 来等待两个 goroutine 的结束。

select 语句中,还做了一个特殊处理:如果某个通道关闭了(!ok),将对应的通道变量设置为 nil(读和写都是会被阻塞),以此来避免在已经关闭的通道上进行接收操作。

1.9.3.6 select的注意点

select 语句会等待多个 channel 中的任意一个可以进行读写操作

go 复制代码
package main  
​
import (  
   "fmt"  
   "time")  
​
func main() {  
   ch1 := make(chan int, 10)  
   ch2 := make(chan int, 10)  
​
loop:  
   for {  
      select {  
      case ch1 <- 1:  
         fmt.Println("send data on ch1")  
      case ch2 <- 1:  
         fmt.Println("send data on ch2")  
      case <-time.After(1 * time.Second):  
         break loop  
      }  
   }  
}

不能在 select 语句中使用相同的 unbuffer channel 进行读写操作

go 复制代码
select {
case x := <-ch:
    // 处理来自 ch 的数据
case ch <- y:
    // 向 ch 中写入数据 y
}
package main  
​
import (  
   "fmt"  
   "time")  
​
func main() {  
   ch := make(chan int)  
​
   select {  
   case x := <-ch:  
      fmt.Printf("num: %d\n", x)  
   case ch <- 0:  
   case <-time.After(1 * time.Second):  
      break  
   }  
}

如果一个 channel 已经被关闭了,再向它写入数据会导致 panic。

go 复制代码
func main() {  
   ch := make(chan int)  
   close(ch)  
   for i := 0; i < 2; i++ {  
      select {  
      case x := <-ch:  
         fmt.Println(x)  
      case ch <- 1:  
         // 向 ch 中写入数据 ydefault:  
         fmt.Println("default")  
      }  
   }  
}
1.9.3.7 Fan-In

在 Go 语言中,Fan-In 是一种常见的并发模式,用于从多个通道中读取数据并将数据合并到一个通道中。这种模式非常适合用于处理并发任务,例如从多个来源收集数据,并将这些数据合并到一个通道中进行处理。

下面是一个 Fan-In 的示例代码:

go 复制代码
package main
​
import (
  "fmt"
  "sync"
)
​
func main() {
  ch1 := make(chan int)
  ch2 := make(chan int)
  out := make(chan int)
​
  // 开启两个 goroutine 向 ch1 和 ch2 发送数据
  go func() {
    for i := 0; i < 5; i++ {
      ch1 <- i
    }
    close(ch1)
  }()
​
  go func() {
    for i := 5; i < 10; i++ {
      ch2 <- i
    }
    close(ch2)
  }()
​
  // Fan-In 模式,将 ch1 和 ch2 的数据合并到 out 通道中
  var wg sync.WaitGroup
  wg.Add(2)
​
  go func() {
    defer wg.Done()
    for num := range ch1 {
      out <- num
    }
  }()
​
  go func() {
    defer wg.Done()
    for num := range ch2 {
      out <- num
    }
  }()
​
  // 等待两个 goroutine 完成
  go func() {
    wg.Wait()
    close(out) // 所有数据发送完成后关闭 out 通道
  }()
​
  // 从 out 通道中读取数据
  for num := range out {
    fmt.Println(num)
  }
}

在这个示例中,我们创建了两个输入通道 ch1ch2,以及一个输出通道 out。两个 goroutine 分别向 ch1ch2 中发送数据。然后,我们启动了两个 goroutine,分别从 ch1ch2 中读取数据,并将数据发送到 out 通道中。最后,我们等待两个 goroutine 完成,然后关闭 out 通道,并从 out 中读取并打印数据。

既然已经明白了这个道理,那么咱看看其他的代码:

go 复制代码
package utils
​
import (
  "fmt"
  "sync"
)
​
func producer(ch chan string, name string) {
  for i := 0; i < 10; i++ {
    ch <- fmt.Sprintf("%s: %d", name, i)
  }
  close(ch)
}
​
func consumer(ch1, ch2 chan string) {
  for {
    select {
    case data, ok := <-ch1:
      if !ok {
        ch1 = nil
        break
      }
      fmt.Println("ch1 : ", data)
    case data, ok := <-ch2:
      if !ok {
        ch2 = nil
        break
      }
      fmt.Println("ch2 : ", data)
    }
    if ch1 == nil && ch2 == nil {
      break
    }
  }
}
​
func fanIn(ch1, ch2 chan string) chan string {
  ch := make(chan string)
  go func() {
    defer close(ch)
    for {
      select {
      case data, ok := <-ch1:
        if !ok {
          ch1 = nil
          break
        }
        ch <- data
      case data, ok := <-ch2:
        if !ok {
          ch2 = nil
          break
        }
        ch <- data
      }
      if ch1 == nil && ch2 == nil {
        break
      }
    }
  }()
  return ch
}
​
func MoreSelectChannelMain() {
  var wg sync.WaitGroup
  ch1 := make(chan string)
  ch2 := make(chan string)
  wg.Add(1)
  go func() {
    defer wg.Done()
    producer(ch1, "goroutine1")
  }()
  wg.Add(1)
  go func() {
    defer wg.Done()
    producer(ch2, "goroutine2")
  }()
  //consumer(ch1, ch2)
  ch := fanIn(ch1, ch2)
  for data := range ch {
    fmt.Println("zuizhjgde", data)
  }
}
1.9.3.8 select反射版本
go 复制代码
func fanInNew(channels ...chan string) chan string {  
   ch := make(chan string)  
   go func() {  
      defer close(ch)  
      var cases []reflect.SelectCase  
      for _, c := range channels {  
         cases = append(cases, reflect.SelectCase{Dir: reflect.SelectRecv, Chan: reflect.ValueOf(c)})  
      }  
      for len(cases) > 0 {  
         i, value, ok := reflect.Select(cases)  
         if !ok {  
            cases = append(cases[:i], cases[i+1:]...)  
            continue  
         }  
         ch <- value.String()  
      }  
   }()  
   return ch  
}

fanInNew 函数实现了一个 Fan-In 模式,它接受一个或多个字符串类型的通道作为参数,将这些通道中的数据合并到一个新的通道 ch 中。

  1. fanInNew 函数接受一个变参(variadic)参数 channels ...chan string,这表示它可以接受任意数量的字符串类型通道。
  2. 创建了一个新的字符串类型通道 ch,用于存放合并后的数据。
  3. 通过 go func() {...}() 创建了一个匿名的 goroutine,这个 goroutine 用于将多个通道的数据合并到 ch 中。
  4. 在 goroutine 中,定义了一个 []reflect.SelectCase 类型的切片 cases,用于存放 reflect.SelectCase 结构体,其中包含了通道的信息(Dir 表示操作方向,Chan 是通道的反射值)。
  5. 使用 reflect.ValueOf(c) 将每个通道转换为反射值,并将其添加到 cases 切片中。
  6. 进入一个无限循环,只有当 cases 切片为空时才会退出。在每次循环中,使用 reflect.Select 函数来选择一个可用的通道。如果通道已经关闭,从 cases 切片中移除它。如果通道是可用的,将其发送的字符串数据(使用 value.String())发送到合并的通道 ch 中。
  7. 当所有的通道都关闭,并且 cases 切片为空时,goroutine 会结束。

请注意,使用反射会导致性能的损失,因此在实际应用中,尽量避免过多地使用反射。

最后,基本用法到此结束,由于工作中并不会涉及任何代码内容,所以不清楚该文章写的内容是否都是正确的。 如有问题请各位大佬批评指正~。

相关推荐
Chrikk5 分钟前
Go-性能调优实战案例
开发语言·后端·golang
幼儿园老大*8 分钟前
Go的环境搭建以及GoLand安装教程
开发语言·经验分享·后端·golang·go
canyuemanyue8 分钟前
go语言连续监控事件并回调处理
开发语言·后端·golang
杜杜的man10 分钟前
【go从零单排】go语言中的指针
开发语言·后端·golang
customer082 小时前
【开源免费】基于SpringBoot+Vue.JS周边产品销售网站(JAVA毕业设计)
java·vue.js·spring boot·后端·spring cloud·java-ee·开源
Yaml43 小时前
智能化健身房管理:Spring Boot与Vue的创新解决方案
前端·spring boot·后端·mysql·vue·健身房管理
小码编匠4 小时前
一款 C# 编写的神经网络计算图框架
后端·神经网络·c#
AskHarries4 小时前
Java字节码增强库ByteBuddy
java·后端