1. 메모리에서의 안전요소

https://medium.com/a-journey-with-go/go-memory-safety-with-bounds-check-1397bef748b5

 

Go: Memory Safety with Bounds Check

ℹ️ This article is based on Go 1.13.

medium.com

https://insanitybit.github.io/2016/12/28/golang-and-rustlang-memory-safety

 

Golang and Rustlang Memory Safety - InsanityBit

I recently read an excellent blog post by Scott Piper about a tool he has released called Serene. The tool analyzes a binary to see if it has been compiled with security mitigation techniques - essentially a sanity check for best practices. As I was readin

insanitybit.github.io

* Go에서 디폴트로 지원하지 않아서 Go는 메모리세이프한 언어라고 하기 좀 머하다는 Address Space Layout Randomization (ASLR)이란?

https://blog.stalkr.net/2015/04/golang-data-races-to-break-memory-safety.html

 

Golang data races to break memory safety

Go is becoming more and more popular as a programming language and getting more scrutiny from a security point of view. You might remember m...

blog.stalkr.net


2. 웹서비스에서의 안전요소

https://blog.sqreen.com/top-6-security-best-practices-for-go/

 

Top 6 security best practices for Go - Sqreen Blog

In this post, we'll cover top six security practices that you need to consider when developing with Go and determine how to apply these practices.

blog.sqreen.com

3. 동시성에서의 안전요소

https://medium.com/@sargun/go-concurrency-considered-harmful-26499a422830

 

Go concurrency considered harmful

Go has been gaining a ton of popularity as of late. I’ve been using Go for work, working on container management software. Before that, I…

medium.com

https://medium.com/dm03514-tech-blog/golang-candidates-and-contexts-a-heuristic-approach-to-race-condition-detection-e2b230e70d08

 

Golang: Candidates and Contexts — A Heuristic Approach to Race Condition Detection

Concurrency In GO

medium.com

 


C++ 에서 최적화를 언급 할 때 "객체복사가 최대한 일어나지 않게 한다" 가 주요 화두를 차지 한다. ("알고리즘"을 개선하라도  중요하긴 하지만) 이에 따라서 RVO 같은 개념도 생겨나고, 아예 RVO를 믿지 못하고 매개변수에 리턴 받을 포인터를 전달하는 형태를 취하기도 한다. 

마찬가지로 Go에서도 아래 처럼 *peer,Proposal 같은 포인터를 넘겨 주면서 객체복사에 대한 낭비를 줄이는데

// GetProposal returns a Proposal message from its bytes
func GetProposal(propBytes []byte) (*peer.Proposal, error) {
	prop := &peer.Proposal{}
	err := proto.Unmarshal(propBytes, prop)
	return prop, errors.Wrap(err, "error unmarshaling Proposal")
}

사실 위의 코드를 C,C++개발자가 보면 식겁 하며 지역에서 할당된 객체의 포인터를 리턴 해 주면 이건 "죽은 자식 불알 만지기" 가 되는게 아니냐로 반문 할 수 있을 것이지만, Go언어에서는 외부로 레퍼런싱 되는 객체의 경우 힙에 할당해 주며 자동으로 관리 해 준다. (참고: Go Memory Management)

 

Go Memory Management

All computing environments must deal with memory management. This article discusses some memory management concepts used by the Go programming language. This article is written for programmers familiar with basic memory management concepts but unfamiliar w

dougrichardson.org

위에서 포인터와 레퍼런싱이라는 말을 혼용해서 사용했는데 Go에서 이 차이는 아래와 같다 (Go에서 C++식 reference는 없고, Pointer 와 Value만 있다고 생각하자) . https://spf13.com/post/go-pointers-vs-references

 

Pointers vs References : spf13.com

 

spf13.com

한글로 쉽게 설명한 블로그 발견 
https://jacking75.github.io/go_stackheap/

 

golang - 스택과 힙에 대해 - jacking75

실행 시 동적으로 메모리를 확보하는 영역으로서 스택과 힙이 있다. 스택 메모리는 함수 호출 스택을 저장하고 로컬 변수, 인수, 반환 값도 여기에 둔다. 스택의 Push와 Pop은 고속이므로 객체를 ��

jacking75.github.io


다음은 Go언어에서포인터를 사용해서 효율적인 코딩을 하려는 사람들에게 좋은 지침이 될 것 같은 글을 스택오버플로우에서 발견하여 공유하고자 한다. 해당 링크는 아래에 있다.  https://stackoverflow.com/questions/23542989/pointers-vs-values-in-parameters-and-return-values

 

Pointers vs. values in parameters and return values

In Go there are various ways to return a struct value or slice thereof. For individual ones I've seen: type MyStruct struct { Val int } func myfunc() MyStruct { return MyStruct{Val: 1} } ...

stackoverflow.com

