Golang 并发编程(Goroutine、Channels、Select、Sync、原子操作函数、Context、gpool)

文章目录

文章中的示例均在 go1.22.3 版本上运行。

Goroutine

Goroutine是由Go语言提供的一种轻量级线程(协程)管理方式。它比操作系统的线程更加高效和轻量,因此可以在同一时间运行成千上万个Goroutine。

Goroutine是实现并发编程的核心。它们是轻量级线程(协程),由Go运行时管理,可以在同一时间运行多个Goroutine,从而实现高并发处理。

  • 特性

    • 轻量级: Goroutine非常轻量,每个Goroutine消耗的内存远小于一个系统线程。
    • 独立栈: 每个Goroutine有一个独立的栈,初始大小通常是很小的(如几KB),并且可以根据需要动态增长。
    • 非抢占式调度: Go运行时会负责调度Goroutine,使用了协作式调度和一小部分抢占式调度。Go1.14版本之后引入异步抢占机制,使得长时间运行的Goroutine可以被抢占,从而提高系统响应性。
    • Race Detector: 提供了竞争检测工具,帮助开发者发现并解决数据竞争问题。
    • 编译器/解释器/虚拟机层面的多任务
    • 多个轻量级线程(协程)可能在一个或多个系统线程上运行
  • 调度器(Goroutine 可能的切换点)

    • I/O, select
    • channel
    • 等待锁
    • 函数调用(有时)
    • runtime.Gosched() :交出控制权,以允许其他轻量级线程(协程)运行
  • 优势

    • 高并发性:Goroutine允许在同一时间处理大量任务,提高了程序的并发能力。
    • 简单的并发模型:相比于传统的多线程编程,Goroutine和Channel提供了一个更简单和安全的并发编程模型。
    • 自动管理:Go运行时自动管理Goroutine的调度和内存分配,使得开发者不需要过多关注底层实现细节。
  • 启动 Goroutine

go 复制代码
package main

import (
	"fmt"
	"time"
)

func say(s string) {
    fmt.Println(s)
}

func main() {
    // go 关键字用于启动一个新的 Goroutine,用于并发执行函数或方法。
	go say("Hello World")
	// 让主Goroutine暂停执行1秒钟
	time.Sleep(1 * time.Second)
}

如果主Goroutine在启动其他Goroutine之后立即结束,其他Goroutine可能没有机会完成其工作。使用time.Sleep来延迟主Goroutine的结束,可以让其他Goroutine有足够的时间完成其任务。

  • 启动匿名函数的 Goroutine
go 复制代码
package main

import (
	"fmt"
	"time"
)

func main() {
	go func(s string) {
		fmt.Println(s)
	}("Hello World")
	time.Sleep(1 * time.Second)
}
  • 启动方法的 Goroutine
go 复制代码
package main

import (
	"fmt"
	"time"
)

type Person struct {
	Name string
	Age  int
}

func (person *Person) sayHello() {
	fmt.Println("Hello, my name is", person.Name)
}

func main() {
	person := &Person{Name: "John Doe", Age: 18}
	go person.sayHello()
	time.Sleep(1 * time.Second)
}
  • Race Detector(数据竞争检测器) 是一个用于检测数据竞争的工具。数据竞争是在两个或多个goroutine同时访问相同的变量,并且至少有一个是写操作而没有同步机制保护的情况下发生的。
go 复制代码
package main

import (
	"fmt"
	"time"
)

var counter int

func increment() {
	counter++
}

func main() {
	for i := 0; i < 10; i++ {
		go increment()
	}
	time.Sleep(time.Second)
	fmt.Println("Final counter:", counter) // 输出:Final counter: 10
}
bash 复制代码
# 执行后输出数据竞争的警告
go run -race main.go

注意: 修复数据竞争问题,可以使用 同步机制,例如sync.Mutex或sync/atomic包。

Sync(同步机制)

  • sync.Mutex(互斥锁)
  • sync.RWMutex(读写互斥锁)
  • sync.WaitGroup(等待组)
  • sync.Map(并发安全的映射)

sync.Mutex(互斥锁)

用于在多Goroutine环境中保护共享资源,防止竞态条件(Race Condition)的发生。互斥锁通过锁和解锁的机制来确保同时只有一个Goroutine可以访问共享资源。

  • 避免死锁:确保锁和解锁成对出现,尤其是在可能出现错误或提前返回的代码路径上使用defer来确保解锁。
  • 最小化锁的粒度:锁定的范围应该尽可能小,以减少锁的持有时间,从而提高并发性能。
  • 避免重复加锁:在同一个Goroutine内重复锁定同一个互斥锁会导致死锁。
