Go学习笔记

一、基础语法

1. 变量
1. var identifier type
var a string = "Runoob"  // 没有初始化则默认为零值,声明了一个局部变量却没有在相同的代码块中使用它,同样会得到编译错误

2. var v_name = value    // 根据值自行判定变量类型
var d = true

3. v_name := value // 等价于 var identifier type = value

4. var vname1, vname2, vname3 = v1, v2, v3    //多变量声明
5. vname1, vname2, vname3 := v1, v2, v3

命名规则:
1)声明在函数内部,是函数的本地值,类似private
2)声明在函数外部,是对当前包可见(包内所有.go文件都可见)的全局值,类似protect
3)声明在函数外部且首字母大写是所有包可见的全局值,类似public

2. 常量
1. const identifier [type] = value
const LENGTH int = 10

2. const (
    Unknown = 0
    Female = 1
    Male = 2
)

iota,特殊常量,可以认为是一个可以被编译器修改的常量。

iota 在 const关键字出现时将被重置为 0(const 内部的第一行之前),const 中每新增一行常量声明将使 iota 计数一次(iota 可理解为 const 语句块中的行索引)。

const (
    a = iota
    b
    c
)
// a=0,b=1,c=2
3. 指针

Go语言中的函数传参都是值拷贝,当我们想要修改某个变量的时候,我们可以创建一个指向该变量地址的指针变量。传递数据使用指针,而无须拷贝数据。

& 返回变量存储地址
* 指针变量
ptr = &a     /* 'ptr' 包含了 'a' 变量的地址 */
fmt.Printf("*ptr 为 %d\n", *ptr)  // 输出a的值

在Go语言中对于引用类型的变量,我们在使用的时候不仅要声明它,还要为它分配内存空间,否则我们的值就没办法存储。而对于值类型的声明不需要分配内存空间,是因为它们在声明的时候已经默认分配好了内存空间。

使用new函数得到的是一个类型的指针,并且该指针对应的值为该类型的零值

a := new(int)  

make也是用于内存分配的,区别于new,它只用于slice、map以及chan的内存创建,而且它返回的类型就是这三个类型本身,而不是他们的指针类型,因为这三种类型就是引用类型

var b map[string]int
b = make(map[string]int, 10)

new与make的区别
1.二者都是用来做内存分配的。
2.make只用于slice、map以及channel的初始化,返回的还是这三个引用类型本身;
3.而new用于类型的内存分配,并且内存对应的值为类型零值,返回的是指向类型的指针

4. 条件语句
switch marks {
      case 90: grade = "A"
      case 80: grade = "B"
      case 50,60,70 : grade = "C"
      default: grade = "D"  
}

// switch 可以用来判断存储类型
switch i := x.(type) {
      case nil:  
         fmt.Printf(" x 的类型 :%T",i)                
      case int:  
         fmt.Printf("x 是 int 型")                      
      case float64:
         fmt.Printf("x 是 float64 型")          
      case func(int) float64:
         fmt.Printf("x 是 func(int) 型")                      
      case bool, string:
         fmt.Printf("x 是 bool 或 string 型" )      
      default:
         fmt.Printf("未知型")    
   }  

select 语句类似于 switch 语句,但是select会随机执行一个可运行的case。如果没有case可运行,它将阻塞,直到有case可运行。
select是Go中的一个控制结构,类似于switch语句,用于处理异步IO操作。select会监听case语句中channel的读写操作,当case中channel读写操作为非阻塞状态(即能读写)时,将会触发相应的动作。 select中的case语句必须是一个channel操作

5. 循环语句
sum := 0
for i := 0; i <= 10; i++ {
    sum += i
}

// 这样写也可以,更像 While 语句形式
for sum <= 10{
   sum += sum
}

for {
      sum++ // 无限循环下去
}

//For-each range 循环
strings := []string{"google", "runoob"}
   for i, s := range strings {
      fmt.Println(i, s)
   }

    // 读取 key 和 value
    for key, value := range map1 {
      fmt.Printf("key is: %d - value is: %f\n", key, value)
    }

    // 读取 key
    for key := range map1 {
      fmt.Printf("key is: %d\n", key)
    }

    // 读取 value
    for _, value := range map1 {
      fmt.Printf("value is: %f\n", value)
    }
	
	// range 迭代字符串时,返回每个字符的索引和 Unicode 代码点(rune)
	for i, c := range "hello" {
        fmt.Printf("index: %d, char: %c\n", i, c)
    }

	// 遍历通道
	ch := make(chan int, 2)
    ch <- 1
    ch <- 2
    close(ch)
   
    for v := range ch {
        fmt.Println(v)
    }

注意,range 会复制对象。

a := [3]int{0, 1, 2}

    for i, v := range a { // index、value 都是从复制品中取出。

        if i == 0 { // 在修改前,我们先修改原数组。
            a[1], a[2] = 999, 999
            fmt.Println(a) // 确认修改有效,输出 [0, 999, 999]。
        }

        a[i] = v + 100 // 使用复制品中取出的 value 修改原数组。迭代过程中不会取出修改后的值
    }

    [0 999 999]
    [100 101 102]
s := []int{1, 2, 3, 4, 5}

    for i, v := range s { // 复制 struct slice { pointer, len, cap }。

        if i == 0 {
            s = s[:3]  // 对 slice 的修改,不会影响 range。
            s[2] = 100 // 对底层数据的修改。
        }

        println(i, v)  // v用的是修改后的数据
    }
    
    0 1
    1 2
    2 100
    3 4
    4 5
6. 函数
func function_name( [parameter list] ) [return_types] {
   函数体
}
func max(num1, num2 int) int {
   /* 定义局部变量 */
   var result int

   if (num1 > num2) {
      result = num1
   } else {
      result = num2
   }
   return result
}

// Go 函数可以返回多个值
func swap(x, y string) (string, string) {
   return y, x
}

在默认情况下,Go 语言使用的是值传递,即在调用过程中不会影响到实际参数
注意1:无论是值传递,还是引用传递,传递给函数的都是变量的副本,不过,值传递是值的拷贝。引用传递是地址的拷贝,一般来说,地址拷贝更为高效。而值拷贝取决于拷贝的对象大小,对象越大,则性能越低
注意2:map、slice、chan、指针、interface默认以引用的方式传递

func myfunc(args ...int) {    //0个或多个参数
  }
// 在参数赋值时可以不用用一个一个的赋值,可以直接传递一个数组或者切片,特别注意的是在参数后加上“…”即可
s := []int{1, 2, 3}
res := myfunc("sum: %d", s...)    // slice... 展开slice
    
func add(a int, args…int) int {    //1个或多个参数
  }

// 任意类型的不定参数
func myfunc(args ...interface{}) {
  }

匿名函数

    fn := func() { println("Hello, World!") }
    fn()

    // --- function collection ---
    fns := [](func(x int) int){
        func(x int) int { return x + 1 },
        func(x int) int { return x + 2 },
    }
    println(fns[0](100))

闭包
闭包是由函数及其相关引用环境组合而成的实体(即:闭包=函数+引用环境)。Go语言是支持闭包的

func a() func() int {
    i := 0
    b := func() int {
        i++
        fmt.Println(i)
        return i
    }
    return b
}

