Golang个人总结(基础部分)
本文最后更新于 226 天前,其中的信息可能已经有所发展或是发生改变。如有疑问或错误请反馈至邮箱super.lucky.qu@gmail.com

前言

普通字体内容为查阅无误,斜体部分为个人理解

电脑端可以于左栏“文章目录”来索引到指定小节,手机端可以通过左上角按钮唤出菜单索引到指定小节

正文

注释

单行注释

单行注释往往用于解释一行代码的作用,格式为两个双斜杠后跟想要注释的内容,例:

//控制台打印输出一行话
fmt.Print("I am an example !")

多行注释

多行注释以/*开头,以*/结尾,注释括中的内容,常用于解释一个函数的作用等用途,如:

/*定义一个对两个int类型参数求和并返回int类型的函数
参数a:加数之一
参数b:加数之一*/

func sum(a, b int) int {
    return a+b
}

常见用法

每个包都应该有一个包注释,注释里包含文件名称,文件描述,姓名和修改时间等,如:

//@Title HelloWorld
//@Description 在控制台输出HelloWorld
//@Author LuckyQu
//@Update 2024-11-9

结构体和接口

每个自定义的结构体或者接口都需要用注释说明,对结构进行简要介绍,如:

// User 用户对象,定义了用户的基本信息
type User struct{
    Username string //用户名
    Email    string //邮箱
}

函数和方法

每个函数和方法都应当用注释说明,如:

//定义一个对两个int类型参数求和并返回int类型的函数
//参数a int型 加数之一
//参数b int型 被加数
func sum(a, b int) int {
    return a+b
}

逻辑

可以用TODO表示这个区块需要完成,用FIXME表示这个模块需要修复或者改进,需要特别说明的问题可以用NOTE表示,如

// TODO 求和函数
func sum(a, b int) int {}

// FIXME 未在控制台输出值
func post(a int){
    print(a)
}

// NOTE 想说的
func something(){}

用法规范

  1. ‌所有导出对象都需要注释说明其用途;非导出对象根据情况进行注释‌
  2. ‌如果对象可数且无明确指定数量的情况下,一律使用单数形式和一般进行时描述;否则使用复数形式‌
  3. ‌包、函数、方法和类型的注释说明都是一个完整的句子‌
  4. ‌句子类型的注释首字母均需大写;短语类型的注释首字母需小写‌
  5. ‌注释的单行长度不能超过80个字符‌‌
  6. 在书写项目时,为了风格统一,可以统一使用一种,如行注释

标识符的声明

声明规则

  1. 字符组成:标识符可以由字母(a-z, A-Z)、数字(0-9)、下划线(_)组成,但不能以数字开头
  2. 大小写敏感:Go语言是大小写敏感的,因此myVarmyvar被视为不同的标识符
  3. 保留字:标识符不能与Go语言的关键字或保留字相同,例如funcvarconsttype
  4. 首字母约定
    • 如果标识符首字母小写(如myVar),则该标识符仅在定义它的包内可见
    • 如果标识符首字母大写(如MyVar),则该标识符在其他包中也可见(即对外导出)

分类

Go中标识符分为基本类型(值类型)和引用类型

基本类型(值类型)

数值类型:

  • 整型:intint8int16int32int64uintuint8uint16uint32uint64uintptr
  • 浮点型:float32float64
  • 复数型:complex64complex128
  • 布尔型:bool

字符串类型:

  • string

字符类型:

  • rune (本质是int32)

数组类型:

  • [长度]类型

引用类型

引用类型在内存中存储的是指向实际数据的指针。当将一个引用类型的值赋给另一个变量时,实际上是复制了指针,而不是实际的数据

指针类型

  • 指针:*T,其中T是任何类型

接口类型

  • 接口:interface{},以及其他自定义接口

切片类型

  • 切片:[]T,其中T是任何类型

映射类型

  • 映射:map[K]V,其中K是键类型,V是值类型

通道类型

  • 通道:chan T,其中T是任何类型

结构体类型

  • 结构体:struct,虽然结构体本身是值类型,但可以通过指针来引用结构体

变量

声明

可以使用var关键字来声明一个变量,若未初始化值,则值为类型对应零值,规定变量被声明之后必须使用,有如下几种声明方式:

