[Golang] 万字长文,一文入门go语言!

930 阅读23分钟

前言

Go语言以其简洁的语法,“少即是多”的设计哲学,和原生支持高并发的特点,已经成为云原生和后端高并发应用的热门选择。

本人作为一名从Java转向Go的开发者,在初学Go经常会困扰于语法,遂整理该文帮助广大开发者快速入门Go语言。

基础入门

关键字一览

关键字作用描述
break跳出当前循环(for)、switch 或 select 结构,可配合标签跳出多层结构。
case用于 switch 或 select 中,定义分支条件,匹配对应的值或通道操作。
chan声明通道(channel)类型
const声明常量,支持批量声明。
continue跳过本次迭代的剩余代码,直接进入下一次迭代,支持标签跳转。
default用于 switch 或 select 中,定义默认分支(当其他所有 case 不匹配时执行)。
defer声明延迟执行的函数,在当前函数返回前(无论正常返回还是 panic)执行,常用于资源清理。
else与 if 配合使用,定义条件不满足时的分支代码块。
fallthrough仅用于 switch 中,强制执行下一个 case
for唯一的循环语句
func声明函数或方法,定义可执行的代码块,支持多返回值、匿名函数等。
go用于启动 goroutine,将函数调用放入后台并发执行。
goto跳转到函数内的标签位置,需谨慎使用
if定义条件判断语句
import导入其他包
interface声明接口类型
map声明映射(map)类型
package声明当前文件所属的包
range配合 for 使用,用于遍历切片、映射、字符串、数组、通道等,返回索引 / 键和对应的值。
recover仅在 defer 中使用,用于捕获 panic 触发的异常,阻止程序崩溃并返回错误值。
return用于函数中,终止当前函数执行并返回指定结果(无返回值时可省略)。
select用于监听多个通道的操作(发送或接收),当其中一个通道可操作时执行对应分支。
struct声明结构体类型
switch多分支条件匹配语句
type定义类型别名(如 type MyInt int)或自定义类型(如结构体、接口等)。
var声明变量,未初始化变量会被赋予零值。

基本数据类型

类型类别具体类型描述
布尔类型bool表示逻辑真假
整数类型(有符号)int与系统位数一致
int88 位有符号整数
int1616 位有符号整数
int32/rune32 位有符号整数,rune用于表示 Unicode 码点(支持中文、 emoji 等)
int6464 位有符号整数
整数类型(无符号)uint与系统位数一致
uint8/byte8 位无符号整数,byte用于表示 ASCII 字符
uint1616 位无符号整数
uint3232 位无符号整数
uint6464 位无符号整数
uintptr用于存储指针地址的无符号整数(位数与指针一致)
浮点数类型float3232 位单精度浮点数
float6464 位双精度浮点数
字符串类型string表示字符串
复数类型complex64实部(float32) + 虚部(float32)
complex128实部(float64) + 虚部(float64)

复合数据类型

类型类别结构描述
数组[<数组长度>]<元素类型>如:[5]int 为存储5个int类型元素的数组
切片[]<元素类型>如:[]int 为存储int类型元素的切片,长度由编译器动态扩充
映射map[<key类型>]<value类型>如:map[string]int 为存储以string为键,以int为值的映射集合
结构体自定义结构字段只定义属性字段,不在声明中囊括方法
函数自定义函数签名将指定签名的函数作为一种类型
接口自定义函数签名的集合定义一组函数但不实现
通道chan <元素类型>如:chan int 为传输int类型元素的通道

函数作为类型是Go中的特色,各类型会在后文中详细讲解。

模块与包

Go语言使用模块(module)与包(package)来组织项目的架构。

通常一个项目就是一个模块,模块下面可以有很多的包,包下可以有很多的go文件,如图所示:

go语言项目架构.png

包的声明与导入

package关键字用于包的声明

package main // 声明当前go文件属于main包下

import关键字用于包的导入。批量导入多个包时需以括号包裹。

导入包必须至少使用一次,否则编译报错

improt (
    "fmt" // 内置包,常用于字符串的格式化输出,是format的缩写
    "net/http" // net是模块名,http是包名,以/分隔
)

主程序入口

Go使用main包下的main函数作为程序的入口,main函数无参无返回值

package main

func main(){
    // 你的代码
}

变量的声明与赋值

基本类型

Go语言中,变量名在前,变量类型在后。