func main() {
    c := a() // 当函数a()的内部函数b()被函数a()外的一个变量引用的时候,就创建了一个闭包。由于闭包的存在使得函数a()返回后,a中的i始终存在,这样每次执行c(),i都是自加1后的值
    c()
    c()
    c()

    a() //不会输出i
}

    1
    2
    3

延迟调用(defer)

  1. 关键字 defer 用于注册延迟调用。
  2. 这些调用直到 return 前才被执。因此,可以用来做资源清理。
  3. 多个defer语句,按先进后出的方式执行。(因此如果先前面的资源先释放了,后面的语句就没法执行了)
  4. defer语句中的变量,在defer声明时就决定了。

defer常被用来关闭文件句柄、锁资源释放、数据库连接释放
多个 defer 注册,按 FILO 次序执行 ( 先进后出 )。哪怕函数或某个延迟调用发生错误,这些调用依旧会被执行
滥用 defer 可能会导致性能问题,尤其是在一个 “大循环” 里

func main() {
    var whatever [5]struct{}

    for i := range whatever {
        defer fmt.Println(i)
    }
}
4 3 2 1 0
func do() error {
    f, err := os.Open("book.txt")
    if err != nil {
        return err
    }
    if f != nil {
        defer func() {
            if err := f.Close(); err != nil {
                fmt.Printf("defer close book.txt err %v\n", err)
            }
        }()
    }

init函数:go语言中init函数用于包(package)的初始化,如初始化包里的变量等

  • 每个包可以拥有多个init函数
  • 包的每个源文件也可以拥有多个init函数
  • 同一个包中多个init函数的执行顺序go语言没有明确的定义
  • 不同包的init函数按照包导入的依赖关系决定该初始化函数的执行顺序。初始化顺序是先导入的包,然后是当前包中的 init 函数,如果有多个 init 函数,它们会按定义的顺序依次调用
  • init函数不能被其他函数调用,而是在main函数执行之前,自动被调用
6.5. 字符串

多行字符串使用反引号

s1 := `第一行
    第二行
    第三行
    `

字符串常用操作使用 strings 包

Go 语言的字符有以下两种:
uint8类型,或者叫 byte 型,代表了ASCII码的一个字符。
rune类型,代表一个 UTF-8字符。
当需要处理中文、日文或者其他复合字符时,需要使用rune类型

for _, r := range s { //rune
            fmt.Printf("%v(%c) ", r, r)
        }

修改字符串,需要先将其转换成[]rune或[]byte,完成后再转换为string。无论哪种转换,都会重新分配内存,并复制字节数组。

s1 := "hello"
        // 强制类型转换
        byteS1 := []byte(s1)
        byteS1[0] = 'H'
        fmt.Println(string(byteS1))

        s2 := "博客"
        runeS2 := []rune(s2)
        runeS2[0] = '狗'
        fmt.Println(string(runeS2))
7. 数组
var arrayName [size]dataType
var numbers [5]int
var numbers = [5]int{1, 2, 3, 4, 5}
numbers := [5]int{1, 2, 3, 4, 5}
a := [2][3]int{{1, 2, 3}, {4, 5, 6}}
d := [...]struct {
        name string
        age  uint8
    }{
        {"user1", 10}, // 可省略元素类型。
        {"user2", 20}, // 别忘了最后一行的逗号。
    }

//  如果设置了数组的长度,可以通过指定下标来初始化元素
balance := [5]float32{1:2.0,3:7.0} // 将索引为 1 和 3 的元素初始化
8. 切片
// 可以声明一个未指定大小的数组来定义切片
var identifier []type
// make([]T, length, capacity)函数定义切片, capacity容量为可选参数
slice1 := make([]type, len)
// 通过下标切片
s := arr[startIndex:endIndex] 
// len() 和 cap() 函数获取长度和容量
fmt.Printf("len=%d cap=%d slice=%v\n",len(x),cap(x),x)

注意,切片是对数组的引号,读写操作实际目标是底层数组

//append() 和 copy() 函数
var numbers []int
numbers = append(numbers, 0) 
numbers = append(numbers, 2,3,4) // 向切片中添加元素

numbers1 := make([]int, len(numbers), (cap(numbers))*2) // 创建切片 numbers1 是之前切片的两倍容量
copy(numbers1,numbers) // 拷贝 numbers 的内容到 numbers1

超出原 slice.cap 限制,就会重新分配底层数组,即便原数组并未填满。 通常以 2 倍容量重新分配底层数组。在大批量添加数据时,建议一次性分配足够大的空间,以减少内存分配和数据复制开销。

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

str := "hello world"
s1 := str[0:5]
9. Map
/* 使用 make 函数 */
map_variable := make(map[KeyType]ValueType, initialCapacity)
m := make(map[string]int)

// 使用字面量创建 Map
m := map[string]int{
    "apple": 1,
    "banana": 2,
    "orange": 3,
}

v2, ok := m["pear"]  // 如果键不存在,ok 的值为 false,v2 的值为该类型的零值
// 删除键值对
delete(m, "banana")
10. 结构体

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

type struct_variable_type struct {
   member definition
   member definition
   ...
   member definition
}

type Books struct {
   title string
   author string
   subject string
   book_id int
}

//一旦定义了结构体类型,它就能用于变量的声明,语法格式如下:
variable_name := structure_variable_type {value1, value2...valuen}
或
variable_name := structure_variable_type { key1: value1, key2: value2..., keyn: valuen}

fmt.Println(Books{"Go 语言", "www.runoob.com", "Go 语言教程", 6495407})
fmt.Println(Books{title: "Go 语言", author: "www.runoob.com", subject: "Go 语言教程", book_id: 6495407})

var p = new(Book)
p.title = "格林童话" // 实际上是 (*p).title,Go语言帮我们实现了语法糖
// 结构体指针
var Book1 Books
printBook(&Book1)
func printBook( book *Books ) {
	// Go语言中支持对结构体指针直接使用.来访问结构体的成员
   fmt.Printf( "Book title : %s\n", book.title)
   fmt.Printf( "Book author : %s\n", book.author)
   fmt.Printf( "Book subject : %s\n", book.subject)
   fmt.Printf( "Book book_id : %d\n", book.book_id)
}

匿名结构体

var user struct{Name string; Age int}
user.Name = "pprof.cn"

构造函数
Go语言的结构体没有构造函数,我们可以自己实现。 例如,下方的代码就实现了一个person的构造函数。 因为struct是值类型,如果结构体比较复杂的话,值拷贝性能开销会比较大,所以该构造函数返回的是结构体指针类型。

func newPerson(name, city string, age int8) *person {
    return &person{
        name: name,
        city: city,
        age:  age,
    }
}

方法和接收者

// 官方建议使用接收者类型名的第一个小写字母
func (接收者变量 接收者类型) 方法名(参数列表) (返回参数) {
        函数体
}

// 指针类型的接收者
// 调用方法时修改接收者指针的任意成员变量,在方法结束后,修改都是有效的
func (p *Person) SetAge(newAge int8) {
        p.age = newAge
}

p1 := NewPerson("测试", 25)
p1.SetAge(30)

// 值类型的接收者
// 当方法作用于值类型接收者时,Go语言会在代码运行时将接收者的值复制一份。在值类型接收者的方法中可以获取接收者的成员值,但修改操作只是针对副本,无法修改接收者变量本身。
func (p Person) SetAge2(newAge int8) {
    p.age = newAge
}

什么时候应该使用指针类型接收者
1.需要修改接收者中的值
2.接收者是拷贝代价比较大的大对象
3.保证一致性,如果有某个方法使用了指针接收者,那么其他的方法也应该使用指针接收者。

嵌套结构体

user1 := User{
        Name:   "pprof",
        Gender: "女",
        Address: Address{
            Province: "黑龙江",
            City:     "哈尔滨",
        },
    }
11. 类型转换
// 数值类型转换
type_name(expression)

var a int = 10
var b float64 = float64(a)

// 字符串转整数
var str string = "10"
var num int
num, _ = strconv.Atoi(str) // 第一个是转换后的整型值,第二个是可能发生的错误,可以使用空白标识符 _ 来忽略这个错误

// 字符串转浮点数
str := "3.14"
num, err := strconv.ParseFloat(str, 64)

// 整数转字符串
num := 123
str := strconv.Itoa(num)

// 浮点数转字符串
num := 3.14
str := strconv.FormatFloat(num, 'f', 2, 64)

实现继承

//Animal 动物
type Animal struct {
    name string
}

func (a *Animal) move() {
    fmt.Printf("%s会动!\n", a.name)
}

//Dog 狗
type Dog struct {
    Feet    int8
    *Animal //通过嵌套匿名结构体实现继承
}

func (d *Dog) wang() {
    fmt.Printf("%s会汪汪汪~\n", d.name)
}

func main() {
    d1 := &Dog{
        Feet: 4,
        Animal: &Animal{ //注意嵌套的是结构体指针
            name: "乐乐",
        },
    }
    d1.wang() //乐乐会汪汪汪~
    d1.move() //乐乐会动!
}
12. 接口

Go 中没有关键字显式声明某个类型实现了某个接口
只要一个类型实现了接口要求的所有方法,该类型就自动被认为实现了该接口

接口的零值是 nil,一个未初始化的接口变量其值为 nil,且不包含任何动态类型或值

接口的常见用法
多态:不同类型实现同一接口,实现多态行为。
解耦:通过接口定义依赖关系,降低模块之间的耦合。
泛化:使用空接口 interface{} 表示任意类型。

关于接口需要注意的是,只有当有两个或两个以上的具体类型必须以相同的方式进行处理时才需要定义接口。不要为了接口而写接口,那样只会增加不必要的抽象,导致不必要的运行时损耗。

// 定义接口
type Shape interface {
        Area() float64
        Perimeter() float64
}

// 定义一个结构体
type Circle struct {
        Radius float64
}

// Circle 实现 Shape 接口
func (c Circle) Area() float64 {
        return math.Pi * c.Radius * c.Radius
}

func (c *Circle) Perimeter() float64 {
        return 2 * math.Pi * c.Radius
}

func main() {
        c := Circle{Radius: 5}
        var s Shape = c // 接口变量可以存储实现了接口的类型
        fmt.Println("Area:", s.Area())
        fmt.Println("Perimeter:", s.Perimeter())
}

使用值接收者实现接口之后,不管是结构体还是结构体指针类型的变量都可以赋值给该接口变量
使用指针接收者实现接口之后,只能给接口变量赋值结构体指针,不能赋值结构体

空接口:定义为 interface{},可以表示任何类型
任意类型都实现了空接口。常用于需要存储任意类型数据的场景,如泛型容器、通用参数等。

func printValue(val interface{}) {
        fmt.Printf("Value: %v, Type: %T\n", val, val)
}

// 使用空接口实现可以保存任意值的字典
var studentInfo = make(map[string]interface{})
    studentInfo["name"] = "李白"
    studentInfo["age"] = 18

匿名字段:go支持只提供类型而不写字段名的方式,也就是匿名字段

type Person struct {
    name string
    sex  string
    age  int
}

type Student struct {
    Person
    id   int
    addr string
}

s1 := Student{Person{"5lmh", "man", 20}, 1, "bj"}

一个接口的方法,不一定需要由一个类型完全实现,接口的方法可以通过在类型中嵌入其他类型或者结构体来实现。

// WashingMachine 洗衣机
type WashingMachine interface {
    wash()
    dry()
}

// 甩干器
type dryer struct{}

// 实现WashingMachine接口的dry()方法
func (d dryer) dry() {
    fmt.Println("甩一甩")
}

// 海尔洗衣机
type haier struct {
    dryer //嵌入甩干器
}

// 实现WashingMachine接口的wash()方法
func (h haier) wash() {
    fmt.Println("洗刷刷")
}

接口类型转换

// 类型断言,将接口类型转换为指定类型
value.(type) 
value.(T)

var i interface{} = "Hello, World"
    str, ok := i.(string)
    if ok {
        fmt.Printf("'%s' is a string\n", str)
    } else {
        fmt.Println("conversion failed")
    }

func justifyType(x interface{}) {
    switch v := x.(type) {
    case string:
        fmt.Printf("x is a string,value is %v\n", v)
    case int:
        fmt.Printf("x is a int is %v\n", v)
    case bool:
        fmt.Printf("x is a bool is %v\n", v)
    default:
        fmt.Println("unsupport type!")
    }
}

接口可以通过嵌套组合,实现更复杂的行为描述

type Reader interface {
        Read() string
}

type Writer interface {
        Write(data string)
}

type ReadWriter interface {
        Reader
        Writer
}
13. 错误处理

Go 标准库定义了一个 error 接口,表示一个错误的抽象,任何实现了 Error() 方法的类型都可以作为错误

type error interface {
    Error() string
}

使用 errors 包创建错误

import (
    "errors"
    "fmt"
)

func main() {
    err := errors.New("this is an error")
    fmt.Println(err) // 输出:this is an error
}

// 函数通常在最后的返回值中返回错误信息,使用 errors.New 可返回一个错误信息
func Sqrt(f float64) (float64, error) {
    if f < 0 {
        return 0, errors.New("math: square root of negative number")
    }
    // 实现
}

result, err:= Sqrt(-1)

if err != nil {
   fmt.Println(err)
}

fmt.Errorf 用于格式化错误信息并返回一个 error 类型的值。它通常在需要生成带有详细信息的错误时使用。

err := fmt.Errorf("文件不存在: %s", "example.txt")
fmt.Println(err) // 输出: 文件不存在: example.txt

自定义错误

type DivideError struct {
        Dividend int
        Divisor  int
}

func (e *DivideError) Error() string {
        return fmt.Sprintf("cannot divide %d by %d", e.Dividend, e.Divisor)
}

func divide(a, b int) (int, error) {
        if b == 0 {
                return 0, &DivideError{Dividend: a, Divisor: b}
        }
        return a / b, nil
}

func main() {
        _, err := divide(10, 0)
        if err != nil {
                fmt.Println(err) // 输出:cannot divide 10 by 0
        }
}
var ErrNotFound = errors.New("not found")

func findItem(id int) error {
		// `%w` 是一个特殊的格式化动词,用于包装错误,使得 `errors.Is` 和 `errors.As` 可以正确处理嵌套错误。
        return fmt.Errorf("database error: %w", ErrNotFound)
}

func main() {
        err := findItem(1)
        // errors.Is检查某个错误是否是特定错误或由该错误包装而成。
        if errors.Is(err, ErrNotFound) {
                fmt.Println("Item not found")
        } else {
                fmt.Println("Other error:", err)
        }
}

type MyError struct {
        Code int
        Msg  string
}

func (e *MyError) Error() string {
        return fmt.Sprintf("Code: %d, Msg: %s", e.Code, e.Msg)
}

func getError() error {
        return &MyError{Code: 404, Msg: "Not Found"}
}

func main() {
        err := getError()
        var myErr *MyError
        // errors.As将错误转换为特定类型以便进一步处理
        if errors.As(err, &myErr) {
                fmt.Printf("Custom error - Code: %d, Msg: %s\n", myErr.Code, myErr.Msg)
        }
}

panic 和 recover
使用 panic 抛出错误,recover 捕获错误。
异常的使用场景简单描述:Go中可以抛出一个panic的异常,然后在defer中通过recover捕获这个异常,然后正常处理

panic:

  • 假如函数F中书写了panic语句,会终止其后要执行的代码,在panic所在函数F内如果存在要执行的defer函数列表,按照defer的逆序执行
  • 返回函数F的调用者G,在G中,调用函数F语句之后的代码不会执行,假如函数G中存在要执行的defer函数列表,按照defer的逆序执行
  • 直到goroutine整个退出,并报告错误

recover:

  • 用来控制一个goroutine的panicking行为,捕获panic,从而影响应用的行为
  • 一般的调用建议
    a). 在defer函数中,通过recever来终止一个goroutine的panicking过程,从而恢复正常代码的执行
    b). 可以获取通过panic传递的error