//var + 变量名 + 类型名
var a int = 1 //此时a的值为int类型的默认零值0

//var + 变量名
var a = 1 //此时编译器会自动推断a的类型为int

//使用海象符号 := 进行短声明
//短声明不能用于声明全局变量(Go中函数体外必须以关键字为开头)
a := 1 //此时编译器会自动推断a的类型为int

//也可以一次声明多个变量
a, b := 1, 2 //此时会自动推断类型并将1,2分别赋值给a,b

//可以用小括号增强可读性
var(
    a int
    b float
    c bool
)

常量

使用const关键字以声明一个常量,常量用于存储不可变的数据,如:

//声明单一常量
const pi = 3.14
//或者一次声明多个
const{
    e = 2.718
    pi = 3.14
}

函数

使用func来定义一个函数,函数的格式如下所示:

//func关键字 + 接受者类型(可省略) + 函数名 + (传入参数) + 返回参数(可以在此处定义其名字) + 函数体({}) 
//定义一个温度类型
type temperature float64

func (temperature) post () (t temperature) {
    //some code
    return t
}

类型

可以使用type关键字来定义一个类型,可以是结构体struct或者是已有的类型如int64,例:

type Temp float64

type People struct(
    Name string
    Age  int64
)

其中,结构体就是一个字段,结构体字段可以用.来访问,也可以用指向结构体的指针来引用,如 (*p).X

使用package来声明包

数组&切片&Map

数组

在Go语言中,数组是一种基本类型,用于存储固定数量的相同类型的数据。数组的大小在声明时确定,并且在运行时不能改变。数组在内存中是连续存储的,这意味着你可以通过索引来快速访问数组中的元素

数组的声明和初始化

1. 声明数组

你可以使用以下语法声明一个数组:

var arrayName [size]elementType

其中:

  • arrayName 是数组的名称
  • size 是数组的大小,必须是一个常量
  • elementType 是数组中元素的类型

2. 初始化数组

数组可以在声明时进行初始化,也可以在声明后进行初始化

声明并初始化

var arr [5]int = [5]int{1, 2, 3, 4, 5}

省略大小

如果在声明时初始化数组,可以省略数组的大小,编译器会根据初始化的元素个数自动推断数组的大小:

var arr [5]int = [5]int{1, 2, 3, 4, 5} // 显式指定大小
var arr = [5]int{1, 2, 3, 4, 5}        // 省略大小,编译器推断

部分初始化

你还可以部分初始化数组,未初始化的元素会被自动设置为该类型的零值:

var arr [5]int = [5]int{1, 2, 3} // arr 的值为 [1, 2, 3, 0, 0]

访问数组元素

数组元素可以通过索引来访问和修改。索引从0开始,最后一个元素的索引是数组大小减1

var arr [5]int = [5]int{1, 2, 3, 4, 5}
fmt.Println(arr[0]) // 输出 1
arr[0] = 10
fmt.Println(arr[0]) // 输出 10

数组的遍历

可以使用 for 循环来遍历数组:

var arr [5]int = [5]int{1, 2, 3, 4, 5}
for i := 0; i < len(arr); i++ {
    fmt.Println(arr[i])
}

数组的多维

Go语言支持多维数组,可以通过嵌套的方括号来声明和初始化多维数组

var matrix [3][3]int = [3][3]int{
    {1, 2, 3},
    {4, 5, 6},
    {7, 8, 9},
}

// 访问多维数组的元素
fmt.Println(matrix[0][0]) // 输出 1
matrix[0][0] = 10
fmt.Println(matrix[0][0]) // 输出 10

示例

以下是一个完整的示例,展示了数组的声明、初始化、访问和遍历:

package main

import "fmt"