3줄 요약) 

  • 리시버 포인터를 사용하는 메소드는 일반적으로 메소드는 리시버 포인터를 사용한다. the rule of thumb for receivers is, "확실하지 않으면 포인터를 사용하세요!!."
  • Slices, maps, channels, strings, function values, 와 interface values 는 내부에 포인터로 구현되 있으므로 굳이 포인터로 처리하는 것은 낭비이다.
  • 거대한 구조체와 변경되길 원하는 구조체에는 포인터를 사용하고, 그 밖에는 value로 넘겨라. 난데없는 변경은 (포인터를 통한) 혼란을 일으킨다. Immutablity 가 중요할때가 많다. 

'Go' 카테고리의 다른 글

안전하게 사용하는 Golang  (2) 2020.06.16
go microservices  (0) 2019.02.28
[Golang] recover 는 언제 사용하나?  (1) 2019.02.22
고성능을 위한 GO  (0) 2019.02.13
[이더리움에서 배우는 Go언어] select 의 거의 모든 패턴들  (1) 2019.02.08


Go언어는 예외처리가 없으며, 에러에 대해서 가장 가까운 위치에서 명시적으로 체크하고 넘어가는 것을 권장하는 언어 이다. 이 행위를 강제하진 않기 때문에 좀 더 단순하나 문제가 발생 할 소지를 없애기 위해 에러 체킹을 강제화 하는 언어(OCaml,Scala등)에 비해 안정성은 좀 떨어진다고 말 할 수도 있겠다. 하지만 이런 것은 팀의 코딩컨벤션으로 항상 체크하고 넘어가면 되는 문제로 생각 할 수도 있을 것이다. 

참고로 예외처리는 굉장히 어려운 주제이며 예외 처리에 대한 6가지 화두 이 글을 통해서 말한바 있다.

Panic 예제

func test () int {
   arr := [] int {}
   element := arr[5]
   return element
}

func main() {
   test()
   fmt.Println("smooth exit")

}
test함수에서 인덱스 범위 밖의 배열을 참조 했으므로, panic이 발생하여 정상종료되지 못하고 뻣어버리는데 
package main

import (
   "fmt"
)

func test() int {
   arr := [] int {}
   defer func() {
      v := recover()
      fmt.Println("recovered:", v)
   }()

   x := arr[5]
   return x
}

func main() {
   result := test()
   fmt.Printf("smooth exit %d", result)

}

이렇게 내부에 recover 를 해주면 panic 이 상쇄되며 리턴값으로 타입의 기본값이 들어가게 된다.

package main

import (
   "fmt"
   "runtime/debug"
)

func r() {
   if r := recover(); r != nil {
      fmt.Println("Recovered", r)
      debug.PrintStack()
   }
}

func a() {
   defer r()
   n := []int{5, 7, 4}
   fmt.Println(n[3])
   fmt.Println("normally returned from a")
}

func main() {
   a()
   fmt.Println("normally returned from main")
}

문제가 발생 했을 당시의 스택트레이스를 확인 할 수도 있을 것이다.
근데 여기서 고민이 그럼 모든 함수에 저런것을 해야한다고 생각하면 정내미가 떨어 질 수 있을 것이다.

그럼 언제 고의적 panic을 해줘야하나? 

1. 에러인가 예외인가?
2. 죽일 것 인가? 살릴 것 인가? 
3. 가까운 곳에서 즉시 처리할 것인가? 상위로 위임할 것인가?
4. 강제적으로 처리 시킬 것인가? 아닌가?  (체크드 / 언체크드) 
5. 모두 되돌릴 것인가? 이미 벌어진 일은 무시,포기할 것인가? 
6.  리턴으로 처리 또는 예외 개념 도입  

예전에 작성한 예외처리에 대한 화두 6가지를 보면, 2번이 이것에 해당되는 내용일 거 같다. 내용은 아래와 같은데 

최근 특정 솔루션에서 나는 예외에 대해 고민하길 포기해버렸다.  따라서 예외를 단순히
 프로그램이 죽지 않게 하기위해 예외처리를 한다. 잘 죽는 부분은 이렇다.

* JSON 변환을 하는 부분  
* 소켓 처리부분, 대부분의 io 부분. (외부의 파일이 어떻게 될지 모름)
* 널포인터가 없을거라고 확신하지 못하는 부분 모두 (이건 좀 너무 광범위한데 C++ 로 개발할때  ASSERT 로 일단 도배한다.) 
* 여러 쓰레드가 접근되는 부분   
* 사용자가 주는 값에 대해 신뢰해선 안되는 부분 
* 컬렉션 처리중 먼가 냄새가 나는 부분

뭐 더 많겠지만 요즘 내가 짜는 프로그램은 이런거 같다. 이렇게 써 놓고 보니 예외를 예외로 생각하지 않고 프로그램 시퀀스의 일부분으로 좀 더 오버해서 즉 if 문 처럼 생각하고 쓰는게 아닌가 싶기도 한데 좀 더 고찰해 볼 시간은 없었다.