func safeFunction() {
        defer func() {
                if r := recover(); r != nil {
                        fmt.Println("Recovered from panic:", r)
                }
        }()
        panic("something went wrong")
}

1.利用recover处理panic指令,defer 必须放在 panic 之前定义,另外 recover 只有在 defer 调用的函数中才有效。否则当panic时,recover无法捕获到panic,无法防止panic扩散。
2.recover 处理异常后,逻辑并不会恢复到 panic 那个点去,函数跑到 defer 之后的那个点。
3.多个 defer 会形成 defer 栈,后定义的 defer 语句会被最先调用。

如何区别使用 panic 和 error 两种方式?

惯例是:导致关键流程出现不可修复性错误的使用 panic,其他使用 error

二、单元测试

Go语言中的测试依赖go test命令。go test命令是一个按照一定约定和组织的测试代码的驱动程序。
在包目录内,所有以_test.go为后缀名的源代码文件都是go test测试的一部分,不会被go build编译到最终的可执行文件中。

Golang单元测试对文件名和方法名,参数都有很严格的要求

  • 文件名必须以xx_test.go命名
  • 方法必须是Test[^a-z]开头
  • 方法参数必须 t *testing.T
  • 使用go test执行单元测试

通过go help test可以看到go test的使用说明:

格式形如: go test [-c] [-i] [build flags] [packages] [flags for test binary]