func main() {
    // 声明并初始化数组
    var arr [5]int = [5]int{1, 2, 3, 4, 5}

    // 访问数组元素
    fmt.Println("arr[0]:", arr[0])
    arr[0] = 10
    fmt.Println("arr[0] after modification:", arr[0])

    // 遍历数组
    for i := 0; i < len(arr); i++ {
        fmt.Printf("arr[%d]: %d\n", i, arr[i])
    }

    // 多维数组
    var matrix [3][3]int = [3][3]int{
        {1, 2, 3},
        {4, 5, 6},
        {7, 8, 9},
    }

    // 访问多维数组的元素
    fmt.Println("matrix[0][0]:", matrix[0][0])
    matrix[0][0] = 10
    fmt.Println("matrix[0][0] after modification:", matrix[0][0])

    // 遍历多维数组
    for i := 0; i < len(matrix); i++ {
        for j := 0; j < len(matrix[i]); j++ {
            fmt.Printf("matrix[%d][%d]: %d\n", i, j, matrix[i][j])
        }
    }
}

切片

在Go语言中,切片(slice)是一种引用类型,它提供了一种灵活的方式来处理动态数组。切片是对底层数组的抽象,允许你在运行时动态地调整大小。切片的三个重要属性是:指向底层数组的指针、长度(len)和容量(cap)

切片的基本概念

  1. 底层数组:切片内部引用了一个数组
  2. 长度(len):切片当前包含的元素个数
  3. 容量(cap):切片可以增长的最大长度,即底层数组从切片起始位置到数组末尾的元素个数

切片的声明和初始化

1. 声明切片

你可以使用以下语法声明一个切片:

var sliceName []elementType

其中:

  • sliceName 是切片的名称
  • elementType 是切片中元素的类型

2. 初始化切片

切片可以通过多种方式初始化:

使用 make 函数

slice := make([]int, length, capacity)
  • length 是切片的初始长度
  • capacity 是切片的容量(可选)

从数组创建切片

array := [5]int{1, 2, 3, 4, 5}
slice := array[1:4] // 创建一个包含 array[1], array[2], array[3] 的切片

直接初始化

slice := []int{1, 2, 3, 4, 5}

访问和修改切片元素

切片元素可以通过索引来访问和修改,索引从0开始,最后一个元素的索引是切片长度减1

slice := []int{1, 2, 3, 4, 5}
fmt.Println(slice[0]) // 输出 1
slice[0] = 10
fmt.Println(slice[0]) // 输出 10

切片的遍历

你可以使用 for 循环来遍历切片:

slice := []int{1, 2, 3, 4, 5}
for i := 0; i < len(slice); i++ {
    fmt.Println(slice[i])
}

切片的动态调整

切片可以通过 append 函数动态地增加元素

slice := []int{1, 2, 3}
slice = append(slice, 4) // 添加一个元素
fmt.Println(slice) // 输出 [1, 2, 3, 4]

slice = append(slice, 5, 6) // 添加多个元素
fmt.Println(slice) // 输出 [1, 2, 3, 4, 5, 6]

切片的子切片

你可以通过切片操作符来创建子切片

slice := []int{1, 2, 3, 4, 5}
subSlice := slice[1:4] // 创建一个包含 slice[1], slice[2], slice[3] 的子切片
fmt.Println(subSlice) // 输出 [2, 3, 4]

示例

以下是一个完整的示例,展示了切片的声明、初始化、访问、遍历和动态调整:

package main

import "fmt"

func main() {
    // 使用 make 函数创建切片
    slice1 := make([]int, 3, 5)
    slice1[0] = 1
    slice1[1] = 2
    slice1[2] = 3
    fmt.Println("slice1:", slice1)

    // 从数组创建切片
    array := [5]int{1, 2, 3, 4, 5}
    slice2 := array[1:4]
    fmt.Println("slice2:", slice2)

    // 直接初始化切片
    slice3 := []int{1, 2, 3, 4, 5}
    fmt.Println("slice3:", slice3)

    // 访问和修改切片元素
    fmt.Println("slice3[0]:", slice3[0])
    slice3[0] = 10
    fmt.Println("slice3[0] after modification:", slice3[0])

    // 遍历切片
    for i := 0; i < len(slice3); i++ {
        fmt.Printf("slice3[%d]: %d\n", i, slice3[i])
    }

    // 动态调整切片
    slice3 = append(slice3, 6)
    fmt.Println("slice3 after appending 6:", slice3)

    // 创建子切片
    subSlice := slice3[1:4]
    fmt.Println("subSlice:", subSlice)
}

数组和切片的区别

切片:动态大小,引用类型,赋值和传递时只复制指针