암튼 위의 리스트에서 느껴지듯이 쓰레드,IO 등에서 문제가 생길 소지가 많은데 그래서 웹개발/서버개발에 있어서는 죽이지 않기 위해 예외처리를 하는 경우가 많은거 같다. "일처리 프로세스 과정이 복잡하지 않고 하나의 시퀀스에 대해 완료하지 못하더라도 큰 타격을 받지 않으며 어차피  다음에 시도하면 되는 것은 그냥 생활의 일부분이야 ~~그런 사소한 이유로 프로그램을 죽일 순 없어. 프로그램 죽는것은 내가 죽는것" 이런 마음가짐이었던 거다.

근데 그냥 빨리 죽여서 문제를 빨리 찾으려는 응용프로그램도 많으며 예러가 발생해선 절대 안되는 프로그램이 있다. 공장이나 의료쪽에서 사용되는 응용프로그램들이 그러한데 이런 프로그램에서는 저 위의 예처럼 모든 경우에 대해 예외처리를 하여 자신도 모르게 넘어가면 안된다. 예외가 로그에 남겠지만 그것만으로 부족하다. 개발자에게  문제가 생겼다는것을 즉시 각인 시켜야한다. 프로그램을 그 즉시 죽임으로써 말이다. 그리고 최대한 에러/예외가 나지 발생하지  않도록 설계를 해야하며 오류를 잡아야한다. 사실 이런 프로그램 경우 웹이나 게임처럼 외부와의 잦은 커뮤니케이션이 없고 안정된 하드웨어 등 시스템  때문에  예외가 발생할 가능성은 크게 적다.  

뭐 이런 글 이었는데 여기서 적용 할 수 있을 거 같다. 즉 계속 살아 있으면 안될때 Panic을 일으켜야 한다는 것이다. 먼가 문제가 있어도 걍 넘기거나 다시 시도해도 되는 많은 경우에는 필요 없을 것이다.  즉 초기에 중요한 설정을 하는데 실패 했을 경우 라든지, 중간에 입력된 상태를 기반으로 추후의 작업을 오래 동안 해야 할 경우. 중간에 입력된 값이 nil이라면 결과에 영향을 크게 미칠 것이고, 그 결과에 따라서 커다란 재산상/신체상 위협이 가해질 경우. 


그럼 언제 recover 를 해줘야하나? 

위와 다음과 같다고 보면 될 거 같다. Panic을 반드시 일으켜야 하는 상황이 아니면서 아래처럼 예외가 발생될 확률이 매우 높은곳으로 보면 될 거 같다.

* JSON 변환을 하는 부분  
* 소켓 처리부분, 대부분의 io 부분. (외부의 파일이 어떻게 될지 모름)
* 널포인터가 없을거라고 확신하지 못하는 부분 모두 (이건 좀 너무 광범위한데 C++ 로 개발할때  ASSERT 로 일단 도배한다.) 
* 여러 쓰레드가 접근되는 부분   
* 사용자가 주는 값에 대해 신뢰해선 안되는 부분 
* 컬렉션 처리중 먼가 냄새가 나는 부분

아래는 다양한 경우에 대한 예를 살펴보자. (해당 예제는 아래 레퍼런스에서 가져왔다)

예1) 프로그램 종료되는 것을 피하자.

굉장히 일반적인 경우의 예로  클라이언트-서버 동시성 프로그래밍에서 문제가 생겼을때 그냥 회피하는 방식이다.

package main

import "errors"
import "log"
import "net"

func main() {
	listener, err := net.Listen("tcp", ":12345")
	if err != nil {
		log.Fatalln(err)
	}
	for {
		conn, err := listener.Accept()
		if err != nil {
			log.Println(err)
		}
		// Handle each client connection in a new goroutine.
		go ClientHandler(conn)
	}
}

func ClientHandler(c net.Conn) {
	defer func() {
		if v := recover(); v != nil {
			log.Println("capture a panic:", v)
			log.Println("avoid crashing the program")
		}
		c.Close()
	}()
	panic(errors.New("just a demo.")) // a demo-purpose panic
}

클라이언트 하나 죽었다고, 모두다 죽으면 안되니깐~

예2) 고루틴 충돌시 자동적으로 재시작하기

고루틴에서 Panic이 감지되었을때 새로운 고루틴을 만들 수 있다.

package main

import "log"
import "time"

func shouldNotExit() {
	for {
		time.Sleep(time.Second) // simulate a workload
		// Simulate an unexpected panic.
		if time.Now().UnixNano() & 0x3 == 0 {
			panic("unexpected situation")
		}
	}
}

func NeverExit(name string, f func()) {
	defer func() {
		if v := recover(); v != nil { // a panic is detected.
			log.Println(name, "is crashed. Restart it now.")
			go NeverExit(name, f) // restart
		}
	}()
	f()
}

func main() {
	log.SetFlags(0)
	go NeverExit("job#A", shouldNotExit)
	go NeverExit("job#B", shouldNotExit)
	select{} // blocks here for ever
} 

예3) 롱점프 Statements 시뮬레이션을 위한panic/recover Calls