go 复制代码
package main

import (
	"fmt"
	"sync"
	"time"
)

func main() {
	var mu sync.Mutex
	var counter int

	// 启动多个Goroutine,每个都增加计数器
	for i := 0; i < 2; i++ {
		go func(id int) {
			for j := 0; j < 4; j++ {
				mu.Lock() // 加锁
				counter++
				fmt.Printf("Goroutine %d incremented counter to %d\n", id, counter)
				mu.Unlock() // 解锁
				time.Sleep(time.Millisecond * 100)
			}
		}(i)
	}

	// 等待一段时间以确保所有Goroutine完成
	time.Sleep(2 * time.Second)
	fmt.Printf("Final counter value: %d\n", counter)
}
  • 输出结果
bash 复制代码
Goroutine 0 incremented counter to 1
Goroutine 1 incremented counter to 2
Goroutine 1 incremented counter to 3
Goroutine 0 incremented counter to 4
Goroutine 1 incremented counter to 5
Goroutine 0 incremented counter to 6
Goroutine 0 incremented counter to 7
Goroutine 1 incremented counter to 8
Final counter value: 8

sync.RWMutex(读写互斥锁)

允许多个读操作并发执行,同时写操作是独占的,即在写操作进行时,不允许任何读操作或其他写操作。读写互斥锁的主要优点是在读多写少的情况下,可以提高程序的并发性能。

  • Lock(): 锁定写操作。
  • Unlock(): 解锁写操作。
  • RLock(): 锁定读操作。
  • RUnlock(): 解锁读操作。
go 复制代码
package main

import (
	"fmt"
	"sync"
	"time"
)

func main() {
	var mu sync.RWMutex
	var counter int

	// 启动多个Goroutine进行读操作
	for i := 0; i < 2; i++ {
		go func(id int) {
			for j := 0; j < 2; j++ {
				mu.RLock() // 加锁读操作
				fmt.Printf("Goroutine %d read counter: %d\n", id, counter)
				mu.RUnlock() // 解锁读操作
				time.Sleep(time.Millisecond * 100)
			}
		}(i)
	}

	// 启动多个Goroutine进行写操作
	for i := 0; i < 2; i++ {
		go func(id int) {
			for j := 0; j < 2; j++ {
				mu.Lock() // 加锁写操作
				counter++
				fmt.Printf("Goroutine %d incremented counter to: %d\n", id, counter)
				mu.Unlock() // 解锁写操作
				time.Sleep(time.Millisecond * 150)
			}
		}(i)
	}

	// 等待一段时间以确保所有Goroutine完成
	time.Sleep(3 * time.Second)
	fmt.Printf("Final counter value: %d\n", counter)
}
  • 输出结果
bash 复制代码
Goroutine 1 read counter: 0
Goroutine 1 incremented counter to: 1
Goroutine 0 read counter: 1
Goroutine 0 incremented counter to: 2
Goroutine 0 read counter: 2
Goroutine 1 read counter: 2
Goroutine 0 incremented counter to: 3
Goroutine 1 incremented counter to: 4
Final counter value: 4

sync.WaitGroup(等待组)

用于等待一组 Goroutine 完成。当我们需要启动多个 Goroutine 并等待它们全部完成时,sync.WaitGroup 非常有用。WaitGroup 提供了一种简单的方式来计数并发操作,并在所有操作完成后继续执行。

  • Add(delta int): 增加或减少等待计数器的值。
  • Done(): 将等待计数器的值减一。通常在 Goroutine 完成时调用。
  • Wait(): 阻塞直到等待计数器变为零。
go 复制代码
package main

import (
	"fmt"
	"sync"
	"time"
)

func worker(id int, wg *sync.WaitGroup) {
	defer wg.Done() // 在函数结束时调用 Done,减少计数器
	fmt.Printf("Worker %d starting\n", id)

	// 模拟工作
	time.Sleep(time.Second)
	fmt.Printf("Worker %d done\n", id)
}

func main() {
	var wg sync.WaitGroup

	for i := 1; i <= 5; i++ {
		wg.Add(1) // 增加计数器
		go worker(i, &wg)
	}

	wg.Wait() // 阻塞直到所有 Goroutine 完成
	fmt.Println("All workers done")
}
  • 输出结果
