go语言快速入门

发布于:2025-07-02 ⋅ 阅读:(25) ⋅ 点赞:(0)

目录

安装开发环境

编译与运行

编译 Go 程序

运行 Go 程序

基础语法

基础格式

变量声明

常量与枚举

函数

defer

slice

map

面向对象

结构体

封装

 继承

多态

interface与类型断言

反射

结构体标签

golang高阶

goroutine

channel

Select


安装开发环境

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")
    }
}

 

 


网站公告

今日签到

点亮在社区的每一天
去签到