하마
5k
2019-02-08 15:15:21 작성 2019-02-08 15:24:17 수정됨
1
602

[Go언어] select 의 거의 모든 패턴들


go언어에서 가장 재밌으며, 강력한 키워드인 select/채널의 다양한 패턴을 살펴봄으로써 우리의 코딩력을 향상시켜 보겠습니다. 이러한 go특유의 핑퐁스타일의 코딩에 빠져들면 헤어나오기 힘들겁니다 :-)

switch 

먼저 형제 관계에 있는 switch를 통해 몸풀기를 좀 하구요.

1. switch (1)

// 일반 switch
func main(){
i := "korea"
switch i {
case "korea":
fmt.Println("korea")
case "usa":
fmt.Println("usa")
case "japan":
fmt.Println("japan")
}
}

switch문은 보통 우리가 생각하는 듯 그러합니다.

2. switch (2)

func main(){
t := time.Now()
switch {
case t.Hour() < 12:
fmt.Println("It's before noon")
default:
fmt.Println("It's after noon")
}
}

비교문을 케이스에 넣을 수도 있습니다.

3. switch (3)


func WhiteSpace(c rune) bool {
switch c {
case ' ', '\t', '\n', '\f', '\r':
return true
}
return false
}

케이스가 리스트가 될 수도 있구요.

4. switch (4)

func main(){

Loop:
for _, ch := range "a b\nc" {
switch ch {
case ' ': // skip space
break
case '\n': // break at newline
break Loop
default:
fmt.Printf("%c\n", ch)
}
}

}

// a
// b

break 문으로 switch문을 탈출 할 수도 있으며, 지정된 위치까지 탈출도 가능합니다. 위의 Loop로 탈출하면 for문을 벗어나게 됩니다.

select 

select문은 switch와 비슷하지만 case에 채널이 사용됩니다. 덕분에 동기화 코딩을 위해 매우 화려한 코딩을 할 수 있습니다. 예를 보면 바로 느낄 수 있을 꺼에요.

패턴-1


func main() {

c1 := make(chan string)
c2 := make(chan string)

go func() {
for{
time.Sleep(5 * time.Second)
c1 <- "one"
}
}()
go func() {
for{
time.Sleep(10 * time.Second)
c2 <- "two"
}
}()

for{
fmt.Println("start select------------------")
select {
case msg1 := <-c1:
fmt.Println("received", msg1)
case msg2 := <-c2:
fmt.Println("received", msg2)
}
fmt.Println("end select-------------------\n\n")
}
}

case 문에서 채널로부터의 이벤트를 받는데요, 여기서 중요한 것은 case 문의 채널에 값이 들어 올 때 까지 select문에서 블록된다는 점입니다. 

default:
fmt.Println("default")
}

하지만 이렇게 default문을 넣어주면 블록되지 않고 default문을 처리하고 다시 순회됩니다. 

패턴-2

package main

import (
"fmt"
"time"
)
func process(ch chan string) {
time.Sleep(10 * time.Second)
ch <- "process successful"
}

func scheduling(){
//do something
}
func main() {
ch := make(chan string)
go process(ch)
for {
time.Sleep(1 * time.Second)
select {
case v := <-ch:
fmt.Println("received value: ", v)
return
default:
fmt.Println("no value received")
}

scheduling()
}
}

어떤 생산자가 결과를 줄 때 까지 기다리는 방식의 코드를 구성 할 수 있습니다. idle 타임에는 주구장장 기다리는게 아니라 어떤 다른 로직을 수행 할 수 도 있을 겁니다. 물론 default가 빠지면 계속 기다리도록 할 수도 있을테구요.

패턴-3

func server1(ch chan string) {
ch <- "from server1"
}
func server2(ch chan string) {
ch <- "from server2"

}
func main() {
output1 := make(chan string)
output2 := make(chan string)
go server1(output1)
go server2(output2)
time.Sleep(1 * time.Second)
select {
case s1 := <-output1:
fmt.Println(s1)
case s2 := <-output2:
fmt.Println(s2)
}
}

case s1 이 선택될지, s2가 선택될지는 모릅니다. 랜덤 선택으로 사용 될 수 있습니다.

패턴-4


package main

import (
"fmt"
"time"
)


func consuming (scheduler chan string){
select {
case <- scheduler:
fmt.Println("이름을 입력받았습니다.")
case <-time.After(5 * time.Second):
fmt.Println("시간이 지났습니다.")
}
}

func producing(scheduler chan string){
var name string
fmt.Print("이름:")
fmt.Scanln(&name)
scheduler <- name
}