数组:固定大小,值类型,赋值和传递时会进行深拷贝

Map

在Go语言中,map 是一种内置的数据结构,用于存储键值对(key-value pairs)。map 是一种引用类型,可以在运行时动态地添加、删除和修改键值对。map 的键和值可以是任何类型,但键必须是可比较的类型(例如,基本类型、指针、接口、结构体等)

声明和初始化 map

1. 声明 map

你可以使用以下语法声明一个 map

var mapName map[keyType]valueType

其中:

  • mapNamemap 的名称
  • keyType 是键的类型
  • valueType 是值的类型

2. 初始化 map

map 可以通过多种方式初始化:

使用 make 函数

mapName := make(map[keyType]valueType)

直接初始化

mapName := map[keyType]valueType{
    key1: value1,
    key2: value2,
    // ...
}

访问和修改 map

1. 访问 map 中的值

你可以通过键来访问 map 中的值:

value := mapName[key]

2. 检查键是否存在

你可以使用多重赋值来检查键是否存在:

value, exists := mapName[key]
  • value 是键对应的值
  • exists 是一个布尔值,表示键是否存在

3. 修改 map 中的值

你可以通过键来修改 map 中的值:

mapName[key] = newValue

4. 删除 map 中的键值对

可以使用 delete 函数来删除 map 中的键值对:

delete(mapName, key)

遍历 map

可以使用 for 循环来遍历 map

for key, value := range mapName {
    fmt.Println("Key:", key, "Value:", value)
}

示例

以下是一个完整的示例,展示了 map 的声明、初始化、访问、修改、删除和遍历:

package main

import "fmt"

func main() {
    // 使用 make 函数创建 map
    ages := make(map[string]int)
    ages["Alice"] = 30
    ages["Bob"] = 25
    ages["Charlie"] = 35
    fmt.Println("ages:", ages)

    // 直接初始化 map
    scores := map[string]int{
        "Alice": 90,
        "Bob":   85,
        "Charlie": 95,
    }
    fmt.Println("scores:", scores)

    // 访问 map 中的值
    age := ages["Alice"]
    fmt.Println("Alice's age:", age)

    // 检查键是否存在
    score, exists := scores["David"]
    if exists {
        fmt.Println("David's score:", score)
    } else {
        fmt.Println("David not found")
    }

    // 修改 map 中的值
    ages["Alice"] = 31
    fmt.Println("Updated Alice's age:", ages["Alice"])

    // 删除 map 中的键值对
    delete(ages, "Bob")
    fmt.Println("ages after deleting Bob:", ages)

    // 遍历 map
    fmt.Println("Scores:")
    for name, score := range scores {
        fmt.Printf("%s: %d\n", name, score)
    }
}

总结

  • 声明和初始化:可以使用 make 函数或直接初始化来创建 map
  • 访问和修改:通过键来访问和修改 map 中的值
  • 检查存在:使用多重赋值来检查键是否存在
  • 删除键值对:使用 delete 函数来删除 map 中的键值对
  • 遍历:使用 for 循环和 range 关键字来遍历 map

结构体

定义

可以用type关键字 + 结构体名 + struct{}来声明一个结构体,如:

type User struct{
    Name string
    Age  int64
}

使用

在编码中使用结构体可以完成组合等功能,在成员字段类型后可以用“指定转化格式时候的字段名,如

type User struct{
    Name string `json:Name`
    Age  int64  `json:Age`
}

流程控制语句

for循环

在Go语言中只存在有for循环一种循环,格式为:

for 初始化语句;条件判定语句;每次循环后执行的语句{
   somecode
}

其中遍历可以使用range关键字:

for i,j := range something{
    somecode
}

其中,i为次数,j为遍历中的值

switch语句

在Go语言中,switch 语句是一种控制结构,用于根据不同的条件执行不同的代码块。switch 语句提供了比多个 if-else 语句更清晰和简洁的语法,特别适用于多个分支的情况

基本语法

switch expression {
case expression1:
    // 代码块1
case expression2:
    // 代码块2
case expression3:
    // 代码块3
default:
    // 默认代码块
}