1. 测试函数

测试函数的名字必须以Test开头,可选的后缀名必须以大写字母开头

func TestName(t *testing.T){
    // ...
}

其中参数t用于报告测试失败和附加的日志信息。testing.T的拥有的方法如下:

func (c *T) Error(args ...interface{})
func (c *T) Errorf(format string, args ...interface{})
func (c *T) Fail()
func (c *T) FailNow()
func (c *T) Failed() bool
func (c *T) Fatal(args ...interface{})
func (c *T) Fatalf(format string, args ...interface{})
func (c *T) Log(args ...interface{})
func (c *T) Logf(format string, args ...interface{})
func (c *T) Name() string
func (t *T) Parallel()
func (t *T) Run(name string, f func(t *T)) bool
func (c *T) Skip(args ...interface{})
func (c *T) SkipNow()
func (c *T) Skipf(format string, args ...interface{})
func (c *T) Skipped() bool
func TestSplit(t *testing.T) { // 测试函数名必须以Test开头,必须接收一个*testing.T类型参数
    got := Split("a:b:c", ":")         // 程序输出的结果
    want := []string{"a", "b", "c"}    // 期望的结果
    if !reflect.DeepEqual(want, got) { // 因为slice不能比较直接,借助反射包中的方法比较
        t.Errorf("excepted:%v, got:%v", want, got) // 测试失败输出错误提示
    }
}

// 测试组
type test struct {
        input string
        sep   string
        want  []string
    }
    // 定义一个存储测试用例的切片
    tests := []test{
        {input: "a:b:c", sep: ":", want: []string{"a", "b", "c"}},
        {input: "a:b:c", sep: ",", want: []string{"a:b:c"}},
        {input: "abcd", sep: "bc", want: []string{"a", "d"}},
        {input: "枯藤老树昏鸦", sep: "老", want: []string{"枯藤", "树昏鸦"}},
    }
    // 遍历切片,逐一执行测试用例
    for _, tc := range tests {
        got := Split(tc.input, tc.sep)
        if !reflect.DeepEqual(got, tc.want) {
            t.Errorf("excepted:%v, got:%v", tc.want, got)
        }
    }
2. Setup与TearDown

测试程序有时需要在测试之前进行额外的设置(setup)或在测试之后进行拆卸(teardown)。

TestMain
通过在*_test.go文件中定义TestMain函数来可以在测试之前进行额外的设置(setup)或在测试之后进行拆卸(teardown)操作。

如果测试文件包含函数:func TestMain(m *testing.M)那么生成的测试会先调用 TestMain(m),然后再运行具体测试。TestMain运行在主goroutine中, 可以在调用 m.Run前后做任何设置(setup)和拆卸(teardown)。退出测试的时候应该使用m.Run的返回值作为参数调用os.Exit。

func TestMain(m *testing.M) {
    fmt.Println("write setup code here...") // 测试之前的做一些设置
    retCode := m.Run()                         // 执行测试
    fmt.Println("write teardown code here...") // 测试之后做一些拆卸工作
    os.Exit(retCode)                           // 退出测试
}
3. GoMock

GoMock 是一个用于 Go 编程语言的模拟框架,它由 Google 开发并维护。GoMock 与 Go 的标准测试包 testing 紧密集成,使得开发者可以轻松地创建和使用模拟对象来测试代码的各个部分。

安装 GoMock

go get github.com/golang/mock/gomock
go get github.com/golang/mock/mockgen
// user_repository.go
package repository
 
type UserRepository interface {
    FindById(id int) (User, error)
    Save(user User) error
}

使用 mockgen 工具生成模拟代码:

mockgen -source=user_repository.go -destination=mocks/mock_user_repository.go -package=mocks

生成的 Mock 代码会包含一个 MockUserRepository 结构体,实现了 UserRepository 接口。

测试用例

// user_repository_test.go
package repository
 
import (
    "testing"
    "github.com/golang/mock/gomock"
    "path/to/your/mocks"
)
 