bash 复制代码
Worker 5 starting
Worker 2 starting
Worker 3 starting
Worker 1 starting
Worker 4 starting
Worker 4 done
Worker 1 done
Worker 2 done
Worker 5 done
Worker 3 done
All workers done

sync.Map(并发安全的映射)

它提供了一种简单且高效的方式来在多个 Goroutine 之间共享数据,而无需显式地使用互斥锁来保护对 map 的访问。与普通的 Go map 不同,sync.Map 进行了优化,特别适用于读多写少的场景。它提供了原子操作来确保并发访问的安全性,同时避免了手动管理锁的复杂性。

  • Store(key, value interface{}): 设置指定 key 的值。
  • Load(key interface{}) (value interface{}, ok bool): 获取指定 key 的值。
  • LoadOrStore(key, value interface{}) (actual interface{}, loaded bool): 获取指定 key 的值,如果 key 不存在则存储指定的值。
  • Delete(key interface{}): 删除指定 key 的值。
  • Range(f func(key, value interface{}) bool): 遍历 map 中的所有 key 和 value。
go 复制代码
package main

import (
	"fmt"
	"sync"
	"time"
)

func main() {
	var m sync.Map
	var wg sync.WaitGroup

	// 启动多个 Goroutine 进行写操作
	for i := 0; i < 2; i++ {
		wg.Add(1)
		go func(id int) {
			defer wg.Done()
			for j := 0; j < 2; j++ {
				key := fmt.Sprintf("key-%d-%d", id, j)
				value := fmt.Sprintf("value-%d-%d", id, j)
				m.Store(key, value)
				fmt.Printf("Goroutine %d stored %s: %s\n", id, key, value)
				time.Sleep(time.Millisecond * 100)
			}
		}(i)
	}

	// 启动多个 Goroutine 进行读操作
	for i := 0; i < 2; i++ {
		wg.Add(1)
		go func(id int) {
			defer wg.Done()
			for j := 0; j < 2; j++ {
				key := fmt.Sprintf("key-%d-%d", id, j)
				if value, ok := m.Load(key); ok {
					fmt.Printf("Goroutine %d loaded %s: %s\n", id, key, value)
				} else {
					fmt.Printf("Goroutine %d failed to load %s\n", id, key)
				}
				time.Sleep(time.Millisecond * 150)
			}
		}(i)
	}

	wg.Wait() // 等待所有 Goroutine 完成

	// 遍历 map 中的所有键值对
	m.Range(func(key, value interface{}) bool {
		fmt.Printf("Final key: %s, value: %s\n", key, value)
		return true
	})
}
  • 输出结果
bash 复制代码
Goroutine 1 failed to load key-1-0
Goroutine 0 failed to load key-0-0
Goroutine 0 stored key-0-0: value-0-0
Goroutine 1 stored key-1-0: value-1-0
Goroutine 0 stored key-0-1: value-0-1
Goroutine 1 stored key-1-1: value-1-1
Goroutine 0 loaded key-0-1: value-0-1
Goroutine 1 loaded key-1-1: value-1-1
Final key: key-0-0, value: value-0-0
Final key: key-1-0, value: value-1-0
Final key: key-0-1, value: value-0-1
Final key: key-1-1, value: value-1-1

sync/atomic(原子操作函数)

在并发编程中,使用原子操作可以有效地避免竞争条件和数据不一致问题。Go 语言标准库中的 sync/atomic 包提供了一组用于原子操作的函数,这些函数可以对整数和指针类型进行原子读写和修改操作。

原子加载和存储(Load/Store)

  • atomic.LoadInt32 和 atomic.StoreInt32
  • atomic.LoadInt64 和 atomic.StoreInt64
  • atomic.LoadUint32 和 atomic.StoreUint32
  • atomic.LoadUint64 和 atomic.StoreUint64
  • atomic.LoadPointer 和 atomic.StorePointer
go 复制代码
package main

import (
	"fmt"
	"sync"
	"sync/atomic"
	"time"
)

func main() {
	var value int64
	var wg sync.WaitGroup

	// 启动一个 Goroutine 来定期更新 value
	wg.Add(1)
	go func() {
		defer wg.Done()
		for i := int64(0); i < 2; i++ {
			atomic.StoreInt64(&value, i)
			fmt.Printf("Stored value: %d\n", i)
			time.Sleep(time.Millisecond * 100)
		}
	}()

	// 启动一个 Goroutine 来定期读取 value
	wg.Add(1)
	go func() {
		defer wg.Done()
		for i := 0; i < 2; i++ {
			v := atomic.LoadInt64(&value)
			fmt.Printf("Loaded value: %d\n", v)
			time.Sleep(time.Millisecond * 150)
		}
	}()

	wg.Wait() // 等待所有 Goroutine 完成
}
  • 输出结果