일반적으로 이러한 방법은 권장되지 않지만 때때로 우리는 crossing-function long jump statements 과 crossing-function returns 를 시뮬레이션하는 방법으로 패닉/회복을 사용할 수 있다. 이 방법은 코드 가독성과 실행 효율성 모두에 해를 끼친다. 유일한 이점은 때때로 코드를 덜 장황하게 보이게 할 수 있다는 것이다.

다음 예에서 내부 함수에 패닉이 발생하면 실행이 지연된 호출로 점프한다.

package main

import "fmt"

func main() {
	n := func () (result int)  {
		defer func() {
			if v := recover(); v != nil {
				if n, ok := v.(int); ok {
					result = n
				}
			}
		}()

		func () {
			func () {
				func () {
					// ...
					panic(123) // panic on succeeded
				}()
				// ...
			}()
		}()
		// ...
		return 0
	}()
	fmt.Println(n) // 123
}

예4) 에러체크를 줄이기 위한  panic/recover 콜 

package main

import "fmt"

func doTask(n int) {
	if n%2 != 0 {
		// Create a demo-purpose panic.
		panic(fmt.Errorf("bad number: %v", n))
	}
	return
}

func doSomething() (err error) {
	defer func() {
		// The second optional return must be present here,
		// otherwise, the assertion will panic if no errors occur.
		err, _ = recover().(error)
	}()

	doTask(22)
	doTask(98)
	doTask(100)
	doTask(53)
	return nil
}

func main() {
	fmt.Println(doSomething()) // bad number: 53
}

위 코드는 아래보다는 덜 장황하다.

func doTask(n int) error {
	if n%2 != 0 {
		return fmt.Errorf("bad number: %v", n)
	}
	return nil
}

func doSomething() (err error) {
	err = doTask(22)
	if err != nil {
		return
	}
	err = doTask(98)
	if err != nil {
		return
	}
	err = doTask(100)
	if err != nil {
		return
	}
	err = doTask(53)
	if err != nil {
		return
	}
	return
}

레퍼런스:

https://go101.org/article/panic-and-recover-use-cases.html
https://golangbot.com/panic-and-recover/

High Performance Go Workshop
- 항상 가능한 최고로 단순하게 코드를 짜라. 컴파일러는 일반적인 코드에 대해 옵티마이즈 된다. 
- 짧은 코드는 빠른 코드이다.
- 짧은 코드는 작은 코드이다,. CPU 캐쉬에 중요하다.
- 할당에 주의를 기울이고, 필요없는 할당을 피하기 위해 노력하라.
- 추측하지말고 보틀넥을 프로파일링 해봐라.

Going Infinite, handling 1M websockets connections in Go


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/ 

[이더리움으로 배우는 GO언어]  자료구조 & 컬렉션 

이번 글에서는 사실 이더리움하고 크게 상관없이 go 자료구조 기본에 대해서 알아 봅니다.

Array 

var a [5]int  // [0 0 0 0 0]
a[4] = 100 // [0 0 0 0 100]

5개의 비어있는 배열 생성과 값 삽입 

b := [5]int{1, 2, 3, 4, 5} // [1 2 3 4 5]

5개 요소가 들어있는 배열 생성 

a2 := [...]string{"USA", "China", "India", "Germany", "France"}
b2 := a2 // a copy of a is assigned to b
b2[0] = "Singapore"
fmt.Println("a is ", a2) // [USA China India Germany France]
fmt.Println("b is ", b2) // [Singapore China India Germany France]

5개 요소가 들어있는 배열 생성과 배열복사시 깊은 복사 이루어 지는 모습 

a3 := [...]float64{67.7, 89.8, 21, 78}
for i := 0; i < len(a3); i++ {
fmt.Printf("%d th element of a is %.2f\n", i, a[i])
}

sum := float64(0)
for i, v := range a3 {// i는 인덱스, v 는 값
fmt.Printf("%d the element of a is %.2f\n", i, v)
sum += v
}

순회를 도는 2가지 방식 

Slice

a4 := [5]int{76, 77, 78, 79, 80}
var b4 []int = a4[1:4] //creates a slice from a[1] to a[3]
b4[0] = 100
fmt.Println(a4) // [76 100 78 79 80]

배열을 가지고 슬라이스 생성 / 그렇게 만들어진 슬라이스는 배열과 레퍼런스로 연결되어 있다. 

i := make([]int, 5, 5)

make 를 이용한 슬라이스 생성 

cars := []string{"Ferrari", "Honda", "Ford"}
cars = append(cars, "Toyota") // [Ferrari Honda Ford Toyota]

append를 이용하여 슬라이스 뒤에 요소 추가 

cars := []string{"Ferrari", "Honda", "Ford"}
cars = append(cars[:0], "Toyota","KIA") // [Toyota KIA]
fmt.Println(cars)

append를 이용하여 새로운 슬라이스 생성. 

veggies := []string{"potatoes","tomatoes","brinjal"}
fruits := []string{"oranges","apples"}
food := append(veggies, fruits...)