func main() {
scheduler := make(chan string)
go consuming(scheduler)
go producing(scheduler)

time.Sleep(100 * time.Second)
}
 case <- scheduler:

scheduler 채널로부터 무엇인가 받아 올 수 있다면 (즉 채널에 무엇인가 입력되었다면) 

 case <-time.After(5 * time.Second):

5초가 지났다면 

패턴-5


func main() {
scheduler := make(chan string, 1)
prompt := "HAMA"

select {
case scheduler <- prompt:
fmt.Println("이름은: ", <-scheduler)
case <-time.After(time.Second):
fmt.Println("시간이 지났습니다.")
}

time.Sleep(100 * time.Second)
}

채널에 값이 들어가는 것으로도 case 문을 충족시킵니다.

패턴-6

package main

import (
"fmt"
"time"
)

var scheduler chan string

func consuming (prompt string){ fmt.Println("consuming 호출됨")
select {
case scheduler <- prompt:
fmt.Println("이름을 입력받았습니다 : ", <- scheduler)
case <-time.After(5 * time.Second):
fmt.Println("시간이 지났습니다.")
}
}

func producing (console chan string) {
var name string
fmt.Print("이름:")
fmt.Scanln(&name)
console <- name
}
func main() {
console := make(chan string, 1)
scheduler = make(chan string, 1)

go func(){
consuming(<-console)
}()

go producing(console)

time.Sleep(100 * time.Second)
}

조금 더 응용하여, 아래와 같은 함수에 매개변수로 


func consuming (prompt string){
.. }

consuming(<-console)

이런식으로 넣어주는 방식도 있습니다. 참고로 이런 방식일 경우 <- console을 통하여 string 매개변수를 받지 못하는 동안에는 consuming 함수 자체가 블록됩니다. 

패턴-7

wg := sync.WaitGroup{}
errC := make(chan error)
done := make(chan bool, maxParallelFiles)
for i, entry := range list {
select {
case done <- true:
wg.Add(1)
case <-quitC:
return fmt.Errorf("aborted")
}
go func(i int, entry *downloadListEntry) {
defer wg.Done()
err := retrieveToFile(quitC, fs.api.fileStore, entry.addr, entry.path)
if err != nil {
select {
case errC <- err:
case <-quitC:
}
return
}
<-done
}(i, entry)
}

for문을 주구장창 도는게 아니라, list만큼만 돌면서 내부의 고루틴을 처리하는데, 위에 select문으로 스케쥴링을 하고 있습니다. done채널에는 maxParallelFiles 만큼만 true가 들어 갈 수 있기 때문에 처리량이 꽉 차면 case done <- true에서 블록되고 다음 고루틴을 시작하지 않게 됩니다. 고루틴 내부의 <- done 을 통해 버퍼가 해소되면 진행됩니다.


패턴-8

package main

import "fmt"
import "time"

func main() {

requestChan := make(chan chan string)

go goroutineC(requestChan)
go goroutineD(requestChan)

time.Sleep(time.Second)

}

func goroutineC(requestChan chan chan string) {

responseChan := make(chan string)
requestChan <- responseChan
response := <-responseChan

fmt.Printf("Response: %v\n", response)

}

func goroutineD(requestChan chan chan string) {

responseChan := <-requestChan
responseChan <- "wassup!"
}

채널에 채널을 넣습니다.
즉 생산자가 소비자한테 채널을 통해 데이터를 보내는게 아니라, 소비자가 생산자한테 이제 나 준비됬으니깐, 니가 만든 것을 내가 준 채널을 통해서 보내줘~ 라는 의미입니다. 적극적인 소비자지요. 

아래처럼 select문에서 처리 될 수 있습니다.

collectSig         chan chan string

collectSig는 string 타입의 채널의 채널로 정의됩니다.

// 리모트 peer로 부터 내 피어 정보가 요청됨.
func (p * Peer) collect() {

cs := make(chan string)
p.node.pm.collectSig <- cs
collected_peers := <- cs
....
}

collectSig에 string 타입의 채널을 태워 보냅니다.


func (pm * PeerManager) run(){
for {
select {

case collchan := <- pm.collectSig:
collchan <- pm.getCollect()
}
time.Sleep(10*time.Millisecond)
}
}

case문에서 collchan 을 통해 string타입의 채널을 받아서, 그 채널에 getCollect를 통해 얻은 데이터를 보내줍니다.

패턴-8

checkInterrupt := func() bool {
select {
case <-stop:
return true
default:
return false
}
}

stop 이라는 이벤트가 있으면 true 아니면 false를 반환하는 함수 

패턴-9

