관리 메뉴

HAMA 블로그

[이더리움에서 배우는 Go언어] select 의 거의 모든 패턴들 본문

Go

[이더리움에서 배우는 Go언어] select 의 거의 모든 패턴들

[하마] 이승현 (wowlsh93@gmail.com) 2019. 2. 8. 11:05

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/ 

Comments