目录
安装开发环境
ubuntu
使用命令行安装即可
sudo apt install golang
安装后运行
go version
如果显示出版本,表示安装成功:
编译与运行
在 Go 语言编译和运行程序的过程非常简单:
编译 Go 程序
简单编译(生成可执行文件)
go build main.go
编译整个包
go build # 在包含main函数的包目录下执行,自动编译所有依赖
指定输出文件名
go build -o myapp main.go # 输出文件名为myapp(或myapp.exe)
运行 Go 程序
直接编译并运行
go run main.go # 直接编译并运行,无需生成中间文件
编译后运行
go build main.go #编译
./main #运行
基础语法
基础格式
go语言中对于花括号的缩进有强制要求:左花括号 { 不能单独成行。
错误格式:
if condition
{ // 左花括号单独成行
// 代码块
}
正确格式:
if condition { // 左花括号与if语句同行
// 代码块
}
Go 语言每行语句后需要以 ; 结尾,但可以不加。
Go 编译器会在一行的末尾,如果该行包含一个完整的语句,并且这个语句没有显式地以 { 或 ( 开头,也没有以 ; 结尾时,自动插入分号。
变量声明
标准声明
使用var关键字声明变量,格式为var 变量名 类型 = 初始值,其中初始值可以省略,此时变量会被赋予零值。
package main
import "fmt"
func main(){
var a int =10
var b rune = 'c'
var c int
fmt.Printf("a=%d\n",a)
fmt.Printf("b=%c\n",b)
fmt.Printf("c=%d\n",c)
}
运行结果:
短变量声明(:=)
在函数内部,可以使用:=
快捷声明并初始化变量,这种方式会自动推断变量类型。这种方法全局变量不可用。
package main
import "fmt"
func main(){
a:=10
b:='c'
c:=1
fmt.Printf("a=%d\n",a)
fmt.Printf("b=%c\n",b)
fmt.Printf("c=%d\n",c)
}
运行结果:
多变量声明
可以同时声明多个变量。
package main
import "fmt"
func main(){
// 标准方式
var (
x int
y string
z bool
)
// 初始化多个变量
var a, b, c = 1, "hello", false
// 短变量方式(函数内)
d, e := 20, "world"
}
常量与枚举
go中的常量与其他语言中常量相同,定义如下:
package main
import "fmt"
func main(){
const a=10
fmt.Printf("a=%d\n",a)
}
连续定义的语法:
const (
a = 1 // 无类型整数常量
b = 2.0 // 无类型浮点数常量
c = "abc" // 无类型字符串常量
)
通过 const 连续定义,可以定义枚举类型,同时我们可以通过 iota 生成值。 itoa 是常量生成器,从 0 开始,每行自增 1,常用于生成枚举值。
示例:
package main
import "fmt"
const (
Sunday = iota // 0
Monday // 1
Tuesday // 2
Wednesday // 3
Thursday // 4
Friday // 5
Saturday // 6
)
func main(){
fmt.Println("Sunday:", Sunday)
fmt.Println("Monday:", Monday)
fmt.Println("Tuesday:", Tuesday)
fmt.Println("Wednesday:", Wednesday)
fmt.Println("Thursday:", Thursday)
fmt.Println("Friday:", Friday)
fmt.Println("Saturday:", Saturday)
}
运行结果:
函数
函数定义的语法如下:
//基本格式:func 函数名(参数列表) 返回值类型 { 函数体 }
func add(a int, b int) int {
return a + b
}
// 调用函数
result := add(3, 5) // result = 8
go 中的函数是支持多返回值,只用修改返回值类型:
package main
import "fmt"
func add(a int,b int) (int,int) {
return a+b,0
}
func main(){
a,b:=add(1,1)
fmt.Println("a=",a)
fmt.Println("b=",b)
}
运行结果:
可在返回值列表中命名结果变量,函数内直接使用并隐式返回:
package main
import "fmt"
func calculate(a, b int) (sum int, diff int) {
sum = a + b // 直接使用命名变量
diff = a - b
return // 无需显式指定返回值
}
func main(){
a,b:=calculate(1,1)
fmt.Println("a=",a)
fmt.Println("b=",b)
}
go中有一个特殊的函数 init,init函数用于在程序运行时进行初始化操作,比如初始化包级别的变量、设置默认值、建立数据库连接、读取配置文件等。它不需要被显式调用,Go 运行时会自动执行。
执行顺序:
- 对于一个包,如果存在多个 init 函数,它们的执行顺序是按照在源文件中出现的顺序依次执行的。
- 当一个程序包含多个包时,包的初始化顺序是按照包导入的依赖关系决定的。先初始化被导入的包,且每个包只会被初始化一次。如果包 A 导入了包 B,包 B 又导入了包 C,那么初始化顺序是包 C、包 B、包 A。
defer
defer 是一个非常有用且独特的关键字,用于延迟执行函数调用。
被 defer 的函数会在包含该 defer 语句的函数执行完毕后被调用。
import "fmt"
func main() {
defer fmt.Println("world")
fmt.Println("hello")
}
运行结果:
如果一个函数中有多个defer语句,它们会以LIFO(后进先出)的顺序执行。
package main
import "fmt"
func main() {
defer fmt.Println("A")
defer fmt.Println("B")
defer fmt.Println("C")
fmt.Println("hello")
}
运行结果:
slice
slice是go中的动态数组类型,
数组(Array):
- 长度固定,声明时必须指定大小(如 [5]int)。
- 是值类型,赋值或传递时会复制整个数组。
切片(Slice):
- 长度动态可变,可以按需扩容。
- 是引用类型,底层指向一个数组,赋值或传递时只复制切片头(不复制数据)。
切片本质是一个结构体,包含三个字段:
- 指针:指向底层数组的起始位置。
- 长度:当前切片的元素个数。
- 容量:底层数组从起始位置到末尾的元素个数。
切片的创建主要有以下几种
从数组或切片派生
示例代码:
package main
import "fmt"
func main() {
var a [4]int
a[0] = 1
a[1] = 2
a[2] = 3
a[3] = 4
s1 := a[1:4]
s2 := a[:2]
fmt.Println(s1)
fmt.Println(s2)
}
运行结果:
这时s1,s2与a指向的是同一块内存,我们简单演示一下:
package main
import "fmt"
func main() {
var a [4]int
a[0] = 1
a[1] = 2
a[2] = 3
a[3] = 4
s1 := a[1:4]
s2 := a[:2]
a[1] = 5
fmt.Println(s1)
fmt.Println(s2)
}
运行结果:
使用make创建
通过 make 函数创建指定长度和容量的切片,底层会自动分配数组。
语法:slice := make([]T, length, capacity)
示例:
func main() {
s:=make([]int, 3)
s[0]=0
s[1]=1
s[2]=2
fmt.Println(s)
}
运行结果:
直接定义切片
可以直接使用字面量定义切片,不指定字面量时为空切片。
func main() {
//字面量定义切片
s1:=[]int{1,2,3,4}
//定义空切片
var s2 []int
fmt.Println(s1)
fmt.Println(s2)
}
运行结果:
常用函数
append 添加元素
- 作用:向切片末尾添加元素,必要时触发扩容。
- 语法:newSlice := append(slice, elements...)
- 特性:若容量不足,会创建新的底层数组并复制数据。返回值必须被接收,否则修改无效。
s := []int{1, 2}
s = append(s, 3) // 添加单个元素
s = append(s, 4, 5) // 添加多个元素
s = append(s, []int{6, 7}...) // 添加另一个切片的所有元素
fmt.Println(s) // 输出: [1 2 3 4 5 6 7]
copy 复制元素
- 作用:将源切片的数据复制到目标切片,返回实际复制的元素数量。
- 语法:n := copy(dst, src)
- 特性:复制长度为 min(len(dst), len(src))。用于创建独立的切片副本。
src := []int{10, 20, 30}
dst := make([]int, 2) // 创建长度为 2 的目标切片
n := copy(dst, src) // 复制 src 到 dst
fmt.Println(dst) // 输出: [10 20]
fmt.Println(n) // 输出: 2(实际复制的元素数)
len获取长度
- 作用:返回切片的当前元素个数。
- 语法:length := len(slice)
s := []int{1, 2, 3, 4}
fmt.Println(len(s)) // 输出: 4
cap 获取容量
- 作用:返回切片底层数组的最大容量。
- 语法:capacity := cap(slice)
s := make([]int, 2, 5) // 长度 2,容量 5
fmt.Println(cap(s)) // 输出: 5
delete 删除元素(切片需配合 append)
作用:删除切片指定位置的元素。
语法:slice = append(slice[:i], slice[i+1:]...)
特性:删除中间元素会导致后续元素前移。可能触发内存复制,性能开销较大。
s := []int{10, 20, 30, 40}
i := 2 // 删除索引 2 的元素(值为 30)
s = append(s[:i], s[i+1:]...)
fmt.Println(s) // 输出: [10 20 40]
6. slicing 切片截取
- 作用:创建原切片的子切片。
- 语法:subSlice := slice[low:high]
- 特性:low:起始索引(包含),默认为 0。high:结束索引(不包含),默认为 len(slice)。子切片与原切片共享底层数组。
s := []int{1, 2, 3, 4, 5}
sub := s[1:3] // 截取索引 1 到 2(不包含 3)
fmt.Println(sub) // 输出: [2 3]
sort 排序(标准库)
- 作用:对切片元素进行排序。
- 语法:sort.Ints(slice) 或自定义排序函数。
- 依赖:import "sort"
s := []int{3, 1, 4}
sort.Ints(s) // 升序排序
fmt.Println(s) // 输出: [1 3 4]
8. range - 遍历切片
- 作用:迭代切片的元素和索引。
- 语法:for index, value := range slice { ... }
s := []string{"a", "b", "c"}
for i, v := range s {
fmt.Printf("索引 %d: 值 %s\n", i, v)
}
// 输出:
// 索引 0: 值 a
// 索引 1: 值 b
// 索引 2: 值 c
map
map 是go内置的数据类型,用于存储键值对的无序集合,类似于 cpp 中的 unordered_map。
声明与初始化
声明的语法
var m map[string]int // 声明一个键为 string,值为 int 的 map
注意:未初始化的 map 值为 nil,不能直接存储键值对。
初始化可以通过 make与或使用字面量
//使用make
m := make(map[string]int)
//使用字面量
m := map[string]int{
"apple": 1,
"banana": 2,
} // 创建并初始化 map
增删查改
使用 map[key] = value 语法添加键值对。若键不存在,则新增;若键已存在,则覆盖旧值。
m := make(map[string]int)
// 添加单个元素
m["apple"] = 10
m["banana"] = 20
// 批量添加(初始化时)
m = map[string]int{
"cherry": 30,
"date": 40,
}
使用 delete(map, key) 函数删除指定键。即使键不存在,delete 也不会报错。
// 删除存在的键
delete(m, "apple")
// 删除不存在的键(安全操作)
delete(m, "nonexistent")
直接获取值
// 若键存在,返回对应值;若不存在,返回值类型的零值(如 int 的零值是 0)
value := m["apple"]
判断键是否存在
value, exists := m["apple"]
if exists {
fmt.Println("键存在,值为:", value)
} else {
fmt.Println("键不存在")
}
修改元素(改)直接通过键赋值即可修改已有键的值。若键不存在,则变为新增操作。
// 修改已有键的值
m["apple"] = 100 // 若 "apple" 已存在,则更新为 100
m["apple"] = 200 // 再次覆盖为 200
// 新增与修改的统一语法
m["newKey"] = 50 // 若 "newKey" 不存在,则新增
面向对象
go中没有类的对象,go的面向对象是通过结构体、接口、方法实现的。
结构体
结构体用于将不同类型的字段组合成一个复合数据结构。它类似于其他语言中的类(Class)
定义结构体
使用 type 和 struct 关键字定义:
type Person struct {
Name string
Age int
Address struct { // 嵌套结构体
City string
Country string
}
}
初始化结构体
// 方式 1:按字段顺序初始化
p1 := Person{
"Alice",
30,
Address{City: "Beijing", Country: "China"},
}
// 方式 2:指定字段名初始化(推荐)
p2 := Person{
Name: "Bob",
Age: 25,
Address: struct { // 匿名结构体初始化
City string
Country string
}{City: "Shanghai", Country: "China"},
}
// 方式 3:零值初始化(字段为类型默认值)
var p3 Person // p3.Name == "", p3.Age == 0
结构体字段的访问与修改
通过点号(.
)访问和修改字段:
p := Person{Name: "Alice", Age: 30}
fmt.Println(p.Name) // 输出: Alice
p.Age = 31 // 修改字段值
fmt.Println(p.Age) // 输出: 31
结构体标签(Tag)
结构体字段可以添加元数据标签,常用于 JSON 序列化、ORM 映射等
type User struct {
ID int `json:"id"` // JSON 序列化时使用小写 id
Name string `json:"name"`
Email string `json:"email,omitempty"` // omitempty:字段为空时忽略
}
方法绑定
结构体可以通过方法绑定实现其他语言的类内成员函数的作用。方法定义的语法为:
func (接收者 类型) 方法名(参数列表) 返回值列表 {
// 方法体
}
方法绑定后就可以通过 对象.方法 调用。
封装
Go 语言中,封装 是通过命名约定和包级访问控制实现的,而非传统面向对象语言(如 Java、C++)中的访问修饰符(private、protected、public)。
Go 使用大小写命名来控制标识符(类型、变量、函数、方法等)的访问权限:
- 大写字母开头:公开(public),可被其他包访问。
- 小写字母开头:私有(private),仅在定义所在的包内可见。
假设有包结构如下:
myproject/
└── person/
├── person.go // 定义 Person 结构体
└── utils.go // 辅助函数
└── main.go // 主程序
person/person.go
package person
// Person 结构体(公开)
type Person struct {
Name string // 公开字段
age int // 私有字段
}
// NewPerson 构造函数(公开)
func NewPerson(name string, age int) *Person {
return &Person{
Name: name,
age: age, // 外部无法直接访问 age,但可通过构造函数初始化
}
}
// GetAge 公开方法,获取私有字段
func (p *Person) GetAge() int {
return p.age
}
// SetAge 公开方法,修改私有字段
func (p *Person) SetAge(age int) {
if age > 0 { // 封装业务逻辑
p.age = age
}
}
main.go
package main
import "myproject/person"
func main() {
p := person.NewPerson("Alice", 30)
// fmt.Println(p.age) // ❌ 错误:无法访问私有字段
fmt.Println(p.Name) // ✅ 正确:可访问公开字段
p.SetAge(31) // ✅ 正确:通过公开方法访问私有字段
fmt.Println(p.GetAge()) // 输出: 31
}
继承
在 Go 语言中,继承是通过结构体嵌套和接口(Interface) 实现的
例:
// 基类:Animal
type Animal struct {
Name string
Age int
}
// 基类方法
func (a *Animal) Eat() string {
return a.Name + " is eating."
}
// 子类:Dog 继承 Animal
type Dog struct {
Animal // 匿名字段,继承 Animal 的所有字段和方法
Breed string // 新增字段
}
// 子类方法(覆盖基类方法)
func (d *Dog) Eat() string {
return d.Name + " (a " + d.Breed + ") is eating bones."
}
// 使用示例
func main() {
dog := &Dog{
Animal: Animal{Name: "Buddy", Age: 3},
Breed: "Golden Retriever",
}
fmt.Println(dog.Name) // 直接访问父类字段
fmt.Println(dog.Eat()) // 调用子类方法:Buddy (a Golden Retriever) is eating bones.
fmt.Println(dog.Animal.Eat()) // 显式调用父类方法:Buddy is eating.
}
一个结构体可以嵌套多个其他结构体,从而组合多个类型的功能。
type Engine struct {
Power int
}
func (e *Engine) Start() string {
return "Engine started with " + strconv.Itoa(e.Power) + " HP."
}
type Wheel struct {
Size int
}
func (w *Wheel) Roll() string {
return "Wheel rolling with size " + strconv.Itoa(w.Size) + "."
}
// Car 组合 Engine 和 Wheel
type Car struct {
Engine
Wheel
Brand string
}
// 使用示例
func main() {
car := &Car{
Engine: Engine{Power: 200},
Wheel: Wheel{Size: 18},
Brand: "Tesla",
}
fmt.Println(car.Start()) // 调用 Engine 方法
fmt.Println(car.Roll()) // 调用 Wheel 方法
}
子类可以定义与父类同名的方法,从而覆盖父类方法的实现。
type Shape struct {
Name string
}
func (s *Shape) Area() float64 {
return 0 // 默认实现
}
type Circle struct {
Shape
Radius float64
}
// 重写 Area 方法
func (c *Circle) Area() float64 {
return math.Pi * c.Radius * c.Radius
}
// 使用示例
func main() {
circle := &Circle{
Shape: Shape{Name: "Circle"},
Radius: 5,
}
fmt.Println(circle.Area()) // 输出: 78.53981633974483
}
多态
Go 语言中,多态是通过 接口(interface) 实现的。Go 的接口是一种动态类型,它定义了一组方法签名,只要类型实现了该接口的所有方法,就可以被视为该接口的实例。
接口的定义:
type 接口名 interface {
方法名1(参数列表) 返回值列表
方法名2(参数列表) 返回值列表
// ...
}
只要类型实现了接口中的所有方法,即隐式实现该接口(无需显式声明)。
示例:
type Animal interface {
Speak() string // 所有实现 Animal 接口的类型必须实现 Speak 方法
}
创建不同的结构体(如 Dog、Cat),并实现 Speak() 方法:
type Dog struct {
Name string
}
func (d Dog) Speak() string { // 实现 Animal 接口的 Speak 方法
return "Woof!"
}
type Cat struct {
Name string
}
func (c Cat) Speak() string { // 实现 Animal 接口的 Speak 方法
return "Meow!"
}
当接口变量引用不同类型的实例时,调用相同方法会执行对应类型的实现:
func main() {
var animal Animal // 声明接口变量
// 接口变量指向 Dog 实例
animal = Dog{Name: "Buddy"}
fmt.Println(animal.Speak()) // 输出:Woof!
// 接口变量指向 Cat 实例
animal = Cat{Name: "Mimi"}
fmt.Println(animal.Speak()) // 输出:Meow!
}
执行结果:
interface与类型断言
在 Go 中,所有类型都隐式实现了空接口 interface{},因此可以将任意值赋给 interface{} 类型的变量。
interface{} 类型的值转换为具体类型需要使用 类型断言,类型断言用于检查 interface{} 变量的实际类型,并提取对应的值。有两种语法形式:
//不安全形式,类型不匹配会触发 panic
value := i.(T) // 将 interface{} 变量 i 转换为具体类型 T
//安全形式(带 ok 检查)
// 若类型匹配,ok 为 true,value 是 T 类型的值
// 若不匹配,ok 为 false,value 是 T 的零值,不会 panic
value, ok := i.(T)
示例用法:
package main
func add(a interface{},b interface{}) int {
x,_ := a.(int)
y,_ := b.(int)
return x+y
}
func main() {
x:=add(1,1);
println(x)
}
运行结果:
断言可以配合swich使用:
switch v := i.(type) {
case type1:
// 当 i 的动态类型是 type1 时执行
case type2:
// 当 i 的动态类型是 type2 时执行
default:
// 当 i 的动态类型不匹配上述任何类型时执行
}
反射
在 Go 语言中,反射(Reflection) 是一种在运行时检查和操作程序自身类型、变量和值的机制。通过反射,程序可以:
- 检查变量的类型和值
- 调用方法和操作字段
- 动态创建新对象
- 修改无法直接访问的变量
在Golang的实现中,变量包括(type, value)两部分
● type 包括 static type和concrete type. 简单来说 static type是你在编码是看见的类型(如int、string),concrete type是runtime系统看见的类型
● 类型断言能否成功,取决于变量的concrete type,而不是static type. 因此,一个 reader变量如果它的concrete type也实现了write方法的话,它也可以被类型断言为writer.
每个interface变量都有一个对应pair,pair中记录了实际变量的值和类型。
(type,value)
value是实际变量值,type是实际变量的类型。一个interface{}类型的变量包含了2个指针,一个指针指向值的类型【对应concrete type】,另外一个指针指向实际的值【对应value】。
interface及其pair的存在,是Golang中实现反射的前提。反射就是用来检测存储在接口变量内部(值value;类型concrete type) pair对的一种机制。
Go 的反射基于两个核心类型:reflect.Type 和 reflect.Value,它们定义在 reflect 包中。
reflect.Type
表示类型的元信息,可通过 reflect.TypeOf() 获取:
package main
import (
"fmt"
"reflect"
)
func main() {
x := 42
t := reflect.TypeOf(x)
fmt.Println("类型名称:", t.Name()) // 输出: int
fmt.Println("类型种类:", t.Kind()) // 输出: int
}
reflect.Value
表示值的运行时表示,可通过 reflect.ValueOf() 获取:
x := 42
v := reflect.ValueOf(x)
fmt.Println("值:", v.Int()) // 输出: 42
fmt.Println("类型:", v.Type()) // 输出: int
fmt.Println("种类:", v.Kind()) // 输出: int
通过反射可以做到:修改值、调用方法和类型转换,但较为复杂这里不再过多介绍。
结构体标签
结构体标签(Struct Tag) 是附加在结构体字段上的元数据,用于在运行时通过反射获取额外信息。
结构体标签写在字段声明的后面,用反引号()包裹,格式为 key:"value"`:
type User struct {
ID int `json:"id" db:"user_id"` // 多个键值对用空格分隔
Name string `json:"name,omitempty"` // omitempty 表示为空时忽略
Age int `json:"age" validate:"min=18,max=100"` // 自定义标签
Email string `json:"-"` // - 表示忽略该字段
}
通过反射机制可以读取结构体标签:
package main
import (
"fmt"
"reflect"
)
type resume struct {
Name string `json:"name" doc:"我的名字"`
}
func findDoc(stru interface{}) map[string]string {
t := reflect.TypeOf(stru).Elem()
doc := make(map[string]string)
for i := 0; i < t.NumField(); i++ {
doc[t.Field(i).Tag.Get("json")] = t.Field(i).Tag.Get("doc")
}
return doc
}
func main() {
var stru resume
doc := findDoc(&stru)
fmt.Printf("name字段为:%s\n", doc["name"])
}
运行结果:
golang高阶
goroutine
goroutine 是 Go 语言中实现并发编程的核心机制,它允许你在同一进程中高效地运行多个函数或方法。可以简单将其看为轻量级的线程。
Goroutine 是 Go 运行时管理的轻量级执行单元,相比传统线程(如操作系统线程),它的内存占用更小(初始仅 2KB),创建和销毁成本极低。
多个 goroutine 可以在单个或多个 CPU 核心上并发执行,具体取决于 GOMAXPROCS 设置和调度器。
goroutine 由 Go 运行时调度,Go 在运行时会将 goroutine 分配到操作系统线程上执行。
在正常函数调用之前加上 go 关键字即可创建 goroutine :
package main
import "time"
func func1(){
for {
println("func: hello world")
time.Sleep(1 * time.Second)
}
}
func main(){
go func1()
for {
println("main: hello world")
time.Sleep(1 * time.Second)
}
}
运行结果:
主goroutine退出后,其它的工作goroutine也会自动退出:
package main
import "time"
func func1(){
for {
println("func: hello world")
time.Sleep(1 * time.Second)
}
}
func main(){
go func1()
time.Sleep(3 * time.Second)
}
运行结果:
channel
Channel (通道)是一种特殊的类型,用于在 goroutine 之间安全地传递数据和同步执行状态。它是 Go 语言 “以通信共享内存” 设计理念的核心实现,解决了并发编程中的数据竞争和同步问题。
通道可以简单理解为进程间通信的管道 。通道内置线程同步机制,所以无需加锁,数据会按发送顺序接收。
核心操作:
- 声明:chan T ,其中 T 是传递的数据类型。
- 发送:ch <- data
- 接收:data := <-ch 或 data, ok := <-ch(检查通道是否已关闭)
- 关闭:close(ch)(仅由发送方关闭)
通道分为无缓存通道与有缓存通道,有缓冲通道可以理解为一个消息队列。
// 无缓冲通道(同步通道)
ch := make(chan int)
// 带缓冲通道(异步通道)
ch := make(chan int, 3) // 缓冲大小为 3
goroutine 在执行通道操作(发送 / 接收)时被暂停,直到条件满足才继续执行
无缓冲通道的阻塞条件:
发送操作(ch <- data)
- 阻塞条件:没有其他 goroutine 同时在执行对应的接收操作(<-ch)。
- 解除阻塞:当有其他 goroutine 开始接收该通道的数据时。
2. 接收操作(<-ch)
- 阻塞条件:没有其他 goroutine 同时在执行对应的发送操作(ch <- data)。
- 解除阻塞:当有其他 goroutine 向该通道发送数据时。
ch := make(chan int) // 无缓冲通道
ch <- 1 // 阻塞!没有接收者
fmt.Println(<-ch) // 永远不会执行到这里
带缓冲通道的阻塞条件:
1. 发送操作(ch <- data)
- 阻塞条件:通道的缓冲区已满,无法容纳新数据。
- 解除阻塞:当有其他 goroutine 从通道接收数据,腾出缓冲区空间时。
2. 接收操作(<-ch)
- 阻塞条件:通道的缓冲区为空,且通道未关闭。
- 解除阻塞:当有其他 goroutine 向通道发送数据时;或通道被关闭(此时接收操作返回零值)。
ch := make(chan int, 2) // 缓冲大小为 2
ch <- 1 // 不阻塞,缓冲区未满
ch <- 2 // 不阻塞,缓冲区未满
ch <- 3 // 阻塞!缓冲区已满,需等待接收操作
go func() {
fmt.Println(<-ch) // 消费一个元素,腾出空间
}()
下面是一个基本示例,实现了一个生产者消费者模型:
package main
import "fmt"
func producer(ch chan<- int) {
for i := 0; i < 5; i++ {
ch <- i // 发送数据到通道
}
close(ch) // 关闭通道,表示不再发送数据
}
func consumer(ch <-chan int) {
for num := range ch { // 自动接收数据直到通道关闭
fmt.Println("Received:", num)
}
}
func main() {
ch := make(chan int)
go producer(ch)
consumer(ch)
}
Select
select 语句允许一个 goroutine 同时监听多个通道操作,当其中一个通道就绪时执行对应的分支,如果有多个通道同时就绪,则随机选择一个执行。可以简单理解为 IO 多路复用。
select {
case data := <-ch1:
// 处理从 ch1 接收的数据
case ch2 <- value:
// 向 ch2 发送数据
case data, ok := <-ch3:
// 处理从 ch3 接收的数据,并检查通道是否已关闭
}
select分为阻塞模式与非阻塞模式:
- 无
default
分支:
当所有通道都未就绪时,select
会阻塞当前 goroutine,直到至少一个通道就绪。 - 有
default
分支:
当所有通道都未就绪时,立即执行default
分支(非阻塞)。
// 非阻塞示例
select {
case data := <-ch:
// 处理数据
default:
// 通道未就绪时立即执行
fmt.Println("通道未就绪,继续执行其他任务")
}
下面是一个示例,使用select做一个任务分发:
func processTask(task Task, workers []chan Task) {
// 选择一个空闲的 worker 通道发送任务
select {
case workers[0] <- task:
fmt.Println("Task sent to worker 0")
case workers[1] <- task:
fmt.Println("Task sent to worker 1")
case workers[2] <- task:
fmt.Println("Task sent to worker 2")
}
}