append를 이용하여 슬라이스 뒤에 슬라이스 추가 ( 추가될 슬라이스 변수에 ... 를 추가한다) 

countries := []string{"USA", "Singapore", "Germany", "India", "Australia"}
neededCountries := countries[:len(countries)-2]
countriesCpy := make([]string, len(neededCountries))
countriesCpy2 := countries[:len(countries)-2]
copy(countriesCpy, neededCountries) //copies neededCountries to countriesCpy
countriesCpy[0] = "korea"
countriesCpy2[1] = "japan"

fmt.Println(countriesCpy) // 깊은복사 [korea Singapore Germany]
fmt.Println(countriesCpy2) // 얖은복사 [USA japan Germany]
fmt.Println(countries) // [USA japan Germany India Australia]

슬라이스의 일부분을 복사하는데 make로 만든 배열에 copy를 이용하여 복사하면 깊은 복사가 된다.

startTasks := func(ts []task) (rest []task) {
i := 0
for ; len(runningTasks) < maxActiveDialTasks && i < len(ts); i++ {
t := ts[i]
...
runningTasks = append(runningTasks, t)
}
return ts[i:]
}

현재 실행되고 있는 업무(runningTasks)가 정해진 숫자(maxActiveDialTasks) 를 넘지 않아서 대기중인 업무를 할 수 있는 상태일때,  매개변수로 받은 대기중인 업무들을 실행한다. 추가 실행한 업무는 runningTasks 에 append를 통해서 실행중인 업무로 추가되고, 실행업무에 추가되지 못한 업무들은 ts[i:] 로 리턴된다. 

scheduleTasks := func() {
queuedTasks = append(queuedTasks[:0], startTasks(queuedTasks)...)

if len(runningTasks) < maxActiveDialTasks {
nt := dialstate.newTasks(len(runningTasks)+len(queuedTasks), peers, time.Now())
queuedTasks = append(queuedTasks, startTasks(nt)...)
}
}

대기업무중인 업무들을 startTasks로 보내서 실행하게 하고, 실행이 안된 업무를 queuedTasks[:0] 같이 append 하여 다시 구성한다. 실행중인 업무가 maxActiveDialTasks 작을경우 새로운 업무들을 만들어서 (newTasks) startTasks로 실행한후에 남은 태스크는  queuedTasks 에 append한다.

Map

m := make(map[string]int)
m["first"] = 1
fmt.Println(m["first"]) // 1

키가 string 값이 int 은 맵을 make 를 통해 만들어서 추가/출력 한다.

var fileExtensions = map[string]string{
"Python": ".py",
"C++": ".cpp",
"Java": ".java",
"Golang": ".go",
"Kotlin": ".kt",
}
fmt.Println(fileExtensions) // map[Python:.py C++:.cpp Java:.java Golang:.go Kotlin:.kt]
delete(fileExtensions, "Kotlin")
delete(fileExtensions, "Javascript")
fmt.Println(fileExtensions) // map[Python:.py C++:.cpp Java:.java Golang:.go]

 delete 를 통해서 삭제한다.

s := map[int]string{5: "five", 2: "second"}
_, ok := s[5] // check for existence // ok true
_, ok2 := s[6] // check for existence // ok false

 키가 없을 경우에는 ok 가 false

var m1 = map[string]int{
"one": 1,
"two": 2,
"three": 3,
"four": 4,
"five": 5,
}

var m2 = m1
fmt.Println("m1 = ", m1) // map[one:1 two:2 three:3 four:4 five:5]
fmt.Println("m2 = ", m2) // map[one:1 two:2 three:3 four:4 five:5]

m2["ten"] = 10
fmt.Println("m1 = ", m1) // map[ten:10 one:1 two:2 three:3 four:4 five:5]
fmt.Println("m2 = ", m2) // map[ten:10 one:1 two:2 three:3 four:4 five:5]

맵끼리의 대입은 레퍼런스식으로 공유한다.

var personAge = map[string]int{
"Rajeev": 25,
"James": 32,
"Sarah": 29,
}

for name, age := range personAge {
fmt.Println(name, age)
}

 맵을 순회하는 방식은 range를 사용하여 key와 value를 추출한다.


MapSet("github.com/deckarep/golang-set")
이 자료구조는 도커,이더리움,쿠버네이트에서 사용되는 간단한 set 타입이다.

s := mapset.NewSet()
s.Add("Cooking")
s.Add("English")
s.Add("Math")
s.Add("Biology")
s.Add("Biology")
s.Add("Biology")
s.Add("Biology")
fmt.Printf(s.String()) // Set{Biology, Cooking, English, Math}

중복적재가 안되는 모습이구요~

s.Pop() // 아무거나 랜덤으로 하나 제거 합니다.

scienceSlice := []interface{}{"Biology", "Chemistry","Biology"}
scienceClasses := mapset.NewSetFromSlice(scienceSlice)
fmt.Printf(scienceClasses.String()) //Set{Biology, Chemistry}