声明的变量必须使用至少1次,否则编译报错。

1. 先声明后赋值
  • 单变量
var a int
a = 5
  • 多变量
var a, b, c int //均为int类型
a, b, c = 10, 20, 30
2. 声明+赋值
  • 显式指定类型
var a int = 5
const PI float64 = 3.14159
  • 隐式推导类型
var height = 1.73 // go编译器会根据等号右边的值推断左边的变量类型
var isOk = true
const PI = 3.14159
  • 短变量声明

Go中提供了特殊的语法糖 ":=" 以简化声明与赋值,但只能在函数内部使用

a := 5 // 声明变量a,类型由编译器推导,并赋值为5
  • 批量声明与赋值
var (
    id int = 23 //显式指定类型
    name = "zhangsan" // 隐式类型推导
    weight = 57.68
)

复合类型

本部分只涉及数组切片映射通道的初步构建与使用,更多细节及结构体函数接口会写在后续对应章节中。

1. 数组

数组为值类型,存储的是值本身

先声明后赋值

var array [3]int // 初始化array为{0, 0, 0}
array = [3]int{1, 2} // 将array赋值为{1, 2, 0}

声明+赋值

// 显式指定类型
var array1 [3]int = [3]int{1, 2, 3}

// 隐式类型推导
var array2 = [3]int{1, 2}

// 短变量声明(语法糖)
array3 := [3]int{1, 2, 3}
2. 切片

切片(slice)是对数组的上层抽象,能够根据元素的格式进行动态扩容,属于引用类型,变量名存储内存地址。这里引入两个名词:长度、容量。

长度表示底层数组实际存储的元素个数。

容量表示初始化时切片时为底层数组分配的初始空间。当长度大于容量时,Go编译器会重新分配更大容量的数组,通常是双倍扩容,并将原数组中的元素拷贝至新的扩容数组。

image.png

先声明后赋值

var slice1 []int // 初始化slice1为nil(零值)
slice1 = []int{2, 3, 4}
slice1[2] = 10 // 此时slice1变为了{2, 3, 10}

var slice2 []float64
slice = []float64{3.28}

声明+赋值

// 显式指定类型
var slice3 []int = []int{1, 2, 3}

// 隐式类型推导
var slice4 = []int{1, 2, 3}

// 语法糖:=
slice5 := []int{4, 5, 6}

使用make函数初始化

make是Go中内置的函数,用于初始化切片、映射和通道类型的变量,对于切片来说具体的参数定义为make([]T, len, cap),其中cap参数为可选参数,默认与len相等。

这种初始化方式也是官方所推荐的。

slice6 := make([]bool, 2, 5) // slice6被初始化为{false, false, _, _, _}
slice6[1] = true // 此时变为{false, true, _, _, _}
slice6 = []bool{true, true} // 一次性赋多个值也是ok的
3. 映射

map同样具有容量的概念,但是不需要初始长度,其长度会随着向map添加KV对而动态变化。

对于map而言,零值KV对的初始化是无意义的,也正因如此,在对未初始化的map进行赋值会触发panicpanic 是一种运行时错误处理机制,用于表示程序遇到了无法正常恢复的严重错误。

先声明后赋值

var m map[string]string
m = map[string]string{"张三": "10220308"} // 将m初始化为{"张三": "10220308"} 

如下示例会触发panic

var m map[string]string // 只声明,未初始化
m["张三"] = "10220308" // 尝试为m赋值, 触发panic

声明+赋值

// 显式指定类型
var m1 map[string]string = map[string]string{"zhangsan": "10220308"}

// 隐式类型推导
var m2 = map[string]string{"zhangsan": "10220308", "lisi": "10220309"}

使用make函数初始化

对于map类型,make的签名为:make(map[key类型]value类型, 初始容量),初始容量是一个可选参数,若不传该参数,编译器会使用默认容量进行初始化。

默认容量随着Go版本的更新会发生变化,作为使用者,我们无需关心其具体值。

m3 := make(map[string]string, 5) // 初始化容量为5的map容器
m3["zhangsan"] = "10220308" // 将"zhangsan": "10220308"添加到容器中
4. 通道

通道(channel)是Go中支持高并发的核心类型,以chan关键字表示。

依据数据的流向分为三类:

1.双向通道
语法结构为: chan 元素类型
2.只读通道
语法结构为: <-chan 元素类型
3.只写通道
语法结构为: chan<- 元素类型