bash 复制代码
Loaded value: 0
Stored value: 0
Stored value: 1
Loaded value: 1

原子加法和减法(Add)

  • atomic.AddInt32
  • atomic.AddInt64
  • atomic.AddUint32
  • atomic.AddUint64
go 复制代码
package main

import (
	"fmt"
	"sync"
	"sync/atomic"
	"time"
)

func main() {
	var counter int64
	var wg sync.WaitGroup

	// 启动多个 Goroutine 进行增加操作
	for i := 0; i < 2; i++ {
		wg.Add(1)
		go func(id int) {
			defer wg.Done()
			for j := 0; j < 2; j++ {
				atomic.AddInt64(&counter, 1)
				fmt.Printf("Goroutine %d added 1, counter: %d\n", id, atomic.LoadInt64(&counter))
				time.Sleep(time.Millisecond * 10)
			}
		}(i)
	}

	// 启动多个 Goroutine 进行减少操作
	for i := 0; i < 2; i++ {
		wg.Add(1)
		go func(id int) {
			defer wg.Done()
			for j := 0; j < 2; j++ {
				atomic.AddInt64(&counter, -1)
				fmt.Printf("Goroutine %d subtracted 1, counter: %d\n", id, atomic.LoadInt64(&counter))
				time.Sleep(time.Millisecond * 20)
			}
		}(i)
	}

	wg.Wait() // 等待所有 Goroutine 完成

	fmt.Printf("Final counter value: %d\n", counter)
}
  • 输出结果
bash 复制代码
Goroutine 1 subtracted 1, counter: -1
Goroutine 0 subtracted 1, counter: 0
Goroutine 1 added 1, counter: 1
Goroutine 0 added 1, counter: 0
Goroutine 0 added 1, counter: 1
Goroutine 1 added 1, counter: 2
Goroutine 1 subtracted 1, counter: 1
Goroutine 0 subtracted 1, counter: 0
Final counter value: 0

原子比较并交换(CompareAndSwap,简称 CAS)

  • atomic.CompareAndSwapInt32
  • atomic.CompareAndSwapInt64
  • atomic.CompareAndSwapUint32
  • atomic.CompareAndSwapUint64
  • atomic.CompareAndSwapPointer
go 复制代码
package main

import (
	"fmt"
	"sync"
	"sync/atomic"
	"time"
)

func main() {
	var counter int64
	var wg sync.WaitGroup

	// 启动多个 Goroutine 进行增加操作
	for i := 0; i < 2; i++ {
		wg.Add(1)
		go func(id int) {
			defer wg.Done()
			for j := 0; j < 2; j++ {
				for {
					current := atomic.LoadInt64(&counter)
					next := current + 1
					if atomic.CompareAndSwapInt64(&counter, current, next) {
						fmt.Printf("Goroutine %d added 1, counter: %d\n", id, next)
						break
					}
				}
				time.Sleep(time.Millisecond * 10)
			}
		}(i)
	}

	// 启动多个 Goroutine 进行减少操作
	for i := 0; i < 2; i++ {
		wg.Add(1)
		go func(id int) {
			defer wg.Done()
			for j := 0; j < 2; j++ {
				for {
					current := atomic.LoadInt64(&counter)
					next := current - 1
					if atomic.CompareAndSwapInt64(&counter, current, next) {
						fmt.Printf("Goroutine %d subtracted 1, counter: %d\n", id, next)
						break
					}
				}
				time.Sleep(time.Millisecond * 20)
			}
		}(i)
	}

	wg.Wait() // 等待所有 Goroutine 完成

	fmt.Printf("Final counter value: %d\n", counter)
}
  • 输出结果
bash 复制代码
Goroutine 1 subtracted 1, counter: 0
Goroutine 0 added 1, counter: 1
Goroutine 1 added 1, counter: 1
Goroutine 0 subtracted 1, counter: 0
Goroutine 1 added 1, counter: 1
Goroutine 0 added 1, counter: 2
Goroutine 0 subtracted 1, counter: 1
Goroutine 1 subtracted 1, counter: 0
Final counter value: 0