func TestUserRepository(t *testing.T) {
    ctrl := gomock.NewController(t) // 初始化 Mock 控制器
    defer ctrl.Finish() // 确保 Mock 被正确清理
 
    mockUserRepo := mocks.NewMockUserRepository(ctrl)
 
    user := User{ID: 1, Name: "Alice"}
    mockUserRepo.EXPECT().FindById(1).Return(user, nil)
    mockUserRepo.EXPECT().Save(user).Return(nil)
 
    foundUser, err := mockUserRepo.FindById(1)
    if err != nil {
        t.Errorf("FindById returned an error: %v", err)
    }
    if foundUser != user {
        t.Errorf("FindById did not return the expected user")
    }
 
    err = mockUserRepo.Save(user)
    if err != nil {
        t.Errorf("Save returned an error: %v", err)
    }
}
// 设置调用次数
mockUserService.EXPECT().GetUser(1).Return(&user.User{ID: 1, Name: "John"}, nil).Times(1)
// 设置调用顺序
gomock.InOrder(
    mockUserService.EXPECT().GetUser(1).Return(&user.User{ID: 1, Name: "John"}, nil),
    mockUserService.EXPECT().GetUser(2).Return(&user.User{ID: 2, Name: "Jane"}, nil),
)
// 匹配任意参数
mockUserService.EXPECT().GetUser(gomock.Any()).Return(&user.User{ID: 1, Name: "John"}, nil)
4. Testify

testify 是 Go 语言中一个非常流行的测试工具库,提供了丰富的断言功能、Mock 支持以及测试套件管理。

安装Testify

go get github.com/stretchr/testify

testify 提供了 assert 和 require 两个包,用于编写更简洁的测试断言。
assert 在断言失败时标记测试失败,但不会终止测试执行

func TestAdd(t *testing.T) {
    result := Add(2, 3)
    assert.Equal(t, 5, result, "2 + 3 should be 5") // 断言相等
    assert.NotEqual(t, 4, result, "2 + 3 should not be 4") // 断言不相等
    assert.Nil(t, err, "error should be nil") // 断言 nil
    assert.NotNil(t, obj, "object should not be nil") // 断言非 nil
}

require 在断言失败时会立即终止测试执行

func TestDivide(t *testing.T) {
    result, err := Divide(10, 2)
    require.NoError(t, err, "divide should not return error") // 断言无错误
    require.Equal(t, 5, result, "10 / 2 should be 5") // 断言相等
}

断言调用次数

type MockCalculator struct {
    mock.Mock
}

func (m *MockCalculator) Add(a, b int) int {
    args := m.Called(a, b) // 记录调用参数
    return args.Int(0) // 返回第一个参数
}

func (m *MockCalculator) Subtract(a, b int) int {
    args := m.Called(a, b)
    return args.Int(0)
}

func TestCalculator(t *testing.T) {
    // 创建 Mock 对象
    mockCalc := new(MockCalculator)

    // 设置 Mock 行为
    mockCalc.On("Add", 2, 3).Return(5) // 当 Add(2, 3) 被调用时返回 5
    mockCalc.On("Subtract", 5, 3).Return(2) // 当 Subtract(5, 3) 被调用时返回 2

    // 调用 Mock 方法
    result := mockCalc.Add(2, 3)
    assert.Equal(t, 5, result, "2 + 3 should be 5")

    result = mockCalc.Subtract(5, 3)
    assert.Equal(t, 2, result, "5 - 3 should be 2")

    // 验证 Mock 调用
    mockCalc.AssertCalled(t, "Add", 2, 3)
    mockCalc.AssertCalled(t, "Subtract", 5, 3)
    mockCalc.AssertNumberOfCalls(t, "Add", 1)
    mockCalc.AssertNumberOfCalls(t, "Subtract", 1)
}

三、标准库

1.fmt
  • Print函数直接输出内容
  • Printf函数支持格式化输出字符串
  • Println函数会在输出内容的结尾添加一个换行符。
    fmt.Print("在终端打印该信息。")
    name := "枯藤"
    fmt.Printf("我是:%s\n", name)
    fmt.Println("在终端打印单独一行显示")

Fprint系列函数会将内容输出到一个io.Writer接口类型的变量w中,我们通常用这个函数往文件中写入内容。

fmt.Fprintln(os.Stdout, "向标准输出写入内容")
fileObj, err := os.OpenFile("./xx.txt", os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0644)
name := "枯藤"
// 向打开的文件句柄中写入内容
fmt.Fprintf(fileObj, "往文件中写如信息:%s", name)

Sprint系列函数会把传入的数据生成并返回一个字符串

s1 := fmt.Sprint("枯藤")

Errorf函数根据format参数生成格式化字符串并返回一个包含该字符串的错误。

err := fmt.Errorf("这是一个错误")

格式化占位符 :

  • %v:值的默认格式表示
  • %+v:类似%v,但输出结构体时会添加字段名
  • %T:打印值的类型
  • %%:百分号
  • %t:布尔型
  • %d:十进制整型
  • %s:直接输出字符串或者[]byte
  • %p:指针,表示为十六进制,并加上前导的0x
  • %9.2f:宽度9,精度2

fmt.Scan

  • Scan从标准输入扫描文本,读取由空白符分隔的值保存到传递给本函数的参数中,换行符视为空白符。
  • 本函数返回成功扫描的数据个数和遇到的任何错误。如果读取的数据个数比提供的参数少,会返回一个错误报告原因
func main() {
    var (
        name    string
        age     int
        married bool
    )
    fmt.Scan(&name, &age, &married)
    fmt.Printf("扫描结果 name:%s age:%d married:%t \n", name, age, married)
}

Scanln类似Scan,它在遇到换行时才停止扫描。最后一个数据后面必须有换行或者到达结束位置。

有时候我们想完整获取输入的内容,而输入的内容可能包含空格,这种情况下可以使用bufio包来实现

func bufioDemo() {
    reader := bufio.NewReader(os.Stdin) // 从标准输入生成读对象
    fmt.Print("请输入内容:")
    text, _ := reader.ReadString('\n') // 读到换行
    text = strings.TrimSpace(text)
    fmt.Printf("%#v\n", text)
}

fmt.Scan、fmt.Scanf、fmt.Scanln三个函数,从io.Reader中读取数据。

func Fscan(r io.Reader, a ...interface{}) (n int, err error)
func Fscanln(r io.Reader, a ...interface{}) (n int, err error)
func Fscanf(r io.Reader, format string, a ...interface{}) (n int, err error)
2. Time

time.Now()函数获取当前的时间对象

func timeDemo() {
    now := time.Now() //获取当前时间
    fmt.Printf("current time:%v\n", now)

    year := now.Year()     //年
    month := now.Month()   //月
    day := now.Day()       //日
    hour := now.Hour()     //小时
    minute := now.Minute() //分钟
    second := now.Second() //秒
    timestamp1 := now.Unix()     //时间戳
    timestamp2 := now.UnixNano() //纳秒时间戳
	// time.Unix()函数可以将时间戳转为时间格式
	timeObj := time.Unix(timestamp, 0) //将时间戳转为时间格式
    fmt.Println(timeObj)
}

