go select原理及使用场景

原理

select 控制结构中的 case

type scase struct {
	c    *hchan         // case中使用的channel
	elem unsafe.Pointer // 数据元素的指针
}

编译器在中间代码生成期间会根据 selectcase 的不同对控制语句进行优化,优化逻辑会在

// src/cmd/compile/internal/walk/select.go:31
func walkSelectCases(cases []*ir.CommClause) []ir.Node {

会有下面 4 种情况:

  1. select 不存在任何的 case

    此时会执行:

    func block() {
    	gopark(nil, nil, waitReasonSelectNoCases, traceEvGoStop, 1) // forever
    }
    

    导致 goroutine 永远阻塞,但传入的 waitReasonSelectNoCases 会使 goroutine 报错并结束程序:

     go run main.go
    fatal error: all goroutines are asleep - deadlock!
    
    goroutine 1 [select (no cases)]:
    main.main()
            /Users/user/go/src/go-demo/select/main.go:4 +0x17
    exit status 2
    
    
  2. select 只存在一个 case

  3. select 存在两个 case,其中一个 casedefault

    会变成 channel 的非阻塞收发操作:

    // 非阻塞收
    select {
    case <-ch:
    	println("case1")
    default:
    	println("default")
    }
    // 非阻塞发
    select {
    case ch <- 0:
    	println("case1")
    default:
    	println("default")
    }
    
  4. select 存在多个 case

案例

  1. 类似 c 语言的多路复用,可以同时监听多个 channel。当有 channel 可读可写之前会一直阻塞

    package main
    
    import "time"
    
    func main() {
    	ch := make(chan int)
    	go func() {
    		// 每5秒往channel写数据
    		for range time.Tick(5 * time.Second) {
    			ch <- 0
    		}
    	}()
    
    	for {
    		select {
    		case <-ch: // 阻塞等,直到channel有数据
    			println("case1", time.Now().String())
    		}
    	}
    
    }
    

    执行结果:

    go run main.go
    case1 2022-08-14 22:14:07.59665 +0800 CST m=+5.001220053
    case1 2022-08-14 22:14:12.596649 +0800 CST m=+10.001247928
    case1 2022-08-14 22:14:17.595772 +0800 CST m=+15.000400037
    
    
  2. case 中的表达式必须都是 channel 的收发操作

    package main
    
    import "time"
    
    func main() {
    	ch := make(chan int)
    	go func() {
    		for range time.Tick(5 * time.Second) {
    			ch <- 0
    		}
    	}()
    
    	for {
    		select {
    		case ch: // 此处会编译不通过
    			println("case1", time.Now().String())
    		}
    	}
    
    }
    

    执行结果:

    go run main.go
    # command-line-arguments
    ./main.go:15:3: select case must be receive, send or assign recv // 必须是收发的channel
    ./main.go:15:8: ch evaluated but not used
    
    
  3. select 中的两个 case 同时被触发时,会随机执行其中的一个

    package main
    
    import "time"
    
    func main() {
    	ch := make(chan int)
    	go func() {
    		for range time.Tick(time.Second) {
    			ch <- 0
    		}
    	}()
    
    	for {
    		select {
    		case <-ch:
    			println("case1")
    		case <-ch:
    			println("case2")
    		}
    	}
    
    }
    

    执行结果:

     go run main.go
    case1
    case1
    case1
    case1
    case1
    case1
    case2
    case1
    case2
    
    

    两个 case 都是同时满足执行条件的,如果我们按照顺序依次判断,那么后面的条件永远都会得不到执行,而随机的引入就是为了避免饥饿问题的发生

  4. 如果 select 控制结构中包含 default 语句,当存在可以收发的 Channel 时,直接处理该 Channel 对应的 case,当不存在可以收发的 Channel 时,执行 default 中的语句

    package main
    
    import "time"
    
    func main() {
    	ch := make(chan int)
    	go func() {
    		for range time.Tick(time.Second * 2) {
    			ch <- 0
    		}
    	}()
    
    	for {
    		select {
    		case <-ch:
    			println("case1")
    		default:
    			time.Sleep(time.Second)
    			println("default")
    		}
    	}
    
    }
    

    执行结果:

    go run main.go
    default
    default
    case1
    default
    default
    case1