func (t *udp) handleReply(from NodeID, ptype byte, req packet) bool {
matched := make(chan bool, 1)
select {
case t.gotreply <- reply{from, ptype, req, matched}:
// loop will handle it
return <-matched
case <-t.closing:
return false
}
}

t.gotreply 채널에 reply 개체를 넣어줍니다. t.gotreply 채널의 버퍼가 충분해서 들어가면 matched 채널을 통해 데이터가 들어 올 때까지 기다렸다가 리턴해 줍니다. 아마도 t.gotreply 채널에 들어온 reply개체를 통해서 어떤 작업을 하다가 결과로 matched 채널에 값을 넣어 주겠지요. 대기중에 closing 이벤트가 먼저 날라오면 false를 리턴하네요. 

패턴-10


ticker := time.NewTicker(time.Millisecond * 50)

for {
select {
case <-ticker.C:

//... 50초에 한번씩 어떤 작업을 합니다 ...

case <-done:
return
}
}

주기적으로 실행되는 로직을 위한 코드입니다. done 채널을 통해 멈출 수도 있습니다.

패턴-11

select {
case <-notifier.Closed():
return
default:
}

함수 호출을 통해서 특정 채널을 리턴 받고, 그 채널에 값이 들어오는 순간을 기다릴수 있습니다.

패턴-12


func (p *Peer) handle(msg Msg) error {
switch {
case msg.Code == pingMsg:
msg.Discard()
go SendItems(p.rw, pongMsg)
case msg.Code == discMsg:
var reason [1]DiscReason
// This is the last message. We don't need to discard or
// check errors because, the connection will be closed after it.
rlp.Decode(msg.Payload, &reason)
return reason[0]
case msg.Code < baseProtocolLength:
// ignore other base protocol messages
return msg.Discard()
default:
// it's a subprotocol message
proto, err := p.getProto(msg.Code)
if err != nil {
return fmt.Errorf("msg code out of range: %v", msg.Code)
}
select {
case proto.in <- msg:
return nil
case <-p.closed:
return io.EOF
}
}
return nil
}

switch 문의 default 문 안에 select 를 위치 시킵니다. msg가 어떤 프로토콜을 위한 것인데 알아낸후에 해당 프로토콜의 in 채널에 msg를 넣어주고 있습니다.

패턴-13

func (rw *protoRW) WriteMsg(msg Msg) (err error) {

select {
case <-rw.wstart:
err = rw.w.WriteMsg(msg)
rw.werr <- err
case <-rw.closed:
err = ErrShuttingDown
}
return err
}

wstart 채널에 값이 들어와서 쓰기가 가능해지면 씁니다. 다 쓰고 난 후에는  werr 채널에 알려주어서 다음 쓰기가 가능하게 합니다. 이 코드에는 안보이지만 다음 쓰기가 가능하게 하는 방법은 wstart 채널에 값을 넣는 것입니다.

패턴-14

func (p *MsgPipeRW) WriteMsg(msg Msg) error {
if atomic.LoadInt32(p.closed) == 0 {
consumed := make(chan struct{}, 1)
msg.Payload = &eofSignal{msg.Payload, msg.Size, consumed}
select {
case p.w <- msg:
if msg.Size > 0 {
// wait for payload read or discard
select {
case <-consumed:
case <-p.closing:
}
}
return nil
case <-p.closing:
}
}
return ErrPipeClosed
}

select 안에 select가 들어 갈 수 도 있습니다. msg를 받아서, p.w채널에 넣어주는데 성공한후에 p.w 채널이 받아서 무엇인가 처리 할 때까지 다음 select문에서 <- consumed: 를 통해서 기다리게 됩니다. 이런 것은 보통 여러 모듈이 공평하게 OS의 I/O를 나누어 같기 위해 사용됩니다. framming이라고도하죠.

패턴-15

type peerOpFunc func(map[discover.NodeID]*Peer)
peerOp     chan peerOpFunc
// PeerCount returns the number of connected peers.
func (srv *Server) PeerCount() int {
var count int
select {
case srv.peerOp <- func(ps map[discover.NodeID]*Peer) { count = len(ps) }:
<-srv.peerOpDone
case <-srv.quit:
}
return count
}

채널에는 함수자체를 넣을 수 도 있습니다. 따라서 관심사의 분리/의존성 주입을 이렇게 할 수 있게 되지요.



레퍼런스:

https://github.com/ethereum/go-ethereum
https://golangbot.com/select/

2
3
  • 댓글 1

  • HANZO
    856
    2019-02-08 17:44:07

    항상 좋은 기술 자료에 감사 드립니다.

    0
  • 로그인을 하시면 댓글을 등록할 수 있습니다.