슬라이스로부터 셋을 만드는 모습이구요~ 덤으로 중복요소가 없어졌습니다.

requiredClasses := mapset.NewSet()
requiredClasses.Add("Biology")
requiredClasses.Add("Cooking")

scienceSlice := []interface{}{"Biology", "Chemistry"}
scienceClasses := mapset.NewSetFromSlice(scienceSlice)

electiveClasses := mapset.NewSet()
electiveClasses.Add("Welding")
electiveClasses.Add("Music")

bonusClasses := mapset.NewSet()
bonusClasses.Add("Go Programming")
bonusClasses.Add("Python Programming")

allClasses := requiredClasses.Union(scienceClasses).Union(electiveClasses).Union(bonusClasses)
fmt.Println(allClasses) //Set{Python Programming, Biology, Cooking, Chemistry, Welding, Music, Go Programming}

Set 을 Union으로 합쳤습니다. 역시 중복요소는 제거됩니다.

//포함여부 검사?
fmt.Println(scienceClasses.Contains("Cooking")) //false

//과학클래스 과목을 제외한 모든 클래스는?
fmt.Println(allClasses.Difference(scienceClasses)) //Set{Music, Go Programming, Python Programming, Cooking, Welding}

//교집합
fmt.Println(scienceClasses.Intersect(requiredClasses)) //Set{Biology}

//How many bonus classes do you offer?
fmt.Println(bonusClasses.Cardinality()) //2

 Contains 로 포함여부 검사/Difference 로 포함그룹군 제외/ Intersect로 교집합추출/ Cardinality로 요소갯수

container/List

values := list.New()
// Add 3 elements to the list.
values.PushBack("bird")
values.PushBack("cat")
values.PushFront("snake")
// Add 100 elements at the front.
for i := 0; i < 20; i++ {
// Convert ints to strings.
values.PushFront(strconv.Itoa(i))
}

// Loop over container list.
for temp := values.Front(); temp != nil; temp = temp.Next() {
fmt.Println(temp.Value)
}

앞,뒤로 삽입할수 있는 리스트 

container/heap


// An Item is something we manage in a priority queue.
type Item struct {
value string // The value of the item; arbitrary.
priority int // The priority of the item in the queue.
// The index is needed by update and is maintained by the heap.Interface methods.
index int // The index of the item in the heap.
}

// A PriorityQueue implements heap.Interface and holds Items.
type PriorityQueue []*Item

func (pq PriorityQueue) Len() int { return len(pq) }

func (pq PriorityQueue) Less(i, j int) bool {
// We want Pop to give us the highest, not lowest, priority so we use greater than here.
return pq[i].priority > pq[j].priority
}

func (pq PriorityQueue) Swap(i, j int) {
pq[i], pq[j] = pq[j], pq[i]
pq[i].index = i
pq[j].index = j
}

func (pq *PriorityQueue) Push(x interface{}) {
n := len(*pq)
item := x.(*Item)
item.index = n
*pq = append(*pq, item)
}

func (pq *PriorityQueue) Pop() interface{} {
old := *pq
n := len(old)
item := old[n-1]
item.index = -1 // for safety
*pq = old[0 : n-1]
return item
}

// update modifies the priority and value of an Item in the queue.
func (pq *PriorityQueue) update(item *Item, value string, priority int) {
item.value = value
item.priority = priority
heap.Fix(pq, item.index)
}

// This example creates a PriorityQueue with some items, adds and manipulates an item,
// and then removes the items in priority order.
func main() {
// Some items and their priorities.
items := map[string]int{
"banana": 3, "apple": 2, "pear": 4,
}

// Create a priority queue, put the items in it, and
// establish the priority queue (heap) invariants.
pq := make(PriorityQueue, len(items))
i := 0
for value, priority := range items {
pq[i] = &Item{
value: value,
priority: priority,
index: i,
}
i++
}
heap.Init(&pq)

// Insert a new item and then modify its priority.
item := &Item{
value: "orange",
priority: 1,
}
heap.Push(&pq, item)
pq.update(item, item.value, 5)

// Take the items out; they arrive in decreasing priority order.
for pq.Len() > 0 {
item := heap.Pop(&pq).(*Item)
fmt.Printf("%.2d:%s ", item.priority, item.value)
// 05:orange 04:pear 03:banana 02:apple
}
}

heap을 이용해서 우선순위큐를 구현한 모습이구요~

container/ring

package main

import (
"container/ring"
"fmt"
"time"
)

func main() {
number := []string{"1", "2", "3", "4", "5"}

// 버퍼 생성
r := ring.New(len(number)) // 버퍼에 값 채움
for i := 0; i < r.Len(); i++ {
r.Value = number[i]
r = r.Next()
}
// 버퍼 순회하며 출력
r.Do(func(x interface{}) {
fmt.Println(x)
})


        // 버퍼 무한 순회하며 출력
for _ = range time.Tick(time.Second * 1) {
fmt.Println(r.Value)
r = r.Next()
}
}