原子交换(Swap)

  • atomic.SwapInt32
  • atomic.SwapInt64
  • atomic.SwapUint32
  • atomic.SwapUint64
  • atomic.SwapPointer
go 复制代码
package main

import (
	"fmt"
	"sync/atomic"
)

func main() {
	var value int32 = 10
	var newValue int32 = 20

	fmt.Printf("Original value: %d\n", value)

	// 使用原子交换将 value 的值替换为 newValue
	oldValue := atomic.SwapInt32(&value, newValue)

	fmt.Printf("New value: %d, Old value: %d\n", value, oldValue)
}
  • 输出结果
bash 复制代码
Original value: 10
New value: 20, Old value: 10

Context(上下文)

context包提供了一种在函数间传递取消信号、超时和截止时间的方式。它主要用于控制多个Goroutine之间的取消、超时和截止操作。context包的核心类型是Context接口,它定义了一些方法来获取和设置上下文中的值、取消操作以及阻塞等待操作。

Context接口定义

go 复制代码
type Context interface {
    Deadline() (deadline time.Time, ok bool)
    Done() <-chan struct{}
    Err() error
    Value(key interface{}) interface{}
}
  • Deadline()方法返回上下文的截止时间(如果有),ok值表示是否有截止时间。如果上下文不支持截止时间,则ok为false。
  • Done()方法返回一个<-chan struct{}类型的通道。当上下文被取消或者超时时,该通道将被关闭。
  • Err()方法返回取消的原因。如果上下文没有被取消,则返回nil。
  • Value(key)方法返回上下文中与key相关联的值,如果没有与key相关联的值,则返回nil。

创建Context

  • context.Background():返回一个空的Context,它在没有明确指定的情况下作为所有其他上下文的父上下文。
  • context.TODO():返回一个空的Context,它表示不应该使用的上下文,用于作为函数的参数传递,而不是nil。
  • context.WithCancel(parent):返回一个具有取消功能的上下文,当调用cancel函数时,它将取消与之关联的上下文及其所有子上下文。
  • context.WithTimeout(parent, timeout):返回一个具有截止时间的上下文,当指定的时间过去后,它将自动取消与之关联的上下文及其所有子上下文。
  • context.WithDeadline(parent, deadline):类似于WithTimeout,不同之处在于它接受一个截止时间而不是持续时间。
  • context.WithValue(parent, key, value):返回一个具有与key关联的值的上下文。这对于传递请求范围的参数很有用。

使用Context

go 复制代码
package main

import (
	"context"
	"fmt"
	"time"
)

// worker 函数代表一个工作任务的执行者。
func worker(ctx context.Context, id int) {
	for {
		select {
		case <-ctx.Done(): // 监听上下文的取消信号
			fmt.Printf("Worker %d: Context canceled\n", id)
			return // 收到取消信号后退出Goroutine
		default:
			fmt.Printf("Worker %d: Working...\n", id)
			time.Sleep(1 * time.Second) // 模拟工作时间
		}
	}
}

func main() {
	// 创建一个带有3秒超时的上下文
	ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second)
	defer cancel() // 在main函数结束时取消上下文

	// 启动多个Goroutine进行工作
	for i := 1; i <= 3; i++ {
		go worker(ctx, i)
	}

	// 等待一段时间,然后取消上下文
	time.Sleep(2 * time.Second)
	cancel() // 取消上下文,通知所有Goroutine停止工作

	// 等待一段时间,以观察效果
	time.Sleep(2 * time.Second)
	fmt.Println("Main program finished")
}
  • 输出结果
bash 复制代码
Worker 3: Working...
Worker 1: Working...
Worker 2: Working...
Worker 2: Working...
Worker 1: Working...
Worker 3: Working...
Worker 1: Context canceled
Worker 2: Context canceled
Worker 3: Context canceled
Main program finished

Channels

通道(Channel)是一种用来在Goroutine之间传递数据的方式。它是一种类型安全的、并发安全的、支持同步操作的数据结构。通道提供了一种通信的机制,允许一个Goroutine将数据发送到通道,另一个Goroutine从该通道接收数据。

注意: 使用通道的双向性可以更清楚地表达通道的用途。chan<- 向通道发送数据,<-chan 从通道接收数据。