关键点

  1. 表达式:switch 后面的表达式可以是任何类型,但所有 case 后面的表达式必须与 switch 表达式的类型相同
  2. 匹配:switch 会依次检查每个 case 后面的表达式,如果匹配,则执行相应的代码块
  3. 默认分支:default 分支是可选的,如果没有匹配的 case,则执行 default 分支
  4. 自动打破:与C语言不同,Go语言的 switch 语句在每个 case 之后会自动打破(即不会继续执行下一个 case),除非使用 fallthrough 关键字
  5. 在switch后面可以执行一个表达式,用分号与判定的表达式分开

示例

基本用法

package main

import "fmt"

func main() {
    day := "Monday"

    switch day {
    case "Monday":
        fmt.Println("It's Monday!")
    case "Tuesday":
        fmt.Println("It's Tuesday!")
    case "Wednesday":
        fmt.Println("It's Wednesday!")
    default:
        fmt.Println("It's another day.")
    }
}

输出

It's Monday!

使用 fallthrough

package main

import "fmt"

func main() {
    grade := 85

    switch {
    case grade >= 90:
        fmt.Println("A")
    case grade >= 80:
        fmt.Println("B")
        fallthrough
    case grade >= 70:
        fmt.Println("C")
    default:
        fmt.Println("D")
    }
}

输出

B
C

无表达式的 switch

你也可以使用无表达式的 switch 语句,这种情况下 case 后面的表达式会被直接评估为布尔值

package main

import "fmt"

func main() {
    number := 10

    switch {
    case number > 0:
        fmt.Println("Positive")
    case number < 0:
        fmt.Println("Negative")
    default:
        fmt.Println("Zero")
    }
}

输出

Positive

多个条件

你可以在一个 case 中列出多个条件,使用逗号分隔

package main

import "fmt"

func main() {
    day := "Saturday"

    switch day {
    case "Monday", "Tuesday", "Wednesday", "Thursday", "Friday":
        fmt.Println("It's a weekday.")
    case "Saturday", "Sunday":
        fmt.Println("It's a weekend.")
    default:
        fmt.Println("Invalid day.")
    }
}

输出

It's a weekend.

总结

  • 基本语法:switch 语句根据不同的条件执行不同的代码块
  • 自动打破:每个 case 之后会自动打破,除非使用 fallthrough
  • 无表达式的 switch:可以在 switch 语句中直接使用布尔表达式
  • 多个条件:一个 case 中可以列出多个条件,使用逗号分隔

if语句

当然可以,以下是修改后的版本:


在Go语言中,if 语句是一种常用的控制结构,用于根据条件执行不同的代码块。if 语句的基本语法非常简单,但也有一些高级用法,如在 if 语句中声明变量。

基本语法

if condition {
    // 代码块1
} else if condition2 {
    // 代码块2
} else {
    // 代码块3
}

关键点

  • 条件:if 后面的条件必须是一个布尔表达式。
  • 代码块:如果条件为 true,则执行相应的代码块。
  • else if:可以有多个 else if 分支,用于处理多个条件。
  • elseelse 分支是可选的,如果没有匹配的 ifelse if 条件,则执行 else 分支。

示例

基本用法

package main

import "fmt"

func main() {
    age := 25

    if age >= 18 {
        fmt.Println("You are an adult.")
    } else {
        fmt.Println("You are a minor.")
    }
}

输出:

You are an adult.

多个 else if 分支

package main

import "fmt"

func main() {
    score := 85

    if score >= 90 {
        fmt.Println("Grade: A")
    } else if score >= 80 {
        fmt.Println("Grade: B")
    } else if score >= 70 {
        fmt.Println("Grade: C")
    } else {
        fmt.Println("Grade: D")
    }
}

输出:

Grade: B

if 语句中声明变量

if 语句中,你可以在条件之前声明并初始化一个变量。这个变量的作用域仅限于 if 语句块及其 elseelse if 分支。

package main

import "fmt"

func main() {
    if num := 10; num > 0 {
        fmt.Println("Number is positive.")
    } else {
        fmt.Println("Number is non-positive.")
    }
}

输出:

Number is positive.

使用逻辑运算符

你可以在条件中使用逻辑运算符(如 &&, ||, !)来组合多个条件。

package main

import "fmt"