Do를 통해서 한바퀴 순회하고, Next()를 통해서 무한이 순회할 수 있는 환영큐입니다.

컴포지트(composite) 디자인 패턴과 이름이 헷갈린 컴포지션은 UML 측면에서는 연관(Aggregation) 하고도 헷갈리기도 하는데 이 글에서는 컴포지션과 연관을 구분하지 않겠다. 컴포지션이란 간단히 말해 내가(객체)가 가지고 있어야 하는 특성을 외부에서 가져오는 것을 말하는데, 가져오는 방식이 상속을 통하는 방법과 다르게 외부에서 주입되는 방식이다. 상속의 경우는 폴리모피즘이 반드시 필요하면 제한적으로 사용하되, 컴포지션을 통해서 객체를 구축하는 방식을 추천한다. 

이 글에서는 Golang에서 컴포지션을 어떻게 지원하는지 살펴 볼 것이다. 자바/C++ 보다는 훨씬 깊숙히 임베디드 되는 모습을 보게 될 것이란 것을 미리 귀뜸해 둔다.

먼저 자바의 컴포지션을 살펴보자. 

abstract class Form{
public void run(){};
}
class WolfForm extends Form{
public void run (){
System.out.println("늑대처럼 달린다!!!");
}
}
class Player{
public String name;
public Form form;
public Player(String name, Form form){
this.name = name;
this.form = form;
}
}
public class Main {
public static void main(String[] args) {
Player player = new Player("hama", new WolfForm());
player.form.run();
}
}

Player 객체는 Wolf 폼 객체를 포함(컴포지션)하고 있으며, Player객체에서 Wolf폼 객체의 기능을 사용하려면 당연히player.wolf.run() 식으로 내부에서 wolf 로 접근한 후에 사용한다. 근데  Golang에서는??
그냥 player.run()으로 접근가능하다.

Golang에서의 컴포지션 

*  객체/값의 구분을 하지 않았음을 알려드린다. 이 글에서는 포인팅되는지 값 자체인지의 구분이 중요치 않다.

type Form interface{
Run()
}
type WolfForm struct{
}
func (wolf *WolfForm) Run() {
fmt.Print("늑대처럼 달린다!!!");
}
type Player struct{
Form
}
func main(){
player := &Player{Form : &WolfForm{}}
player.Run()
}

Player 구조체는 Form을 혼연일체 (임베디드) 되어 가지고 있게 된다. 즉 자신의 메소드인 양 그냥 사용 해 버릴수 있다. 어찌보면 강력해 보이기도 하는데, 이게 코드읽기/관리가 될 때는 좀 피곤 할 수 있을 것이다. 
자바의 경우 player.wolf.run()으로 관계가 명시적으로 코드에 보이는 반면에, Golang식의 player.run()은 도대체 run은 어디에 구현되어있는지 알수 없으면, 찾아보는 수고를 해야한다. player 내부의 Form 인터페이스를 확인해야하며, 이 Form인터페이스를 덕타이핑으로 구현상속한 녀석을 또 찾아봐야한다. player 내부에 Form인터페이스 말고 다른것들도 있다면?? 두배로 힘들어지게 된다. 아래 이더리움 코드를 통해 피부로 느껴보자.

이더리움코드에서의  컴포지션 
[이더리움 코어] DevP2P 소스코드 분석 (feat. golang) 를 같이 읽으면  좋을 거 같다.

이더리움에서 P2P 부분을 보면 리모트서버와 접속이 이루어짐과 동시에 conn 객체가 만들어 지는데,
conn 구조체는 아래와 같다. 위에 공부해 보았듯이 transport 가 컴포지션 되었다는 것을 알 수 있다.

type conn struct {
fd net.Conn
transport
flags connFlag
cont chan error
id discover.NodeID
caps []Cap
name string
}

conn 객체에는 net.Conn이라는 소켓파일디스크립터과 그 fd 를 가지고transporting 하는 방식을 책임지는 transport 인터페이스를 구현상속한 녀석을 가지고 있다. 그럼 transport는 무엇을 하는 것일까? transport 인터페이스를 살펴보자.

type transport interface {
doEncHandshake(prv *ecdsa.PrivateKey, dialDest *discover.Node)(discover.NodeID, error)
doProtoHandshake(our *protoHandshake) (*protoHandshake, error)
MsgReadWriter
close(err error)
}

리모트노드와 접속이 이루어지면 Encrypt와 Protocol 정보를 교환해야하는 책임을 가지고 있으며, 
MsgReadWriter 인터페이스를 구현상속한 또 어떤 녀석을 통해서 리모트노드와 읽고,쓰기를 하는거 같다.
이더리움에서는 메세지를 시리얼라이즈 하는데  RLP 라는 인코딩/디코딩 알고리즘을 사용하는데, MsgReadWriter 인터페이스를 구현한 녀석은 RLP의 능력도 가지고 있다. 