需要注意的是,权限扩大是被禁止的,例如:将一个只读通道赋值给双向通道

使用make函数初始化

注意,通道类型只支持使用make函数初始化

make函数的签名为:make(chan 元素类型, [缓冲区大小])

// 初始化一个元素类型为int,缓冲区大小为5的双向通道并赋值给变量ch1
ch1 := make(chan int, 3) 

向通道写入值

// 写入的值会被存入缓冲区中
ch1 <- 10
ch1 <- 20

从通道读取值

// 从缓冲区中读取值并赋值给变量a, b
a := <-ch1
b := <-ch1

流程控制

值得一提的是,在Go中,breakcontinue关键字均支持标签跳转,意味着可以一次性跳出多层循环,但这种做法与goto关键字一样,会破坏代码的结构性,存在着争议。

if ... else条件判断

  • 条件无需括号包裹,代码块必须以{ }包裹,即使只有一条语句
  • if条件前可执行变量的初始化,作用域仅限if...else...
if flag := true; flag {
    fmt.Println("if 块已执行") // 在控制台打印 "if 块已执行" 
}else {
    fmt.Println("显然这句话不会被打印")
}

for 循环

Go语言中, 关键字for是唯一能定义循环的,没有其他语言中的while,do-while等,但却可以实现其功能。

  • 标准for循环
for i:=0; i<10; i++ {
    // 你的代码
}
  • while循环
flag := true

// flag条件可省略,省略后为死循环,需配合break使用
for flag {
    // 你的代码
}
  • for range遍历
    用于迭代切片、映射、通道等类型的变量,以切片为例
slice := []int{1, 2, 3, 4, 5}

// 当传入参数为切片类型时,range函数会返回每次迭代的值和索引供后续逻辑使用
// 若无需使用索引,可用"_"作为占位符
// 使用索引和值:
for index, value := range(slice) {
    // 你的代码
}

// 使用值:
for _, value := range(slice) {
    // 你的代码
}

// 使用索引:
for index := range(slice) {
    // 你的代码
}

switch匹配

  • 表达式支持任意类型,甚至可以省略
// 条件前初始化,分号后省略条件,相当于多个if...else...
switch score := 89; {
    case score >= 90:
        fmt.Println("优秀")
    case score >= 60:
        fmt.Println("及格")
    default:
        fmt.Println("不及格")
}
  • 自动break,每个case执行完自动跳出。若需执行下一个case,需使用fallthrough强制执行。
switch x := 2; x {
    case 1:
        fmt.Println(1)
    case 2:
        fmt.Println(2)
        fallthrough // 强制执行下一个case
    case 3:
        fmt.Println(3) // 会被执行
}
  • 支持多值匹配,使用","分隔
switch day := "wed"; day {
    case "mon", "tue", "wed", "thu", "fri":
        fmt.Println("工作日")
    case "sat", "sun":
        fmt.Println("周末")
}

goto跳转

  • 标签必须在当前函数内
// 错误示例
func f1() {
    label: // 标签在f1中
        fmt.Println("f1")
}

func main() {
    goto label // 错误:label不在main函数内
}
  • 不能跳过变量声明
// 错误示例
func main() {
    goto skip // 尝试跳转到变量x声明之后
    
    // 变量声明(被跳过)
    x := 10
    
skip:
    fmt.Println(x) // 错误:x在跳转后未声明就被使用
}
  • 标签不能重复定义

函数

Go语言中,函数的作用域由函数名称的首字母决定。

大写字母表示该函数包外可访问,小写字母则说明该函数只能在包内访问。

此外,多返回值函数作为类型也是Go的语法特色。

定义

语法结构:func 函数名称(参数列表)(返回值列表),通常我们也把函数的参数类型列表 + 返回值类型列表统称为函数的签名

// b, c两个变量简写了类型,均为int
// 首字母大写,允许包外访问
func FirstFunction(a string, b, c int) (int, float64){
    // 你的代码
    
    // 返回值需和签名中定义的类型、数量保持一致
    return 3, 6.48
}

返回值

单返回值

若某个函数只有一个返回值,则括号可以省略

// 省略返回值列表括号
// 首字母小写,仅限包内访问
func secondFunction(d bool) bool{
    // 你的代码
    return true
}

多返回值

Go语言中,多返回值函数是非常常见的。