func main() {
    age := 25
    hasLicense := true

    if age >= 18 && hasLicense {
        fmt.Println("You can drive.")
    } else {
        fmt.Println("You cannot drive.")
    }
}

输出:

You can drive.

嵌套 if 语句

你可以在 if 语句中嵌套另一个 if 语句,以实现更复杂的逻辑。

package main

import "fmt"

func main() {
    age := 25
    hasLicense := true

    if age >= 18 {
        if hasLicense {
            fmt.Println("You can drive.")
        } else {
            fmt.Println("You are old enough to drive, but you don't have a license.")
        }
    } else {
        fmt.Println("You are too young to drive.")
    }
}

输出:

You can drive.

总结

  • 基本语法:if 语句根据条件执行不同的代码块。
  • 多个 else if 分支:可以处理多个条件。
  • else 分支:如果没有匹配的 ifelse if 条件,则执行 else 分支。
  • if 语句中声明变量:可以在条件之前声明并初始化一个变量。
  • 使用逻辑运算符:可以组合多个条件。
  • 嵌套 if 语句:可以实现更复杂的逻辑。

方法

在Go语言中,方法是一种特殊的函数,它与特定的类型关联。方法可以为任何类型(包括内置类型、自定义类型、结构体等)定义行为。方法的定义和调用与普通函数类似,但有一些关键的区别。

方法的基本概念

  1. 接收者:方法的第一个参数称为接收者,它指定了方法作用于哪个类型。
  2. 方法集:一个类型的方法集包括所有为该类型定义的方法。

定义方法

方法的定义语法如下:

func (receiverType receiverName) methodName(parameters) returnType {
    // 方法体
}

其中:

  • receiverType 是接收者的类型。
  • receiverName 是接收者的名称,可以在方法体内使用。
  • methodName 是方法的名称。
  • parameters 是方法的参数列表。
  • returnType 是方法的返回类型。

示例

为结构体定义方法

package main

import "fmt"

// 定义一个结构体
type Person struct {
    Name string
    Age  int
}

// 为 Person 结构体定义一个方法
func (p Person) SayHello() {
    fmt.Printf("Hello, my name is %s and I am %d years old.\n", p.Name, p.Age)
}

func main() {
    // 创建一个 Person 实例
    p := Person{Name: "Alice", Age: 30}

    // 调用 SayHello 方法
    p.SayHello()
}

输出

Hello, my name is Alice and I am 30 years old.

指针接收者 vs 值接收者

方法的接收者可以是值类型或指针类型,这会影响方法的行为。

值接收者

  • 方法接收者是值类型时,方法会接收一个副本。
  • 修改接收者不会影响原始值。
package main

import "fmt"

type Person struct {
    Name string
    Age  int
}

// 值接收者
func (p Person) ChangeName(newName string) {
    p.Name = newName
}

func main() {
    p := Person{Name: "Alice", Age: 30}
    p.ChangeName("Bob")
    fmt.Println(p.Name) // 输出 "Alice"
}

指针接收者

  • 方法接收者是指针类型时,方法会接收一个指针。
  • 修改接收者会影响原始值。
package main

import "fmt"

type Person struct {
    Name string
    Age  int
}

// 指针接收者
func (p *Person) ChangeName(newName string) {
    p.Name = newName
}

func main() {
    p := Person{Name: "Alice", Age: 30}
    p.ChangeName("Bob")
    fmt.Println(p.Name) // 输出 "Bob"
}

方法集

一个类型的方法集包括所有为该类型定义的方法。例如:

package main

import "fmt"

type MyInt int

// 为 MyInt 类型定义一个方法
func (mi MyInt) Double() MyInt {
    return mi * 2
}

func main() {
    var a MyInt = 5
    fmt.Println(a.Double()) // 输出 10
}

接口和方法

方法在实现接口时非常有用。接口定义了一组方法,任何实现了这些方法的类型都可以满足该接口。

package main

import "fmt"

// 定义一个接口
type Greeter interface {
    Greet() string
}

// 定义一个结构体
type Person struct {
    Name string
}

// 为 Person 结构体实现 Greeter 接口
func (p Person) Greet() string {
    return "Hello, my name is " + p.Name
}

func main() {
    p := Person{Name: "Alice"}
    var g Greeter = p
    fmt.Println(g.Greet()) // 输出 "Hello, my name is Alice"
}