time.Duration是time包定义的一个类型,它代表两个时间点之间经过的时间,以纳秒为单位
time包中定义的时间间隔类型的常量如下

const (
    Nanosecond  Duration = 1
    Microsecond          = 1000 * Nanosecond
    Millisecond          = 1000 * Microsecond
    Second               = 1000 * Millisecond
    Minute               = 60 * Second
    Hour                 = 60 * Minute
)

时间操作

now := time.Now()
later := now.Add(time.Hour) // 当前时间加1小时后的时间

func (t Time) Add(d Duration) Time
func (t Time) Sub(u Time) Duration
func (t Time) Equal(u Time) bool
func (t Time) Before(u Time) bool
func (t Time) After(u Time) bool

定时器

使用time.Tick(时间间隔)来设置定时器,定时器的本质上是一个通道(channel)
func tickDemo() {
    ticker := time.Tick(time.Second) //定义一个1秒间隔的定时器
    for i := range ticker {
        fmt.Println(i)//每秒都会执行的任务
    }
}

时间格式化
时间类型有一个自带的方法Format进行格式化,需要注意的是Go语言中格式化时间模板不是常见的Y-m-d H:M:S而是使用Go的诞生时间2006年1月2号15点04分(记忆口诀为2006 1 2 3 4)。

fmt.Println(now.Format("2006-01-02 15:04:05.000 Mon Jan"))
fmt.Println(now.Format("2006/01/02"))

// 加载时区
loc, err := time.LoadLocation("Asia/Shanghai")

// 按照指定时区和指定格式解析字符串时间
timeObj, err := time.ParseInLocation("2006/01/02 15:04:05", "2019/08/04 14:15:20", loc)
3. IO操作
func Create(name string) (file *File, err Error)
根据提供的文件名创建新的文件,返回一个文件对象,默认权限是0666
func NewFile(fd uintptr, name string) *File
根据文件描述符创建相应的文件,返回一个文件对象
func Open(name string) (file *File, err Error)
只读方式打开一个名称为name的文件
func OpenFile(name string, flag int, perm uint32) (file *File, err Error)
打开名称为name的文件,flag是打开的方式,只读、读写等,perm是权限
func (file *File) Write(b []byte) (n int, err Error)
写入byte类型的信息到文件
func (file *File) WriteAt(b []byte, off int64) (n int, err Error)
在指定位置开始写入byte类型的信息
func (file *File) WriteString(s string) (ret int, err Error)
写入string信息到文件
func (file *File) Read(b []byte) (n int, err Error)
读取数据到b中
func (file *File) ReadAt(b []byte, off int64) (n int, err Error)
从off开始读取数据到b中
func Remove(name string) Error
删除文件名为name的文件
// 只读方式打开当前目录下的main.go文件
file, err := os.Open("./main.go")
    if err != nil {
        fmt.Println("open file failed!, err:", err)
        return
    }
// 关闭文件
file.Close()
	file, err := os.Create("./xxx.txt")
    if err != nil {
        fmt.Println(err)
        return
    }
    defer file.Close()
    for i := 0; i < 5; i++ {
        file.WriteString("ab\n")
        file.Write([]byte("cd\n"))
    }
file, err := os.Open("./xxx.txt")
    if err != nil {
        fmt.Println("open file err :", err)
        return
    }
    defer file.Close()
    // 定义接收文件读取的字节数组
    var buf [128]byte
    var content []byte
    for {
        n, err := file.Read(buf[:])
        // 读到文件末尾会返回io.EOF的错误
        if err == io.EOF {
            // 读取结束
            break
        }
        if err != nil {
            fmt.Println("read file err ", err)
            return
        }
        content = append(content, buf[:n]...)
    }
    fmt.Println(string(content))

bufio:bufio包实现了带缓冲区的读写,是对文件读写的封装

file, err := os.OpenFile("./xxx.txt", os.O_CREATE|os.O_WRONLY, 0666)
    if err != nil {
        return
    }
    defer file.Close()
    // 获取writer对象
    writer := bufio.NewWriter(file)
    for i := 0; i < 10; i++ {
        writer.WriteString("hello\n")
    }
    // 刷新缓冲区,强制写出
    writer.Flush()

file, err := os.Open("./xxx.txt")
    if err != nil {
        return
    }
    defer file.Close()
    reader := bufio.NewReader(file)
    for {
        line, _, err := reader.ReadLine()
        if err == io.EOF {
            break
        }
        if err != nil {
            return
        }
        fmt.Println(string(line))
    }
4. Strconv

strconv包实现了基本数据类型与其字符串表示的转换

s1 := "100"
i1, err := strconv.Atoi(s1) // 将字符串类型的整数转换为int类型

i2 := 200
s2 := strconv.Itoa(i2) // 将int类型数据转换为对应的字符串表示

b, err := strconv.ParseBool("true")
f, err := strconv.ParseFloat("3.1415", 64)
i, err := strconv.ParseInt("-2", 10, 64) // 10进制int64
u, err := strconv.ParseUint("2", 10, 64)

s1 := strconv.FormatBool(true)
s2 := strconv.FormatFloat(3.1415, 'E', -1, 64) // ’E’(-d.ddddE±dd,十进制指数), ’f’(-ddd.dddd),’e’(-d.dddde±dd,十进制指数) 
s3 := strconv.FormatInt(-2, 16)
s4 := strconv.FormatUint(2, 16)
5. json

编码json使用json.Marshal()函数可以对一组数据进行JSON格式的编码

type Person struct {
	 //"-"是忽略的意思
    Name  string	`json:"-"`
    Hobby string	`json:"hobby"`
}