此外,还可以给返回值命名并直接在函数中使用,在return时省略返回对象。

举个栗子,执行完某个函数后将计算结果和错误信息一并返回。

func returnWithMultipleResults(a, b float64) (result float64, msg string){
    // 给返回值赋予默认值,未赋予默认值的返回值会默认返回对应类型的零值
    result = 0.0
    if b == 0.0 {
        msg = "除数不能为0"
        return
    }
    result = a / b
    // 省略返回对象result和msg
    return
}

可变参数

可变参数是函数中的特殊语法,允许函数接收任意数量(零个或多个)的同类型参数。

在函数内部,可变参数被视为同类型的切片。

定义

在参数的类型前加上...标识该参数为可变参数。

  • 每个函数仅能接收至多一个可变参数,并且应在参数列表的末尾
  • 必须是同类型的参数
func Add(params ...int)int{
    result := 0
    for _, param := range params{
        result += param
    }
    return result
}

调用

含有可变参数的函数需传入零个或多个同类型的参数

package main

func main(){
    result := Add(1, 2, 3, 4, 5) // result = 15
}

若我们希望传入同类型的切片,Go也提供了相应的语法糖:使用...将切片展开。

package main

func main(){
    params := []int{1, 2, 3, 4, 5} // 初始化params为int类型的切片
    result := Add(params...) // 使用...将切片元素展开,相当于Add(1, 2, 3, 4, 5)
}

特殊函数

匿名函数

对于只使用1次而无需复用的函数,可以省略函数名称并直接调用。

请注意,匿名函数可以在函数内定义

// 定义一个无参无返回值的匿名函数并调用
func main(){
    func (){
        fmt.Println("这是一个匿名函数") // 在控制台打印 "这是一个匿名函数"
    }() // 使用()进行函数的调用
}

闭包函数

引用了外部函数变量的函数称为闭包函数。 需要注意的是:

  • 闭包捕获的外部变量是引用,而非副本,意味着闭包和外部函数对被捕获变量的操作是相互可见的
  • 被捕获变量的生命周期会被延长

如下示例:

func outer() func() int {
    count := 0 // 外部函数定义的局部变量
    return func() int {    // 内部函数(闭包):引用了外部的count
        count++ // 操作捕获的变量
        return count
    }
}// 这一行本应是count的生命尽头,但由于闭包的存在,count得以续命

高阶函数

Go同样支持函数式编程,这意味着函数可以作为参数被传递、作为返回值被返回,给予开发者很高的灵活性。

// f1接收一个签名为func(a, b int) int类型的函数,命名为f
// 在内部将20,10作为参数传递给f,并调用
// 将f返回的结果赋值给result并返回
func f1(f func(a, b int) int) int {
    c, d := 20, 10
    result := f(c, d)
    return result
}

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

func sub(a, b int) int{
    return a - b
}

func main(){
    // f1可以根据传入的函数不同而执行不同的逻辑
    result1 := f1(add) // result1=30
    result2 := f1(sub)// result2=10
    result3 := f1(func(c, d int)int{
        return c * d
    })// result3 = 200
}

延迟函数

延迟函数也是Go中的一大特色,常用于简化资源管理和确保清理操作。

语法结构为:defer 函数调用

在C/C++语言中,经常需要在函数的末尾释放申请的内存;在Java语言中,也需要在合适的地方手动释放锁。

而在Go语言中,可以在相关操作的下一行直接释放资源(例如释放锁),只需在函数调用前加上关键字defer

延迟函数会在其所在方法、函数执行完毕后才执行,并且一定会执行,即使该方法、函数报错或出现panic

若存在多个延迟函数,则会依照后进先出的顺序执行

值得注意的是,延迟函数的参数会在defer声明时立即求值,而非在执行时求值

func main() {
    // 调用os包下的公开函数Open,并将返回值依次赋值给file, err
    // file, err 是自定义的结构体类型,这里无需过多关心
    file, err := os.Open("test.txt")
    
    // Close函数会在main函数执行完后执行
    defer file.Close()
    
    if err != nil {
        fmt.Println("打开文件失败:", err)
        return
    }
    
    // 读取文件内容(示例操作)
    fmt.Println("文件已打开,准备读取...")
}

函数作为类型

不同于其他语言,在Go语言中,函数是一等公民,可以像int、float64等基本类型一样被赋值和传递。

定义

语法结构为:type 类型名称 函数签名