通道的特性

  • 类型安全: 通道是类型安全的,即通道在创建时必须指定数据元素的类型,只能发送和接收相同类型的数据。
  • 并发安全: 通道在多个Goroutine之间进行通信时是并发安全的,无需额外的锁。
  • 阻塞等待: 当通道为空时,试图从通道中接收数据的操作会阻塞当前Goroutine,直到有数据可用;当通道已满时,试图向通道发送数据的操作也会阻塞当前Goroutine,直到有空间可用。
  • 同步操作: 通道提供了同步操作的机制,发送操作和接收操作都是同步的。

创建和使用通道

可以使用make函数来创建通道,并使用<-操作符来发送和接收数据。

go 复制代码
package main

import "fmt"

func main() {
	// 创建一个int类型的通道
	ch := make(chan int)

	// 启动一个Goroutine发送数据到通道
	go func() {
		ch <- 42 // 将数据42发送到通道
	}()

	// 从通道接收数据并打印
	value := <-ch      // 从通道接收数据并赋值给变量value
	fmt.Println(value) // 输出:42
}

通道的关闭

通道还支持关闭操作,可以使用close函数来关闭通道。关闭通道后,不能再向其发送数据,但仍然可以从中接收数据。关闭的通道会立即释放其所有资源。

go 复制代码
package main

import "fmt"

func main() {
	ch := make(chan int)

	// 启动一个Goroutine发送数据到通道
	go func() {
		defer close(ch) // 在Goroutine结束前关闭通道
		ch <- 42        // 将数据42发送到通道
	}()

	// 从通道接收数据并打印
	for value := range ch { // 循环接收通道数据,直到通道关闭
		fmt.Println(value) // 输出42
	}
}

通道作为函数参数

当将通道作为函数参数传递时,可以将通道作为参数传递给函数,并在函数内部对该通道进行操作。

go 复制代码
package main

import "fmt"

// send 函数用于向通道发送数据
func send(ch chan<- int, value int) {
	ch <- value // 向通道发送数据
}

// receive 函数用于从通道接收数据
func receive(ch <-chan int) {
	value := <-ch // 从通道接收数据
	fmt.Println("Received:", value)
}

func main() {
	// 创建一个int类型的通道
	ch := make(chan int)

	// 启动一个Goroutine发送数据到通道
	go send(ch, 42)

	// 在主Goroutine中从通道接收数据
	receive(ch)
}
  • 输出结果
bash 复制代码
Received: 42

通道作为函数的返回值

将通道作为函数的返回值通常用于在不同的Goroutine之间进行通信。

go 复制代码
package main

import "fmt"

// createChannel 函数用于创建并返回一个int类型的通道
func createChannel() <-chan int {
	ch := make(chan int) // 创建一个int类型的通道

	// 启动一个Goroutine发送数据到通道
	go func() {
		defer close(ch) // 在Goroutine结束前关闭通道
		ch <- 42        // 向通道发送数据
	}()

	return ch // 返回通道
}

// receiveData 函数用于从通道接收数据
func receiveData(ch <-chan int) {
	value := <-ch
	fmt.Println("Received:", value) // 输出Received: 42
}

func main() {
	// 调用createChannel函数创建并返回通道
	ch := createChannel()

	// 在主Goroutine中从通道接收数据
	receiveData(ch)
}
  • 输出结果
bash 复制代码
Received: 42

注意: 使用通道的双向性可以更清楚地表达通道的用途。chan<- 向通道发送数据,<-chan 从通道接收数据。

结构体类型的通道示例

go 复制代码
package main

import "fmt"

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

// PersonChannel 结构体封装了一个传递 Person 结构体的通道
type PersonChannel struct {
    ch chan Person
}

// NewPersonChannel 函数用于创建并返回一个只接收 Person 结构体的通道
func NewPersonChannel() <-chan Person {
    ch := make(chan Person)

    // 启动一个Goroutine发送数据到通道
    go func() {
        defer close(ch) // 在Goroutine结束前关闭通道
        ch <- Person{Name: "Alice", Age: 30} // 向通道发送 Person 结构体
    }()

    return ch
}

// ReceiveDataFromChannel 函数接收一个只接收 Person 结构体的通道,并从该通道接收数据
func ReceiveDataFromChannel(ch <-chan Person) {
    person := <-ch // 从通道接收 Person 结构体
    fmt.Printf("Received: %+v\n", person) // 输出Received: {Name:Alice Age:30}
}

func main() {
    // 创建一个只接收 Person 结构体的通道
    ch := NewPersonChannel()

    // 在主Goroutine中从通道接收数据
    ReceiveDataFromChannel(ch)
}
  • 输出结果
