Go语言并发编程:深入理解select语句

Evan
Evan
发布时间:2026-10-15 | 4人浏览过
原创

本文深入解析 Go 语言中的 select 语句,涵盖空 select 导致的死锁、仅有 default 的非阻塞特性、case 匹配的随机性原理,以及如何通过 for + select 实现持续监听。

理解 Select 基础

select 是 Go 中专门用于处理 channel 的语句结构,类似于其他编程语言中的 switch 语句,但它只能用于 channel 操作。它的核心作用是在多个通道操作中选择一个可执行的进行。

第1步:了解空 Select 的阻塞行为

一个没有任何 case 和 default 的 select 语句会无限期阻塞当前 goroutine。由于没有其他任务在执行,这会导致程序整体触发死锁(fatal error)。这在开发初期调试时经常作为测试手段,但生产环境中必须避免。

package main
import (
    "fmt"
    "time"
)
/**
* 没有内容的select 会阻塞
* 没有内容是指,没有case,也没有default
* 如果没有其它的任务指执行,将会触发死锁
*/
func main() {
    go func() {
        time.Sleep(2 * time.Second)
        fmt.Println("没有内容的select 会阻塞")
    }()
    select {}
}

第2步:掌握带有 Default 的非阻塞模式

如果 select 中只包含 default,它将不会阻塞。此时它的行为类似于串行化处理,立即执行默认分支。此外,当有多个 case 且都有数据可读时,Go 会随机选择一个 case 执行,而并非按照编写顺序。

package main
import "fmt"
/**
 只有default的select 和串行化没有区别
 */
func main() {
	go func() {
		fmt.Println("quick")
	}()
	select {
	default:
		fmt.Println("end")
	}
}

当既有 case 又有 default 时,若能匹配到 case 就执行对应的逻辑;若匹配不到(即没有数据准备),则执行 default。只要存在 default,select 就不会发生阻塞。

package main
import (
	"fmt"
)
func main() {
	ch1 := make(chan int, 2)
	ch2 := make(chan int, 2)
	select {
	case v1 := <-ch1:
		fmt.Println(v1)
	case v2 := <-ch2:
		fmt.Println(v2)
	default:
		fmt.Println(22)
	}
}
package main
import (
	"fmt"
	"time"
)
func main() {
	ch1 := make(chan int, 2)
	ch2 := make(chan int, 2)
	go func() {
		ch1 <- 1
	}()
	time.Sleep(1 * time.Second)
	select {
	case v1 := <-ch1:
		fmt.Printf("get v1 chan value %d", v1)
	case v2 := <-ch2:
		fmt.Printf("get v1 chan value %d", v2)
	default:
		fmt.Println(22)
	}
}

第3步:处理无 Default 时的长期阻塞

如果没有设置 default,select 将一直阻塞等待,直到某个 case 的数据就绪或被关闭。这种方式常用于需要“等到某件事发生”的场景。例如在下面的示例中,主 goroutine 会暂停约 3 秒,直到子进程向 ch1 发送数据后才会继续向下执行。

package main
import (
	"fmt"
	"time"
)
func main() {
	ch1 := make(chan int, 2)
	ch2 := make(chan int, 2)
	fmt.Printf("start unix:%d \n", time.Now().Unix())
	go func() {
		time.Sleep(3 * time.Second)
		ch1 <- 1
	}()
	select {
	case v1 := <-ch1:
		fmt.Printf("case unix: %d \n", time.Now().Unix())
		fmt.Printf("get v1 chan value %d \n", v1)
	case v2 := <-ch2:
		fmt.Printf("get v1 chan value %d \n", v2)
	}
	fmt.Println("end")
}

第4步:使用 For-Select 循环实现持续监听

注意:单个 select 块仅会被求值一次。如果需要在多个通道上持续不断地轮询数据,必须结合 for 循环使用。这是 Go 中实现消息队列消费端或事件驱动服务的基础模式。

package main
import (
	"fmt"
	"time"
)
func main() {
	ch1 := make(chan int, 2)
	ch2 := make(chan int, 2)
	fmt.Printf("start unix:%d \n", time.Now().Unix())
	go func() {
		for {
			time.Sleep(1 * time.Second)
			ch1 <- 1
		}
	}()
	for {
		select {
		case v1 := <-ch1:
			fmt.Printf("case unix: %d \n", time.Now().Unix())
			fmt.Printf("get v1 chan value %d \n", v1)
		case v2 := <-ch2:
			fmt.Printf("get v1 chan value %d \n", v2)
		}
	}
}

第5步:验证 Case 匹配的随机无序性

当 select 中有多个 case 同时满足条件(即通道均有数据)时,Go 选择哪个分支是伪随机决定的。可以通过循环多次调用测试来观察这一现象:

package main
import (
	"fmt"
	"time"
)
func test() {
	ch1 := make(chan int)
	ch2 := make(chan int)
	go func() {
		ch1 <- 1
		close(ch1)
	}()
	go func() {
		time.Sleep(1 * time.Second)
		ch2 <- 1
		close(ch2)
	}()
	time.Sleep(2 * time.Second)
	// 如果有顺序,那么因该每次都是v1
	select {
	case v1 := <-ch1:
		fmt.Printf("case unix: %d \n", time.Now().Unix())
		fmt.Printf("get v1 chan value %d \n", v1)
	case v2 := <-ch2:
		fmt.Printf("case unix: %d \n", time.Now().Unix())
		fmt.Printf("get v2 chan value %d \n", v2)
	}
}
func main() {
	for i := 0; i < 10; i++ {
		test()
	}
}
以上就是 Go 语言并发编程中关于 select 语句实现的详细内容。更多关于 Go 语言 golang 通道的运用技巧请关注本站其它相关文章!
相关标签:

本站声明

本文内容仅供参考,不代表本站观点。转载或引用请注明出处。如发现内容问题,请通过联系我们页面反馈。

Python 相关产品和应用

全部下载

专题列表