func main() {
    p := Person{"5lmh.com", "女"}
    // 编码json
    b, err := json.Marshal(p)
    if err != nil {
        fmt.Println("json err ", err)
    }
    fmt.Println(string(b))

解码json使用json.Unmarshal()函数可以对一组数据进行JSON格式的解码

type Person struct {
    Age       int    `json:"age,string"`
    Name      string `json:"name"`
    Niubility bool   `json:"niubility"`
}

    // 假数据
    b := []byte(`{"age":"18","name":"5lmh.com","marry":false}`)
    var p Person
    err := json.Unmarshal(b, &p)
    if err != nil {
        fmt.Println(err)
    }
    fmt.Println(p)

	var i interface{}
    err := json.Unmarshal(b, &i)
    if err != nil {
        fmt.Println(err)
    }
    // 自动转到map
    fmt.Println(i)
    // 可以判断类型
    m := i.(map[string]interface{})
    for k, v := range m {
        switch vv := v.(type) {
        case float64:
            fmt.Println(k, "是float64类型", vv)
        case string:
            fmt.Println(k, "是string类型", vv)
        default:
            fmt.Println("其他")
        }
    }
6. 反射

reflect包封装了反射相关的方法

  • 获取类型信息:reflect.TypeOf,是静态的
  • 获取值信息:reflect.ValueOf,是动态的
t := reflect.TypeOf(a)
   fmt.Println("类型是:", t)
   // kind()可以获取具体类型
   k := t.Kind()
   fmt.Println(k)
   switch k {
   case reflect.Float64:
      fmt.Printf("a is float64\n")
   case reflect.String:
      fmt.Println("string")
   }

v := reflect.ValueOf(a)
k := v.Kind()

// 反射修改值
v := reflect.ValueOf(a)
k := v.Kind()
switch k {
	case reflect.Float64:
     	// 反射修改值
     	v.SetFloat(6.9)
     	fmt.Println("a is ", v.Float())
    case reflect.Ptr:
        // Elem()获取地址指向的值
        v.Elem().SetFloat(7.9)
        fmt.Println("case:", v.Elem().Float())
        // 地址
        fmt.Println(v.Pointer())
    }

结构体与反射

type User struct {
    Id   int
    Name string
    Age  int
}

// 绑方法
func (u User) Hello() {
    fmt.Println("Hello")
}

func Poni(o interface{}) {
	t := reflect.TypeOf(o)
	for i := 0; i < t.NumField(); i++ {
        // 取每个字段
        f := t.Field(i)
        fmt.Printf("%s : %v", f.Name, f.Type)
        // 获取字段的值信息
        // Interface():获取字段对应的值
        val := v.Field(i).Interface()
        fmt.Println("val :", val)
    }

	for i := 0; i < t.NumMethod(); i++ {
        m := t.Method(i)
        fmt.Println(m.Name)
        fmt.Println(m.Type)
    }
    
	v := reflect.ValueOf(o)
    // 获取指针指向的元素
    v = v.Elem()
    // 取字段
    f := v.FieldByName("Name")
    if f.Kind() == reflect.String {
        f.SetString("kuteng") //修改字段
    }

	u := User{1, "5lmh.com", 20}
    v := reflect.ValueOf(u)
    // 获取方法
    m := v.MethodByName("Hello")
    // 构建一些参数
    args := []reflect.Value{reflect.ValueOf("6666")}
    // 没参数的情况下:var args2 []reflect.Value
    // 调用方法,需要传入方法的参数
    m.Call(args)
}

四、并发

1. Goroutine

goroutine的概念类似于线程,但 goroutine是由Go的运行时(runtime)调度和管理的。Go程序会智能地将 goroutine 中的任务合理地分配给每个CPU。Go语言之所以被称为现代化的编程语言,就是因为它在语言层面已经内置了调度和上下文切换的机制。

只需要在调用函数的时候在前面加上go关键字,就可以为一个函数创建一个goroutine。

一个goroutine必定对应一个函数,可以创建多个goroutine去执行相同的函数。

func hello() {
    fmt.Println("Hello Goroutine!")
}
func main() {
    go hello() // 启动另外一个goroutine去执行hello函数
    fmt.Println("main goroutine done!")
}

主线程结束了,goroutine线程也会停止

一个goroutine的栈在其生命周期开始时只有很小的栈(典型情况下2KB),goroutine的栈不是固定的,他可以按需增大和缩小,goroutine的栈大小限制可以达到1GB,虽然极少会用到这个大。所以在Go语言中一次创建十万左右的goroutine也是可以的。

  • goroutine调度

GPM是Go语言运行时(runtime)层面的实现,是go语言自己实现的一套调度系统。区别于操作系统调度OS线程。

1.G很好理解,就是一个goroutine,里面除了存放本goroutine信息外 还有与所在P的绑定等信息。
2.P管理着一组goroutine队列,P里面会存储当前goroutine运行的上下文环境(函数指针,堆栈地址及地址边界),P会对自己管理的goroutine队列做一些调度(比如把占用CPU时间较长的goroutine暂停、运行后续的goroutine等等)当自己的队列消费完了就去全局队列里取,如果全局队列里也消费完了会去其他P的队列里抢任务
3.M(machine)是Go运行时(runtime)对操作系统内核线程的虚拟, M与内核线程一般是一一映射的关系, 一个groutine最终是要放到M上执行的
P与M一般也是一一对应的。他们关系是: P管理着一组G挂载在M上运行。当一个G长久阻塞在一个M上时,runtime会新建一个M,阻塞G所在的P会把其他的G 挂载在新建的M上。当旧的G阻塞完成或者认为其已经死掉时 回收旧的M。

P的个数是通过runtime.GOMAXPROCS设定(最大256),Go1.5版本之后默认为物理线程数。 在并发量大的时候会增加一些P和M,但不会太多,切换太频繁的话得不偿失。

单从线程调度讲,Go语言相比起其他语言的优势在于OS线程是由OS内核来调度的,goroutine则是由Go运行时(runtime)自己的调度器调度的,这个调度器使用一个称为m:n调度的技术(复用/调度m个goroutine到n个OS线程)。 其一大特点是goroutine的调度是在用户态下完成的, 不涉及内核态与用户态之间的频繁切换,包括内存的分配与释放,都是在用户态维护着一块大的内存池, 不直接调用系统的malloc函数(除非内存池需要改变),成本比调度OS线程低很多。 另一方面充分利用了多核的硬件资源,近似的把若干goroutine均分在物理线程上, 再加上本身goroutine的超轻量,以上种种保证了go调度方面的性能。

Go语言中的操作系统线程和goroutine的关系:

  1. 一个操作系统线程对应用户态多个goroutine
  2. go程序可以同时使用多个操作系统线程
  3. goroutine和OS线程是多对多的关系,即m:n
2. runtime包
  • runtime.Gosched():让出CPU时间片,重新等待安排任务
  • runtime.Goexit():退出当前协程
func main() {
    go func() {
        defer fmt.Println("A.defer")
        func() {
            defer fmt.Println("B.defer")
            // 结束协程
            runtime.Goexit()
            defer fmt.Println("C.defer")
            fmt.Println("B")
        }()
        fmt.Println("A")
    }()
    for {
    }
}

输出:
B.defer
A.defer
  • runtime.GOMAXPROCS:Go运行时的调度器使用GOMAXPROCS参数来确定需要使用多少个OS线程来同时执行Go代码。默认值是机器上的CPU核心数,可以通过runtime.GOMAXPROCS()函数设置当前程序并发时占用的CPU逻辑核心数。
func main() {
    runtime.GOMAXPROCS(1)
    go a()
    go b()
    time.Sleep(time.Second)
}
3. Channel

虽然可以使用共享内存进行数据交换,但是共享内存在不同的goroutine中容易发生竞态问题。为了保证数据交换的正确性,必须使用互斥量对内存进行加锁,这种做法势必造成性能问题。

Go语言的并发模型是CSP(Communicating Sequential Processes),提倡通过通信共享内存而不是通过共享内存而实现通信

如果说goroutine是Go程序并发的执行体,channel就是它们之间的连接。channel是可以让一个goroutine发送特定值到另一个goroutine的通信机制。

Go 语言中的通道(channel)是一种特殊的类型。通道像一个传送带或者队列,总是遵循先入先出(First In First Out)的规则,保证收发数据的顺序。每一个通道都是一个具体类型的导管,也就是声明channel的时候需要为其指定元素类型。

  • channel使用
var 变量 chan 元素类型

var ch1 chan int   // 声明一个传递整型的通道
var ch2 chan bool  // 声明一个传递布尔型的通道

make(chan 元素类型, [缓冲大小])
ch4 := make(chan int)

通道有**发送(send)、接收(receive)和关闭(close)**三种操作

ch <- 10 // 把10发送到ch中

x := <- ch // 从ch中接收值并赋值给变量x
<-ch       // 从ch中接收值,忽略结果

close(ch) //关闭channel

只有在通知接收方goroutine所有的数据都发送完毕的时候才需要关闭通道。通道是可以被垃圾回收机制回收的,它和关闭文件是不一样的,在结束操作之后关闭文件是必须要做的,但关闭通道不是必须的

关闭后的通道有以下特点:

  • 对一个关闭的通道再发送值就会导致panic。
  • 对一个关闭的通道进行接收会一直获取值直到通道为空。
  • 对一个关闭的并且没有值的通道执行接收操作会得到对应类型的零值。
  • 关闭一个已经关闭的通道会导致panic。
无缓冲的通道

无缓冲的通道又称为阻塞的通道,无缓冲的通道只有在有接收者的时候才能发送值,如果没有接收者会出现死锁

无缓冲通道上的发送操作会阻塞,直到另一个goroutine在该通道上执行接收操作,这时值才能发送成功,两个goroutine将继续执行。相反,如果接收操作先执行,接收方的goroutine将阻塞,直到另一个goroutine在该通道上发送一个值。

使用无缓冲通道进行通信将导致发送和接收的goroutine同步化。因此,无缓冲通道也被称为同步通道。

func recv(c chan int) {
    ret := <-c
    fmt.Println("接收成功", ret)
}
func main() {
    ch := make(chan int)
    go recv(ch) // 启用goroutine从通道接收值
    ch <- 10
    fmt.Println("发送成功")
}
有缓冲的通道

可以在使用make函数初始化通道的时候为其指定通道的容量,只要通道的容量大于零,那么该通道就是有缓冲的通道,通道的容量表示通道中能存放元素的数量。如果容量满了发送就会阻塞,直到有接受者取走值

func main() {
    ch := make(chan int, 1) // 创建一个容量为1的有缓冲区通道
    ch <- 10
    fmt.Println("发送成功")
}
优雅的从通道循环取值

通道关闭后会退出for range循环

    go func() {
        for {
            i, ok := <-ch1 // 通道关闭后再取值ok=false
            if !ok {
                break
            }
            ch2 <- i * i
        }
        close(ch2)
    }()
    // 在主goroutine中从ch2中接收值打印
    for i := range ch2 { // 通道关闭后会退出for range循环
        fmt.Println(i)
    }
单向通道

有的时候我们会将通道作为参数在多个任务函数间传递,很多时候我们在不同的任务函数中使用通道都会对其进行限制,比如限制通道在函数中只能发送或只能接收。

Go语言中提供了单向通道来处理这种情况。

// chan<- int是一个只能发送的通道,可以发送但是不能接收;
// <-chan int是一个只能接收的通道,可以接收但是不能发送。
func squarer(out chan<- int, in <-chan int) {
    for i := range in {
        out <- i * i
    }
    close(out)
}

4. 协程池
// 创建工作池
// 参数1:开几个协程
func createPool(num int, jobChan chan *Job, resultChan chan *Result) {
    // 根据开协程个数,去跑运行
    for i := 0; i < num; i++ {
        go func(jobChan chan *Job, resultChan chan *Result) {
            // 执行运算
            // 遍历job管道所有数据,进行相加
            for job := range jobChan {
                // 随机数接过来
                r_num := job.RandNum
                // 随机数每一位相加
                // 定义返回值
                var sum int
                for r_num != 0 {
                    tmp := r_num % 10
                    sum += tmp
                    r_num /= 10
                }
                // 想要的结果是Result
                r := &Result{
                    job: job,
                    sum: sum,
                }
                //运算结果扔到管道
                resultChan <- r
            }
        }(jobChan, resultChan)
    }
}
5. select多路复用

在某些场景下我们需要同时从多个通道接收数据。通道在接收数据时,如果没有数据可以接收将会发生阻塞。Go内置了select关键字,可以同时响应多个通道的操作

select {
    case <-chan1:
       // 如果chan1成功读到数据,则进行该case处理语句
    case chan2 <- 1:
       // 如果成功向chan2写入数据,则进行该case处理语句
    default:
       // 如果上面都没有成功,则进入default处理流程
    }

使用default可以判定管道是否存满

6. 锁
互斥锁
var lock sync.Mutex

func add() {
    for i := 0; i < 5000; i++ {
        lock.Lock() // 加锁
        x = x + 1
        lock.Unlock() // 解锁
    }
}
读写锁
rwlock sync.RWMutex

func write() {
    rwlock.Lock() // 加写锁
    x = x + 1
    time.Sleep(10 * time.Millisecond) 
    rwlock.Unlock()                   // 解写锁
}

func read() {
    rwlock.RLock()               // 加读锁
    time.Sleep(time.Millisecond) 
    rwlock.RUnlock()             // 解读锁
}
7. Sync
sync.WaitGroup

类似CountDownLatch

func hello() {
    defer wg.Done()
    fmt.Println("Hello Goroutine!")
}
func main() {
    wg.Add(1)
    go hello() // 启动另外一个goroutine去执行hello函数
    fmt.Println("main goroutine done!")
    wg.Wait() // 阻塞直到计数器减为0
}
sync.Once

确保在高并发的场景下只执行一次

var icons map[string]image.Image

var loadIconsOnce sync.Once

func loadIcons() {
    icons = map[string]image.Image{
        "left":  loadIcon("left.png"),
        "up":    loadIcon("up.png"),
        "right": loadIcon("right.png"),
        "down":  loadIcon("down.png"),
    }
}

// Icon 是并发安全的
func Icon(name string) image.Image {
    loadIconsOnce.Do(loadIcons)
    return icons[name]
}

sync.Once其实内部包含一个互斥锁和一个布尔值,互斥锁保证布尔值和数据的安全,而布尔值用来记录初始化是否完成。这样设计就能保证初始化操作的时候是并发安全的并且初始化操作也不会被执行多次。

sync.Map

类似ConcurrentHashMap,保证高并发场景下map的线程安全

var m = sync.Map{}

func main() {
    wg := sync.WaitGroup{}
    for i := 0; i < 20; i++ {
        wg.Add(1)
        go func(n int) {
            key := strconv.Itoa(n)
            m.Store(key, n)
            value, _ := m.Load(key)
            fmt.Printf("k=:%v,v:=%v\n", key, value)
            wg.Done()
        }(i)
    }
    wg.Wait()
}
8. 原子操作(atomic包)

针对基本数据类型我们还可以使用原子操作来保证并发安全,因为原子操作是Go语言提供的方法它在用户态就可以完成,因此性能比加锁操作更好。

包含以下操作

  • 读取操作:LoadInt32(addr *int32) (val int32)
  • 写入操作:StoreInt32(addr *int32, val int32)
  • 修改操作: AddInt32(addr *int32, delta int32) (new int32)
  • 交换操作:SwapInt32(addr *int32, new int32) (old int32)
  • 比较并交换操作:CompareAndSwapInt32(addr *int32, old, new int32) (swapped bool)
var x int64
func atomicAdd() {
    atomic.AddInt64(&x, 1)
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值