bash 复制代码
Received: {Name:Alice Age:30}

Select

select语句用于在多个通道操作中进行选择。它类似于switch语句,但用于通道操作。select会阻塞,直到其中一个case中的操作可以进行,然后执行该操作。如果有多个通道可以操作,则随机选择一个执行。select主要用于处理多路通道操作,以实现超时、非阻塞发送/接收以及处理多个通道的场景。

  • select 会选择一个可以立即执行的 case 语句。如果有多个 case 语句可以执行,会随机选择一个执行。
  • 如果没有任何 case 语句可以执行,且有 default 语句,则执行 default 语句。
  • 如果没有任何 case 语句可以执行,且没有 default 语句,则 select 会阻塞,直到有一个 case 语句可以执行。

select 语法

go 复制代码
select {
case <-ch1:
    // 从 ch1 接收到了数据
case ch2 <- x:
    // 向 ch2 发送了数据 x
case <-time.After(time.Second):
    // 超时,执行超时逻辑
default:
    // 如果没有任何通道操作可以立即进行,则执行这里的逻辑
}

从通道中接收数据

go 复制代码
select {
case msg := <-ch1:
    fmt.Println("Received from ch1:", msg)
}
  • 应用场景: 当你有一个 Goroutine 在等待从通道 ch1 中接收数据时,可以使用这种方式。例如,一个服务器 Goroutine 从多个客户端连接通道中接收消息。
go 复制代码
package main

import (
    "fmt"
    "time"
)

func main() {
    ch1 := make(chan string)
    go func() {
        time.Sleep(1 * time.Second)
        ch1 <- "Message from ch1"
    }()

    select {
    case msg := <-ch1:
        fmt.Println("Received:", msg)
    }
}
  • 输出结果
bash 复制代码
Received: Message from ch1

向通道发送数据

go 复制代码
select {
case ch2 <- x:
    fmt.Println("Sent to ch2:", x)
}
  • 应用场景: 当你需要在 Goroutine 中向通道发送数据时,可以使用这种方式。例如,一个任务分配器向工作者通道发送任务。
go 复制代码
package main

import "fmt"

func main() {
    ch2 := make(chan int)
    go func() {
        select {
        case ch2 <- 42:
            fmt.Println("Sent 42 to ch2")
        }
    }()

    // 接收数据以避免死锁
    fmt.Println("Received from ch2:", <-ch2)
}
  • 输出结果
bash 复制代码
Received from ch2: 42

超时处理

go 复制代码
select {
case <-time.After(time.Second):
    fmt.Println("Timeout after 1 second")
}
  • 应用场景: 当你希望在一定时间内等待某些操作完成,如果超时则执行其他操作时,可以使用这种方式。例如,网络请求超时处理。
go 复制代码
package main

import (
    "fmt"
    "time"
)

func main() {
    ch := make(chan string)

    go func() {
        time.Sleep(2 * time.Second)
        ch <- "Message"
    }()

    select {
    case msg := <-ch:
        fmt.Println("Received:", msg)
    case <-time.After(1 * time.Second):
        fmt.Println("Timeout after 1 second")
    }
}
  • 输出结果
bash 复制代码
Timeout after 1 second

非阻塞操作

go 复制代码
select {
default:
    fmt.Println("No channel operations ready")
}
  • 应用场景: 当你希望在没有任何通道操作可以立即进行时执行某些逻辑,可以使用这种方式。例如,检查通道是否有数据可接收或发送,但不希望阻塞。
go 复制代码
package main

import "fmt"

func main() {
    ch := make(chan int, 1)

    select {
    case ch <- 1:
        fmt.Println("Sent to ch")
    default:
        fmt.Println("No channel operations ready")
    }

    select {
    case msg := <-ch:
        fmt.Println("Received from ch:", msg)
    default:
        fmt.Println("No channel operations ready")
    }
}
  • 输出结果
bash 复制代码
Sent to ch
Received from ch: 1

综合应用场景

go 复制代码
package main