总结

  • 方法:为特定类型定义的行为。
  • 接收者:方法的第一个参数,指定了方法作用于哪个类型。
  • 值接收者:方法接收一个副本,修改不会影响原始值。
  • 指针接收者:方法接收一个指针,修改会影响原始值。
  • 方法集:一个类型的所有方法。
  • 接口:定义了一组方法,任何实现了这些方法的类型都可以满足该接口。

Goroutine&Chan

在Go语言中,Goroutine 是轻量级的线程,由Go运行时管理和调度。Goroutine 的设计目的是为了简化并发编程,使开发者能够轻松地编写高效的并发代码。Goroutine 的创建和管理成本非常低,可以同时运行成千上万个Goroutine

Goroutine 的基本概念

  1. 轻量级:Goroutine 的开销远小于操作系统线程,因为它们是由Go运行时管理和调度的。
  2. 并发:Goroutine 之间可以并发执行,充分利用多核处理器的性能。
  3. 调度:Go运行时负责Goroutine的调度,确保它们在可用的处理器核心上高效运行。

创建 Goroutine

要创建一个Goroutine,只需在函数调用前加上 go 关键字。

go functionCall()

示例

基本用法

package main

import (
    "fmt"
    "time"
)

func say(s string) {
    for i := 0; i < 5; i++ {
        time.Sleep(100 * time.Millisecond)
        fmt.Println(s)
    }
}

func main() {
    go say("world") // 创建一个新的Goroutine
    say("hello")    // 在主Goroutine中执行

    // 主Goroutine需要等待一段时间,以便其他Goroutine完成
    time.Sleep(1 * time.Second)
}

输出

hello
world
hello
world
hello
world
hello
world
hello

通道(Channel)

Goroutine 之间的通信通常通过通道(Channel)来实现。通道是一种类型安全的管道,可以在Goroutine之间发送和接收值。

创建通道

ch := make(chan int)

发送和接收

ch <- value // 发送值到通道
value := <-ch // 从通道接收值

示例:使用通道

package main

import (
    "fmt"
    "time"
)

func sum(s []int, c chan int) {
    sum := 0
    for _, v := range s {
        sum += v
    }
    c <- sum // 将结果发送到通道
}

func main() {
    s := []int{7, 2, 8, -9, 4, 0}

    c := make(chan int)
    go sum(s[:len(s)/2], c)
    go sum(s[len(s)/2:], c)

    x, y := <-c, <-c // 从通道接收值

    fmt.Println(x, y, x+y)
}

输出

5 -5 0

通道的方向

通道可以有方向,用于限制通道的操作:

  • chan int:双向通道,可以发送和接收。
  • chan<- int:只写通道,只能发送。
  • <-chan int:只读通道,只能接收。

示例:使用方向通道

package main

import (
    "fmt"
)

func producer(c chan<- int) {
    for i := 0; i < 5; i++ {
        c <- i
    }
    close(c) // 关闭通道,表示没有更多的值发送
}

func consumer(c <-chan int) {
    for v := range c {
        fmt.Println(v)
    }
}

func main() {
    c := make(chan int)
    go producer(c)
    consumer(c)
}

输出

0
1
2
3
4

选择(Select)

select 语句用于在多个通道操作之间进行选择。当多个通道操作都准备好时,select 会随机选择一个执行。

示例:使用 select

package main

import (
    "fmt"
    "time"
)

func main() {
    c1 := make(chan string)
    c2 := make(chan string)

    go func() {
        time.Sleep(time.Second * 1)
        c1 <- "one"
    }()

    go func() {
        time.Sleep(time.Second * 2)
        c2 <- "two"
    }()

    select {
    case msg1 := <-c1:
        fmt.Println(msg1)
    case msg2 := <-c2:
        fmt.Println(msg2)
    }
}

输出

one

总结

  • Goroutine:轻量级的线程,由Go运行时管理和调度。
  • 创建 Goroutine:在函数调用前加上 go 关键字。
  • 通道:用于Goroutine之间的通信。
  • 方向通道:限制通道的操作。
  • 选择select 语句用于在多个通道操作之间进行选择。
上一篇
下一篇