// 将接受两个int类型的参数,返回值为int类型的函数定义为MathFunc类型
type MathFunc func(int, int) int 

声明与赋值

// 同类型参数类型简写
func Add(a, b int) int{
    return a + b
}

func Sub(a, b int) int{
    return a - b
}

func main(){
    var f MathFunc // 声明一个类型为MathFunc的变量f
    f = Add // 将f赋值为Add函数
    result1 := f(2, 3) // 调用f,并将返回值5赋值给result1变量,result1=5
    
    f = Sub
    result2 := f(12, 5)// result=7
}

结构体与方法

基于面向对象的思想,Go也提供了类似C/C++语言的实现方式,即结构体类型。

Java所不一样的是,Go中的结构体只定义属性,而非像Java一样,将属性和行为(方法)全部封装在一起。

定义

// 只定义属性
type Person struct{
    name string
    age int
    height float64
    weight float64
    hobby []string
}

声明与赋值

值类型

var person Person // 值类型,在声明之初,已经完成内存的分配,并将各字段赋值为对应类型的零值
person.height = 1.73 // 此处将height赋值为1.73

// 根据字面量为对应的字段赋值
// 由于未指定height的值,又将height赋值为float64类型的零值了
person = Person{ 
    name: "张三",
    age: 18,
    hobby: []string{"羽毛球", "乒乓球"}, // 末尾行的","不能省
}
// 

指针类型

var person2 *Person // 声明为指针,初始值是nil
person2 = &Person{ // 取完成初始化的内存地址,赋值给变量person2
    name: "李四",
    age: 20,
}

注意,在未初始化前赋值会在运行时报错,本质上该操作在尝试为nil赋值,如下示例:

var person2 *Person // 声明为指针,初始值是nil
person2.name = "李四" // 这里报错

使用new函数获取指针实例

new是一个内置函数,会返回初始化后的结构体指针

语法结构:new(结构体名)

// person3的类型为 *Person,所有字段初始化为零值
person3 := new(Person)

组合

Go中没有继承的概念,通过结构体的嵌套(组合)实现代码的复用

命名嵌套

type Address struct{
    Province string
    City string
}

// Person在外层,Address在内层
type Person struct{
    name string
    age int
    height float64
    weight float64
    hobby []string
    Addr Address // 嵌套Address结构体并命名为Addr
}

func main(){
    p := new(Person)
    name := p.name // 访问外层字段
    city := p.Addr.City // 访问内层字段
}

匿名嵌套

  • 字段名默认与类型名相同
  • 内层字段和方法会被"提升"到外层,可直接访问
type Address struct{
    Province string
    City string
}

// Person在外层,Address在内层
type Person struct{
    name string
    age int
    height float64
    weight float64
    hobby []string
    Address // 嵌套Address结构体,字段名默认为Address
}

func main(){
    p := new(Person)
    name := p.name // 访问外层字段
    city := p.City // 访问内层字段
}

接收者

有了属性后,我们已经能够通过Person类型去描述一个人,但我们只知道他的信息,却无法定义他的行为,即能够做什么。

Go语言通过接收者将类型与函数相关联,关联到特定类型的函数称为方法

对于结构体而言,方法可以操作结构体的字段,定义其行为。

定义

语法结构:func (接收者名称 接收者类型) 方法名(参数列表)(返回值列表)

func (p Person) Eat(){
    fmt.Println("干饭中。。。")
}

值接收者

值接受者传入的是调用实例的副本,适用于只读取信息的场景

func main(){
    person := new(Person)
    person.Eat() // 使用person实例调用Eat方法,方法内可通过接收者名称p访问实例副本
}

指针接收者

传入实例的指针,可修改原实例

func (per *Person) SetName(name string){
    per.name = name
}

func main(){
    person := new(Person)
    name := "孙六"
    person.SetName(name) // 使用person实例调用SetName方法,将person实例的字段name赋值为孙六
}

结构体标签

结构体标签是定义时以反引号 `` 包裹,写在字段类型后的键值对。

常用于传递元数据,本身不影响结构体的内存分配,也不可直接访问,通常是框架/库约定的标识符,如encoding/json包下的json

使用结构体标签的字段需以大写字母开头提供包外访问的权限

type User struct {
    Name string `json:"username"` // 定义序列化后的字段名为username
    Age  int    `json:"age"`
}

接口