import (
	"fmt"
	"time"
)

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

	// 启动一个Goroutine,在1秒后向ch1发送数据
	go func() {
		time.Sleep(1 * time.Second)
		ch1 <- "Hello from ch1"
	}()

	// 启动一个Goroutine,在2秒后向ch2发送数据
	go func() {
		time.Sleep(2 * time.Second)
		ch2 <- 42
	}()

	// 创建一个3秒的超时通道
	timeout := time.After(3 * time.Second)

	// 循环4次以涵盖所有操作,包括非阻塞和超时处理
	for i := 0; i < 4; i++ {
		select {
		case msg := <-ch1:
			// 从ch1接收到数据并输出
			fmt.Printf("Received from ch1: %s\n", msg)
		case val := <-ch2:
			// 从ch2接收到数据并输出
			fmt.Printf("Received from ch2: %d\n", val)
		case <-timeout:
			// 如果所有通道操作在3秒内未完成,执行超时处理并退出循环
			fmt.Println("Timeout: No more data to receive")
			return
		default:
			// 如果没有任何通道操作可以立即进行,输出提示信息
			fmt.Println("No channel operations ready")
		}
		// 模拟其他处理逻辑
		time.Sleep(500 * time.Millisecond)
	}

	// 循环结束后输出完成信息
	fmt.Println("Finished processing all channel operations")
}
  • 输出结果
bash 复制代码
No channel operations ready
No channel operations ready
Received from ch1: Hello from ch1
No channel operations ready
Finished processing all channel operations

gpool(协程池库)

字节跳动的 gpool 是一个高效的 Goroutine 池,用于管理和调度 Go 语言中的并发任务。gpool 提供了一种机制,通过复用 Goroutine 来减少创建和销毁 Goroutine 的开销,从而优化性能和资源使用。

  • 高效的任务调度: gpool 能够高效地调度任务,确保在高并发场景下,任务能够被及时处理。
  • Goroutine 复用: 通过复用 Goroutine,减少频繁创建和销毁 Goroutine 带来的开销,提升系统的整体性能。
  • 灵活的配置: 提供了多种配置选项,可以根据具体的业务场景调整 Goroutine 池的大小和行为。
  • 任务队列: 任务被提交到一个队列中,由 Goroutine 池中的 Goroutine 按顺序执行。
  • 错误处理: 支持任务执行中的错误处理机制,确保系统的稳定性。

安装使用

bash 复制代码
go get github.com/bytedance/gopkg/util/gopool
go 复制代码
package main

import (
	"fmt"
	"sync"
	"time"

	"github.com/bytedance/gopkg/util/gopool"
)

func main() {
	// 定义一个任务函数
	task := func(i int) {
		fmt.Printf("Processing task: %d\n", i)
		time.Sleep(time.Second) // 模拟任务处理时间
		fmt.Printf("Completed task: %d\n", i)
	}

	// 创建一个具有5个worker的协程池
	pool := gopool.NewPool("examplePool", 5, gopool.NewConfig())

	var wg sync.WaitGroup

	// 提交10个任务到协程池
	for i := 0; i < 10; i++ {
		wg.Add(1)
		i := i // 避免闭包问题
		pool.Go(func() {
			defer wg.Done()
			task(i)
		})
	}

	// 等待所有任务完成
	wg.Wait()
	fmt.Println("All tasks completed.")
}
  • 输出结果
bash 复制代码
Processing task: 0
Processing task: 1
Processing task: 3
Processing task: 2
Processing task: 4
Completed task: 1
Processing task: 5
Completed task: 4
Completed task: 2
Processing task: 7
Completed task: 0
Processing task: 8
Processing task: 6
Completed task: 3
Processing task: 9
Completed task: 5
Completed task: 6
Completed task: 9
Completed task: 7
Completed task: 8
All tasks completed.
相关推荐
爱吃生蚝的于勒1 小时前
C语言内存函数
c语言·开发语言·数据结构·c++·学习·算法
码上一元2 小时前
SpringBoot自动装配原理解析
java·spring boot·后端
小白学大数据3 小时前
Python爬虫开发中的分析与方案制定
开发语言·c++·爬虫·python
冰芒猓4 小时前
SpringMVC数据校验、数据格式化处理、国际化设置
开发语言·maven
失落的香蕉4 小时前
C语言串讲-2之指针和结构体
java·c语言·开发语言
jerry6094 小时前
7天用Go从零实现分布式缓存GeeCache(改进)(未完待续)
分布式·缓存·golang
枫叶_v4 小时前
【SpringBoot】22 Txt、Csv文件的读取和写入
java·spring boot·后端
红中马喽4 小时前
JS学习日记(webAPI—DOM)
开发语言·前端·javascript·笔记·vscode·学习
杜杜的man4 小时前
【go从零单排】Closing Channels通道关闭、Range over Channels
开发语言·后端·golang
java小吕布5 小时前
Java中Properties的使用详解
java·开发语言·后端