이제 conn 객체의 생성 코드를 살펴보자. 확인해 볼것은 2가지인데 하나는 transport 인터페이스는 어떤 객체로 만들어지나이고,  transport 인터페이스가 가지고 있는 MsgReadWriter 인터페이스 또한 어떤 객체로 만들어 지나이다.

func (srv *Server) SetupConn(fd net.Conn, flags connFlag, dialDest *discover.Node) error {
...
c := &conn{fd: fd, transport: srv.newTransport(fd), flags: flags, cont: make(chan error)}
...
}

transport 에 srv.newTransport(fd) 가 컴포지션 되고 있다.

srv.newTransport = newRLPX

func newRLPX(fd net.Conn) transport {
return &rlpx{fd: fd}
}

rlpx 객체를 만드는 것 같다. (내부에 fd 를 가지고 있으며, rlp 엔코딩/디코딩으로 시리얼라이즈를 한 후에 fd로 소켓read/write호출)  

type rlpx struct {
fd net.Conn

rmu, wmu sync.Mutex
rw *rlpxFrameRW
}

rlpx 구조체는 위와 같으며, 이 구조체는 transport 인터페이스가 가지고 있는 메소드를 모두 덕타이핑(자바처럼 extends로 명시적으로 상속한다고 말하지 않아도 동일한 함수시그니처를 구현하고 있으면 폴리모피즘적으로 같다라고 봄) 으로 포함 할 것이다. 그래야만 rlpx 가 transport가 될 수 있으니~ 아래와 같이~

func (t *rlpx) doProtoHandshake(our *protoHandshake) (their *protoHandshake, err error) {

....서로가 가진 프로토콜 정보 확인 ...난 eth63을 가지고있는데 넌 무엇을?
}

func (t *rlpx) doEncHandshake(prv *ecdsa.PrivateKey, dial *discover.Node) (discover.NodeID, error) {
....서로가 가진 암호화 정보 확인 ECC기반...auth정보 교환..
t.rw = newRLPXFrameRW(t.fd, sec)
...
}

마지막으로 transport 인터페이스에서 선언한 MsgReadWriter 또한 transport를 구현상속한 rlpx 가 가지고 있어야 하는데 어디 있을까? 다시 rlpx 구조체를 보자.

type rlpx struct {
fd net.Conn

rmu, wmu sync.Mutex
rw *rlpxFrameRW
}

rlpxFrameRW가 뭔가 읽고,쓰기와 관련되어 있는 감이 오지 않나? (자바라면 명시적으로 보였을텐데..) 
그럼rlpxFrameRW가 생성되는 곳을 살펴봐야한다. 이것은 위의 doEncHandshake 메소드 안에 있다.

t.rw = newRLPXFrameRW(t.fd, sec)

newRLPXFrameRW 는 이렇다.

func newRLPXFrameRW(conn io.ReadWriter, s secrets) *rlpxFrameRW {
...
return &rlpxFrameRW{
conn: conn,
enc: cipher.NewCTR(encc, iv),
dec: cipher.NewCTR(encc, iv),
macCipher: macc,
egressMAC: s.EgressMAC,
ingressMAC: s.IngressMAC,
}
}

rlpxFrameRW 라는 구조체를 만든다. 이쯤되면 감 잡힐 것이다. 이 구조체는 MsgReadWriter 인터페이스를 구현 상속 했다는 것을..(RW라는 이름을 통해서 유추해야한다. 따라서 Golang에서 이름짓기는 더 중요할 것이다) 

type MsgReadWriter interface {
MsgReader
MsgWriter
}}

type MsgReader interface {
ReadMsg() (Msg, error)
}
type MsgWriter interface {
WriteMsg(Msg) error
}

MsgReadWriter 인터페이스는 MsgReader/MsgWriter라는 인터페이스를 또  가지고 있으며, rlpx 에서는 ReadMsg/WriteMsg만 덕타이핑해 놓으면 될 것이다. 아래 코드에서 확인하자.

func (t *rlpx) ReadMsg() (Msg, error) {
t.rmu.Lock()
defer t.rmu.Unlock()
t.fd.SetReadDeadline(time.Now().Add(frameReadTimeout))
return t.rw.ReadMsg()
}

func (t *rlpx) WriteMsg(msg Msg) error {
t.wmu.Lock()
defer t.wmu.Unlock()
t.fd.SetWriteDeadline(time.Now().Add(frameWriteTimeout))
return t.rw.WriteMsg(msg)
}

이렇게 이더리움에서 컴포지션이 이루어지는 모습을 살펴보았다. 어떻게 느껴졌는가?? 강력하다? 헷갈린다?? 모든 기술은 장,단이 있으며 사람의 취향에 따라서 달라질 것이다. 자바스러움이 좋은 사람, Golang스러움이 좋은 사람~개인적으로는 자바스러움이 좋긴한데, Golang의 동시성  철학과 빠른컴파일속도에 점수를 주어서 Golang에 집중하고 있다. 언어 선택에 더 중요한것은 내가 지금 하는 일에 어떤 언어가 더 가깝냐는것이 겠지만~



+ Recent posts