接口本质是一组函数定义的集合,任何实现了接口中所有函数的类型都可以作为值赋给该接口类型的变量。

定义

type Calculate interface{
    Add(a, b int) int // 加法
    Sub(a, b int) int // 减法
    Mult(a, b int) int // 乘法
    Div(a, b int) float64 // 除法
}

组合

与结构体类似,接口也支持组合,但只支持匿名嵌套。

外层接口会包含所有内层接口定义的方法。

外层接口的实现类型必须实现包括内层接口定义函数的所有函数。

// 读接口
type Reader interface {
    Read() string
}

// 写接口
type Writer interface {
    Write(content string)
}

// 组合Reader和Writer,形成读写接口
type ReadWriter interface {
    Reader // 嵌入Reader接口
    Writer // 嵌入Writer接口
}

实现

若一个结构体类型实现了接口中定义的所有函数,那么该结构体就成为该接口的实现类型。

接口类型的变量,可以存储其实现类型的值。特别的,所有结构体类型都实现了空接口,所以空接口可以存储任何类型的值

一个接口可以存在多个实现类型,一个结构体也可以实现多个接口。

接收者绑定的方式为接口的实现提供了相当高的灵活性。

// 接口定义
type Calculate interface{
    Add(a, b int) int // 加法
    Sub(a, b int) int // 减法
    Mult(a, b int) int // 乘法
    Div(a, b float64) (float64, string) // 除法
}

//结构体定义
type MathUtil struct{}

func (m MathUtil) Add(a, b int) int{
    return a + b
}

func (m MathUtil) Sub(a, b int) int{
    return a - b
}

func (m MathUtil) Mult(a, b int) int{
    return a * b
}

func (m MathUtil) Div(a, b float64) (float64, string){
    if b == 0 {
        return 0.0, "除数不能为0"
    }
    return a / b, ""
}

func main() {
    m := new(MathUtil)    // 初始化结构体实例
    result := m.Add(6, 7) // 调用Add方法

    // 声明接口实例
    var i Calculate
    i = MathUtil{} // 将其实现类型赋值给接口
    result2 := i.Sub(8, 3) // 使用接口调用
}

类型断言

此时我们产生了疑问,如果一个接口有很多实现类型,那么我们怎么知道该接口变量存储的是哪种类型的值呢?

Go提供了一种语法,能够获取接口的底层类型,即类型断言。

// ok 为true,value为类型的值
// ok 为false,value为零值,断言失败
value, ok := 接口变量.(具体类型)

协程与多路监听

由于本篇为入门篇,这里只讲述基本用法。

协程

Go语言中,协程通常指goroutine。一种轻量级线程(初始栈大小为2KB),由Go运行时管理。

开启一个goroutine也非常简单,只需在函数调用前加上go关键字。

func save(user User){
    // 将user存入数据库(耗时操作)
}

// main函数也运行在一个goroutine中,通常称为主goroutine
func main(){
    // 省略获取user代码
    
    // 开启另一个goroutine异步执行save操作
    go save(user)
    return
}

多路监听

想象一下以下场景:现在有多个通道,当某个通道执行完操作后需要执行一段代码逻辑。

很容易想到的是,可以开启多个for循环,对通道的状态进行持续判断,一旦通道变为期望的状态,结束循环并执行相应代码。

但是,当通道的数量非常多时,这样处理未免麻烦。

Go语言就提供了select关键字以简化处理。

// 模拟任务1:1秒后返回结果
func task1(ch chan<- string) {
    // 调用time包下的公开函数Sleep,将当前goroutine阻塞
    time.Sleep(1 * time.Second)
    ch <- "任务1完成"
}

// 模拟任务2:500毫秒后返回结果
func task2(ch chan<- string) {
    time.Sleep(500 * time.Millisecond)
    ch <- "任务2完成"
}

func main() {
    ch1 := make(chan string)
    ch2 := make(chan string)

    // 开启两个goroutine异步执行任务
    go task1(ch1)
    go task2(ch2)

    // 用select等待两个channel,谁先就绪就处理谁
    select {
    case res := <-ch1:
        fmt.Println("收到:", res)
    case res := <-ch2:
        fmt.Println("收到:", res)
    }
    // 输出:收到:任务2完成(因为task2更快)
}

写在最后

作者水平有限,如有错误,敬请指正

倘若该文章有帮助到你,点一点赞和关注🤗