일단 WSGI는 파이썬에만 해당이 됩니다. Go에는 3가지 옵션이 있습니다. (실제로는 4가지지만 일반적으로 CGI는 높은 부하로 인해 뺐습니다.)

Go의 표준 라이브러리에 내장 된 HTTP 서비스 기능.

이 경우 앱은 독립 실행형 서버입니다. 가장 간단한 설정 일 수도 있지만 다음과 같은 문제가 있을 수 있습니다. 

  • 다운그레이드 권한으로 권한이 부여된 포트(1024 아래의 포트, 예를들어 80포트)를 가진 앱을 실행하려면, 특별한 wrapper나 POSIX 기능을 사용해야합니다.
  • 연결을 끊지 않고 재배포를 하려면 goagain과 같은 다른 wrapper가 필요합니다.

웹 서버 형태의 reverse HTTP proxy 문제가 존재

대부분 독립 실행형의 다양한 문제를 해결하지만, 그래도 전체 HTTP 트래픽을 이리저리 전달하는 오버 헤드가 존재합니다.

FastCGI는 적절한 웹 서버를 통해 제공

FastCGI는  Nginx와 Apache와 같은 웹서버도 연결이 가능합니다. FCGI 클라이언트 구현은 Go 표준 라이브러리에서 사용할 수 있습니다. 


독립 실행형 서버 실행의 문제가 없는 것 이외로 효율적인 데이터 교환 프로토콜을 구현합니다. 또 다른 보너스는 Go 서버가 reverse HTTP proxy과 관련된 TCP 소켓보다 전송 비용이 낮은 Unix 파이프를 사용하여 front-end 웹 서버와 통신 할 수 있다는 것입니다. 결론적으로 WSGI나 CGI를 생각하지 말고 FCGI를 구현하는 것이 좋습니다.


많이 사용하는 라이브러리, ORM, 웹 프레임워크 등 간단한 설명과 github 주소가 링크되어 있는 사이트

https://awesome-go.com/

CORS 정책상 Access-Control-Allow-Origin은 1개만 등록이 가능합니다. Origin을 *로 할 경우 전 URL에 대해 허용이 가능하지만 아래와 같은 경우면 특정 URL을 지정해야 합니다.

c.Header("Access-Control-Allow-Credentials", "true")
c.Header("Access-Control-Allow-Origin", "*") // 에러 - Credentials이 true일 경우 특정 URL 1개만 허용


필자가 사용한 방법은 허용할 URL 리스트를 만든 다음, 요청된 URL이 해당 리스트에 맞는지 체크하는 형식으로 문제를 해결했습니다.

allowUrlList := []string{"http://foo.com", "http://bar.com", "http://zoo.com"}
var allowUrl string
for _, url := range allowUrlList {
if c.Request.Header.Get("Origin") == url {
allowUrl = url
break
}
}

c.Header("Access-Control-Allow-Headers", "Content-Type, Authorization, Origin")
c.Header("Access-Control-Allow-Credentials", "true")
c.Header("Access-Control-Allow-Origin", allowUrl)


간단한 문젠대..

GET

 gin-gonic 웹 프레임워크에서 GET은 .Query()함수로 query parameter로 넘어온 데이터를 받습니다.

http://foo.com?id=1234
 
...
a := c.Query(id)

POST

POST의 경우 body로 넘어온 데이터를 받습니다.

http://foo.com
# body 부분
{id:1234}
 
...
a := c.PostForm("id")

DELETE

gin-gonic에서 DELETE 메소드는 무조건 query parameter로 데이터를 넘겨야합니다. body로 데이터를 넘겨줄 경우 이를 인식하지 못합니다.

http://foo.com
# body 부분
{id:1234}


...
a := c.PostForm("id") // nil 


http://foo.com?id=1234


...
a := c.Query(id) // 1234


'언어 > GO' 카테고리의 다른 글

[GO] 유용 사이트  (0) 2017.03.22
[GO] gin-gonic 헤더 Access-Control-Allow-Origin 여러개 추가 방법  (0) 2017.03.20
[GO] gin-gonic DELETE 메소드 데이터 호출방법  (0) 2017.03.17
[GO] gin-gonic CORS 문제 해결방법  (0) 2017.03.17
[GO] 채널  (0) 2017.03.02
[GO] 고루틴  (0) 2017.02.22

먼저 CORS에 대한 개념이 어느정도 존재한다는 가정하에 진행합니다.

TEST 환경

자바스크립트 -> Go Gin-gonic API를 요청하는 상황입니다.

Request Headers

var req = new XMLHttpRequest();
req.setRequestHeader("Content-type", "application/x-www-form-urlencoded");
req.withCredentials = true;
req.setRequestHeader("Authorization", "Bearer "+$.cookie("AUTH_A_TOKEN"));

Response Headers

c.Header("Access-Control-Allow-Headers", "Content-Type, Authorization, Origin")
c.Header("Access-Control-Allow-Credentials", "true")
c.Header("Access-Control-Allow-Origin", "http://foo.com:8080")
c.Header("Access-Control-Allow-Methods", "GET, DELETE, POST")

주의사항

위 Response Headers의 환경은 OPTIONS에만 나열하면 절대 안됩니다. OPTIONS에만 작성하고 GET, POST 등 메인 메소드에 작성하지 않을 경우 CORS문제가 발생했다고 에러가 계속나오게 됩니다. (몇 시간헤맴...) 

자세하게 Simple Request의 경우에는 서버에서 요청을 1번만 하고 요청에 대한 응답을 1번만 하게되어 OPTIONS 메서드를 호출하지 않습니다. (즉, GET을 요청했을 경우 GET에 대한 요청과 응답이 1번씩만 출력됩니다.) 하지만 Simple Request가 아닌 모든 경우에는 Preflight Request로 요청이 되는데 이때는 예비요청, 예비응답, 본 요청, 본 응답 으로 요청 2번, 응답 2번을 하게 됩니다. 예비 요청은 서버에서 구현하지 않아도 자동적으로 OPTIONS 메서드를 호출하게되고 해당 응답이 정상일 경우 본 요청을 하게 됩니다. (즉, GET을 요청하면 먼저 OPTIONS 요청, OPTIONS 응답 한 다음 정상적인 응답일 경우에 GET 요청, GET 응답이 됩니다.) 따라서 OPTIONS와 본 요청 메서드에 response header환경을 전부 적용해야 합니다.

해결법

위 header옵션들을 사용할 메소드마다 전부 나열해줍니다.

func main() {
	router = gin.Default()
	router.GET("/test", func(c *gin.Context) {
	    c.Header("Access-Control-Allow-Headers", "Content-Type, Authorization, Origin")
		c.Header("Access-Control-Allow-Credentials", "true")
		c.Header("Access-Control-Allow-Origin", "http://foo.com:8080")
		c.Header("Access-Control-Allow-Methods", "GET, DELETE, POST")
		c.Next()
	   if http.StatusOK == 200 {
	      ...
	      c.JSON(200, gin.H{
	         "status":  http.StatusOK,
	         "message": "ㅁ",
	         "data":    nil,
	      })
	   }
	})

	router.POST("/test", func(c *gin.Context) {
		c.Header("Access-Control-Allow-Headers", "Content-Type, Authorization, Origin")
		c.Header("Access-Control-Allow-Credentials", "true")
		c.Header("Access-Control-Allow-Origin", "http://foo.com:8080")
		c.Header("Access-Control-Allow-Methods", "GET, DELETE, POST")
		c.Next()
	   if http.StatusOK == 200 {
	      ...
	      c.JSON(200, gin.H{
	         "message":"success",
	         "status":http.StatusOK,
	         "data":nil,
	      })
	   }
	})
	router.OPTIONS("/test", func(c *gin.Context) {
	   c.Header("Access-Control-Allow-Headers", "Content-Type, Authorization, Origin")
	   c.Header("Access-Control-Allow-Credentials", "true")
	   c.Header("Access-Control-Allow-Origin", "http://foo.com:8080")
	   c.Header("Access-Control-Allow-Methods", "GET, DELETE, POST")
	   c.AbortWithStatus(204)
	})
	router.Run(":8081")
}


위 코드는 잘 동작하지만 중복되는 코드가 계속해서 포함됩니다. 아래와 같이 깔끔하게 변경할 수 있습니다.

func main() {
	router = gin.Default()
	router.Use(CORSMiddleware())
	router.GET("/test", func(c *gin.Context) {
	   if http.StatusOK == 200 {
	      ...
	      c.JSON(200, gin.H{
	         "status":  http.StatusOK,
	         "message": "ㅁ",
	         "data":    nil,
	      })
	   }
	})

	router.POST("/test", func(c *gin.Context) {
	   if http.StatusOK == 200 {
	      ...
	      c.JSON(200, gin.H{
	         "message":"success",
	         "status":http.StatusOK,
	         "data":nil,
	      })
	   }
	})
	router.Run(":8081")
}
 

func CORSMiddleware() gin.HandlerFunc {
    return func(c *gin.Context) {
        c.Header("Access-Control-Allow-Headers", "Content-Type, Authorization, Origin")
      c.Header("Access-Control-Allow-Credentials", "true")
      c.Header("Access-Control-Allow-Origin", "http://foo.com:3000")
      c.Header("Access-Control-Allow-Methods", "GET, DELETE, POST")

        if c.Request.Method == "OPTIONS" {
            c.AbortWithStatus(204)
            return
        }

        c.Next()
    }
}


Go 채널은 그 채널을 통하여 고루틴끼리 데이터를 주고 받는 통로라 볼 수 있는데, 채널은 make(chan 자료형) 함수를 통해 미리 생성되어야 하며, 채널 연산자 <- 을 통해 데이타를 보내고 받습니다. 채널은 흔히 goroutine들 사이 데이터를 주고 받는데 사용되는데, 상대편이 준비될 때까지 채널에서 대기함으로써 별도의 lock을 걸지 않고 데이타를 동기화하는데 사용됩니다. 채널은 실행 흐름을 제어하는 기능이 있고 모든 타입을 채널로 사용할 수 있습니다(이 때문에 일급시민(first class citizen)이라 할 수 있음). 그리고 채널 자체는 값이 아닌 레퍼런스 타입입니다. 채널을 그림으로 표현하면 다음과 같습니다.

아래 예제는 정수형 채널을 생성하고, 한goroutine 에서 그 채널에 123이란 정수 데이터를 보낸 후, 이를 다시 메인 루틴에서 채널로부터 123 데이터를 받는 코드입니다. 채널을 생성할 때는 make() 함수에 어떤 타입의 데이터를 채널에서 주고 받을지를 미리 지정해 주어야 합니다. 채널로 데이터를 보낼 때는 채널명 <- 데이터 와 같이 사용하고, 채널로부터 데이터를 받을 경우는 <- 채널명 와 같이 사용합니다. 

package main
 
func main() {
  // 정수형 채널을 생성
  ch := make(chan int)
 
  go func() {
    ch <- 123   //채널에 123을 보냄
  }()
 
  var i int
  i = <- ch  // 채널로부터 123을 받음
  println(i)
}


아래는 고루틴과 채널을 이용하여 두 정수 값을 더하는 예제입니다.

package main

import "fmt"

func sum(a int, b int, c chan int) {
	c <- a + b          // 채널에 a와 b의 합을 보냄
}

func main() {
	c := make(chan int) // int형 채널 생성

	go sum(1, 2, c)     // sum을 고루틴으로 실행한 뒤 채널을 매개변수로 넘겨줌

	n := <-c            // 채널에서 값을 꺼낸 뒤 n에 대입
	fmt.Println(n)      // 3
}

채널을 사용하기 전에는 반드시 make 함수로 공간을 할당해야 합니다. 그리고 이렇게 생성하면 동기 채널(synchronous channel)이 생성됩니다.

아래는 고루틴A와 고루틴B 간 채널로 통신하는 그림입니다.

동기 채널

Go 채널은 수신자와 송신자가 서로를 기다리는 속성때문에, 이를 이용하여 (다음 예제와 같이) Go루틴이 끝날 때까지 기다리는 기능을 구현할 수 있습니다. 즉, 익명함수를 사용한 한 Go 루틴에서 어떤 작업이 실행되고 있을 때, 메인루틴은 <-done 에서 계속 수신하며 대기하고 있게 됩니다. 익명함수 Go 루틴에서 작업이 끝난 후, done채널에 true를 보내면, 수신자 메인루틴은 이를 받고 프로그램을 끝내게 됩니다.

package main
 
import "fmt"
 
func main() {
    done := make(chan bool)
    go func() {
        for i := 0; i < 10; i++ {
            fmt.Println(i)
        }
        done <- true
    }()
 
    // 위의 Go루틴이 끝날 때까지 대기
    <-done
}


다음예제는 고루틴과 메인함수를 번갈아가면서 실행하는 예제입니다.

package main

import (
	"fmt"
	"time"
)

func main() {
	done := make(chan bool) // 동기 채널 생성
	count := 3              // 반복할 횟수

	go func() {
		for i := 0; i < count; i++ {
			done <- true                // 고루틴에 true를 보냄, 값을 꺼낼 때까지 대기
			fmt.Println("고루틴 : ", i) // 반복문의 변수 출력
			time.Sleep(1 * time.Second) // 1초 대기
		}
	}()

	for i := 0; i < count; i++ {
		<-done                         // 채널에 값이 들어올 때까지 대기, 값을 꺼냄
		fmt.Println("메인 함수 : ", i) // 반복문의 변수 출력
	}
}

make(chan bool)처럼 채널을 생성합니다. 여기서는 채널로 값을 주고 받아도 실제로 사용하지 않으므로 자료형은 큰 의미가 없습니다. make 함수에 매개 변수를 하나만 지정했으므로 동기 채널을 생성됩니다. 비동기 채널과 버퍼는 뒤에서 설명하겠습니다.

다음 고루틴 내 for문에서 먼저 고루틴을 생성하고, 반복문을 실행할 때마다 채널 done에 true 값을 보낸 뒤 1초를 기다립니다.

동기 채널이므로 done에 값을 보내면 다른 쪽에서 값을 꺼낼 때까지 대기합니다. 따라서 반복문도 실행되지 않으므로 “고루틴 : 숫자”가 계속 출력되지 않습니다. 이제 메인 함수에서는 일반 반복문을 실행할 때마다 채널 done에서 값을 꺼냅니다.

<-done 부분에서 채널에 값이 들어올 때까지 대기합니다. 먼저 앞의 고루틴에서 done에 값을 보냈기 때문에 값을 꺼낸 뒤 다음 코드로 진행합니다. 그리고 고루틴 쪽의 대기도 종료되면서 다시 반복문이 실행된 뒤 채널에 값을 보냅니다. 그리고 메인 함수는 채널에서 값을 꺼내고, 다시 고루틴도 채널에 값을 보냅니다. 따라서 다음과 같이 고루틴 → 메인 함수 → 고루틴 → 메인 함수 순서로 실행됩니다.

// 결과 고루틴 : 0 메인 함수 : 0 고루틴 : 1 메인 함수 : 1 메인 함수 : 2 고루틴 :


동기 채널은 보내는 쪽에서는 값을 받을 때까지 대기하고, 받는 쪽에서는 채널에 값이 들어올 때까지 대기합니다. 따라서, 동기 채널을 활용하면 고루틴의 코드 실행 순서를 제어할 수 있습니다.

채널 버퍼링

Go 채널은 2가지의 채널이 있는데, Unbuffered Channel과 Buffered Channel이 있습니다. 위에서 설명한 예제에서의 Go 채널은 Unbuffered Channel로서 이 채널에서는 하나의 수신자가 데이타를 받을 때까지 송신자가 데이터를 보내는 채널에 묶여 있게 됩니다. 하지만, Buffered Channel을 사용하면 비록 수신자가 받을 준비가 되어 있지 않을 지라도 지정된 버퍼만큼 데이터를 보내고 계속 다른 일을 수행할 수 있습니다. 버퍼 채널은 make(chan type, N) 함수를 통해 생성되는데, 두번째 파라미터 N에 사용할 버퍼 개수를 넣습니다. 예를 들어, make(chan int, 10)은 10개의 정수형을 갖는 버퍼 채널을 만듭니다.

다음은 채널의 버퍼가 가득차면 값을 꺼내서 출력하는 예제입니다.

package main
import (
	"fmt"
	"runtime"
)
func main() {
	runtime.GOMAXPROCS(1)
	done := make(chan bool, 2) // 버퍼가 2개인 비동기 채널 생성
	count := 4                 // 반복할 횟수
	go func() {
		for i := 0; i < count; i++ {
			done <- true             // 채널에 true를 보냄, 버퍼가 가득차면 대기
			fmt.Println("고루틴 : ", i) // 반복문의 변수 출력
		}
	}()
	for i := 0; i < count; i++ {
		<-done                     // 버퍼에 값이 없으면 대기, 값을 꺼냄
		fmt.Println("메인 함수 : ", i) // 반복문의 변수 출력
	}
}
 
// 결과
고루틴 :  0
고루틴 :  1
고루틴 :  2
메인 함수 :  0
메인 함수 :  1
메인 함수 :  2
메인 함수 :  3

채널에 버퍼를 1개 이상 설정하면 비동기 채널(asynchronous channel)이 생성됩니다. 비동기 채널은 보내는 쪽에서 버퍼가 가득 차면 실행을 멈추고 대기하며 받는 쪽에서는 버퍼에 값이 없으면 대기합니다.

  • 이번 예제에서는 채널에 값을 연달아서 보내고 꺼냈지만 시간차를 두고 보내거나 꺼낼 수도 있습니다. 이렇게 되면 채널에서 값을 꺼냈을 때 버퍼에 공간이 생기므로 다른 쪽에서 다시 값을 보낼 수 있습니다. 버퍼 개수를 수정하고, 고루틴과 메인 함수의 반복문에서 time.Sleep 함수를 사용하여 여러 가지 형태로 실행해보기 바랍니다.
  • 이번 예제에서는 runtime.GOMAXPROCS(1)를 설정하여 CPU 코어를 하나만 사용합니다. CPU 코어를 여러 개 사용하면 결과가 달라질 수 있는데, 여러 곳에서 채널에 값을 보내고 꺼내기 때문입니다. 그러므로 멀티코어에서 비동기 채널을 사용할 때는 실행 순서나 채널 사용 방법에 좀 더 신경써야 합니다.
  • Go 1.4에서 채널 버퍼링 예제를 실행하면 고루틴 2번, 메인 함수 2번씩 출력됩니다. 하지만 Go 1.5 이상부터는 고루틴 스케쥴링 구현이 바뀌었기 때문에 이렇게 출력되지 않을 수 있습니다. 단, 비동기 채널의 구현과 동작은 바뀐 것이 없으므로 보내는 쪽에서 버퍼가 가득 차면 실행을 멈추고 대기하며 받는 쪽에서는 버퍼에 값이 없으면 대기한다는 점만 기억하면 됩니다.


버퍼 채널을 이용하지 않는 경우, 아래와 같은 코드는 에러 (fatal error: all goroutines are asleep - deadlock!) 를 발생시킵니다. 왜냐하면 메인루틴에서 채널에 1을 보내면서 상대편 수신자를 기다리고 있는데, 이 채널을 받는 수신자 Go루틴이 없기 때문입니다.

package main
 
import "fmt"
 
func main() {
  c := make(chan int)
  c <- 1   //수신루틴이 없으므로 데드락 
  fmt.Println(<-c) //코멘트해도 데드락 (별도의 Go루틴없기 때문)
}


하지만 아래와 같이 버퍼채널을 사용하면, 수신자가 당장 없더라도 최대버퍼 수까지 데이터를 보낼 수 있으므로, 에러가 발생하지 않습니다.

package main
 
import "fmt"
 
func main() {
    ch := make(chan int, 1)
 
    //수신자가 없더라도 보낼 수 있다.
    ch <- 101
 
    fmt.Println(<-ch)
}

채널 파라미터 (보내기 전용 및 받기 전용 채널 사용)

채널을 함수의 파라미터도 전달할 때, 일반적으로 송수신을 모두 하는 채널을 전달하지만, 특별히 해당 채널로 송신만 할 것인지 혹은 수신만 할 것인지를 지정할 수도 있습니다. 송신 파라미터는 (p chan<- int)와 같이 chan<- 을 사용하고, 수신 파라미터는 (p <-chan int)와 같이 <-chan 을 사용합니다. 만약 송신 채널 파라미터에서 수신을 한다거나, 수신 채널에 송신을 하게되면, 에러가 발생합니다.

다음은 0부터 4까지 채널에 값을 보내고, 다시 채널에서 값을 꺼내서 출력합니다. 그리고 반복문이 끝난 뒤 채널에 100을 보낸 뒤 다시 꺼내서 출력합니다.

package main

import "fmt"

func producer(c chan<- int) { // 보내기 전용 채널
	for i := 0; i < 5; i++ {
		c <- i
	}

	c <- 100           // 채널에 값을 보냄

	//fmt.Println(<-c) // 채널에서 값을 꺼내면 컴파일 에러
}

func consumer(c <-chan int) { // 받기 전용 채널
	for i := range c {
		fmt.Println(i)
	}

	fmt.Println(<-c) // 채널에 값을 꺼냄

	// c <- 1        // 채널에 값을 보내면 컴파일 에러
}

func main() {
	c := make(chan int)

	go producer(c)
	go consumer(c)

	fmt.Scanln()
}
 
// 결과
0
1
2
3
4
100

보내기 전용 및 받기 전용 채널은 채널 앞 뒤로 <- 연산자를 붙여서 만듭니다. 보통 함수의 매개변수로 사용하거나, 구조체의 필드로 사용합니다.

  • 보내기 전용(send-only)chan<- 자료형 형식입니다. c chan<- int는 int형 보내기 전용 채널 c를 뜻합니다. 보내기 전용 채널은 값을 보낼 수만 있으며 값을 가져오려고 하면 컴파일 에러가 발생합니다.
  • 받기 전용(receive-only)<-chan 자료형 형식입니다. c <-chan int는 int형 받기 전용 채널 c를 뜻합니다. 받기 전용 채널은 range 키워드 또는 <- 채널 형식으로 값을 꺼낼 수만 있으며 값을 보내려고 하면 컴파일 에러가 발생합니다.

chan 키워드를 기준으로 <- (화살표)가 붙은 방향을 보면 보내기 전용인지 받기 전용인지 알 수 있습니다. 즉 chan<-은 chan 키워드로 <-가 들어가므로 보내기 전용, <-chan은 chan 키워드에서 <-가 나오고 있으므로 받기 전용 채널입니다.

여기서는 producer 함수는 매개변수로 보내기 전용 채널을 사용하고, consumer 함수는 매개변수로 받기 전용 채널을 사용합니다. 따라서 producer 함수는 값을 보내기만 하고, consumer 함수는 값을 꺼내기만 합니다. 이번에는 채널을 리턴값으로 사용해보겠습니다. 다음은 두 수를 더한 뒤 채널로 리턴합니다.

package main

import "fmt"

//                    ↓ 함수의 리턴 값은 int 형 받기 전용 채널
func sum(a, b int) <-chan int {
	out := make(chan int) // 채널 생성
	go func() {
		out <- a + b // 채널에 a와 b의 합을 보냄
	}()
	return out           // 채널 변수 자체를 리턴
}

func main() {
	c := sum(1, 2)   // 채널을 리턴값으로 받아서 c에 대입

	fmt.Println(<-c) // 3: 채널에서 값을 꺼냄
}
 
// 결과
3


sum 함수는 받기 전용 채널을 리턴하도록 만들었습니다. 채널을 리턴하려면 먼저 make 함수로 채널을 생성합니다. 그리고 고루틴 안에서 채널에 값을 보낸 뒤 고루틴 바깥에서 채널을 리턴합니다. sum 함수를 사용하여 채널을 리턴값으로 받았으면 <-c처럼 값을 꺼내면 됩니다.

이번에는 채널만 사용하여 값을 더해보겠습니다.

package main

import "fmt"

//                    ↓ 함수의 리턴 값은 int 형 받기 전용 채널
func num(a, b int) <-chan int {
	out := make(chan int) // int형 채널 생성
	go func() {
		out <- a   // 채널에 a의 값을 보냄
		out <- b   // 채널에 b의 값을 보냄
		close(out) // 채널을 닫음
	}()
	return out // 채널 변수 자체를 리턴
}

//            ↓ 함수의 매개변수는 int형 받기 전용 채널
func sum(c <-chan int) <-chan int {
//                        ↑ 함수의 리턴 값은 int형 받기 전용 채널
	out := make(chan int) // int형 채널 생성
	go func() {
		r := 0
		for i := range c { // range를 사용하여 채널이 닫힐 때까지 값을 꺼냄
			r = r + i  // 꺼낸 값을 모두 더함
		}
		out <- r           // 더한 결과를 채널에 보냄
	}()
	return out // 채널 변수 자체를 리턴
}

func main() {
	c := num(1, 2) // 1과 2가 들어있는 채널이 리턴됨
	out := sum(c)  // 채널 c를 매개변수에 넘겨서 모두 더함, 더한 값이 들어있는 out 채널을 리턴

	fmt.Println(<-out) // 3: out 채널에서 값을 꺼냄
}
 
// 결과
3


아래 예제에서는 sendChan() 함수 안에서 x := <- ch 를 실행하면 송신전용 채널에 수신을 시도하므로 에러가 발생합니다.

package main
 
import "fmt"
 
func main() {
    ch := make(chan string, 1)
    sendChan(ch)
    receiveChan(ch)
}
 
func sendChan(ch chan<- string) {
    ch <- "Data"
    // x := <-ch // 에러발생
}
 
func receiveChan(ch <-chan string) {
    data := <-ch
    fmt.Println(data)
}

채널 닫기 (close)

채널을 오픈한 후 데이타를 송신한 후, close()함수를 사용하여 채널을 닫을 수 있습니다. 채널을 닫게 되면, 해당 채널로는 더이상 송신을 할 수 없지만, 채널이 닫힌 이후에도 계속 수신은 가능합니다. 채널 수신에 사용되는 <- ch 은 두개의 리턴 값을 갖는데, 첫째는 채널 메시지이고, 두번째는 수신이 제대로 되었는가를 나타냅니다. 만약 채널이 닫혔다면, 두번째 리턴값은 false를 리턴합니다.

package main
 
func main() {
    ch := make(chan int, 2)
     
    // 채널에 송신
    ch <- 1
    ch <- 2
     
    // 채널을 닫는다
    close(ch)
 
    // 채널 수신
    println(<-ch)
    println(<-ch)
     
    if _, success = <-ch; !success {
        println("더이상 데이타 없음.")
    }
}

채널 range 문

채널에서 송신자가 송신을 한 후, 채널을 닫을 수 있습니다. 그리고 수신자는 임의의 개수의 데이터를 채널이 닫힐 때까지 계속 수신할 수 있습니다. 아래 예제는 이러한 송수신 방법을 표현한 것으로, 수신자는 채널이 닫히는 것을 체크하면서 계속 루프를 돌게 됩니다. 첫 번째 방법은 무한 for 루프 안에서 if 문으로 수신 채널의 두번째 파라미터를 체크하는 방식이고, 두 번째 방법는 첫 번째 방법과 동일한 표현이지만, for range문으로 보다 간결하게 표현한 것입니다. 채널 range문은 range 키워드 다음의 채널로부터 계속 수신하다가 채널이 닫힌 것을 감지하면 for 루프를 종료합니다.

package main
 
func main() {
    ch := make(chan int, 2)
 
    // 채널에 송신
    ch <- 1
    ch <- 2
 
    // 채널을 닫는다
    close(ch)
 
    // 방법1
    // 채널이 Close된 때 까지 계속 수신
    /*
    for {
        if i, success := <-ch; success {
            println(i)
        } else {
            break
        }
    }
    */
 
    // 방법2
    // 위 표현과 동일한 채널 range 문
    for i := range ch {
        println(i)
    }
}


다음은 range와 close 함수의 특징입니다.

  • 이미 닫힌 채널에 값을 보내면 패닉이 발생
  • 채널을 닫으면 range 루프가 종료
  • 채널이 열려있고, 값이 들어오지 않는다면 range는 실행되지 않고 계속 대기합니다. 만약 다른 곳에서 채널에 값을 보냈다면(채널에 값이 들어오면) 그때부터 range가 계속 반복

select문 사용

Go의 select문은 복수 채널들을 기다리면서 준비된 (데이터를 보내온) 채널을 실행하는 기능을 제공합니다. 즉, select문은 여러 개의 case문에서 각각 다른 채널을 기다리다가 준비가 된 채널 case를 실행하는 것입니다. select문은 case 채널들이 준비되지 않으면 계속 대기하게 되고, 가장 먼저 도착한 채널의 case를 실행합니다. 만약 복수 채널에 신호가 오면, Go 런타임이 랜덤하게 그 중 한 개를 선택합니다. 하지만, select문에 default 문이 있으면, case문 채널이 준비되지 않더라도 계속 대기하지 않고 바로 default문을 실행합니다.

select {
case <-채널1:
	// 채널1에 값이 들어왔을 때 실행할 코드를 작성합니다.
case <-채널2:
	// 채널2에 값이 들어왔을 때 실행할 코드를 작성합니다.
default:
	// 모든 case의 채널에 값이 들어오지 않았을 때 실행할 코드를 작성합니다.
}


select 분기문은 switch 분기문과 비슷하지만 select 키워드 뒤에 검사할 변수를 따로 지정하지 않으며 각 채널에 값이 들어오면 해당 case가 실행됩니다(close 함수로 채널을 닫았을 때도 case가 실행됩니다). 그리고 보통 select를 계속 처리할 수 있도록 for로 반복해줍니다(반복하지 않으면 한 번만 실행하고 끝냅니다).

switch 분기문과 마찬가지로 select 분기문도 default 케이스를 지정할 수 있으며 case에 지정된 채널에 값이 들어오지 않았을 때 즉시 실행됩니다. 단, default에 적절한 처리를 하지 않으면 CPU 코어를 모두 점유하므로 주의합니다.

다음은 채널 2개를 생성하고 100밀리초, 500밀리초 간격으로 숫자와 문자열을 보낸 뒤 꺼내서 출력합니다.

package main

import (
	"fmt"
	"time"
)

func main() {
	c1 := make(chan int)    // int형 채널 생성
	c2 := make(chan string) // string 채널 생성

	go func() {
		for {
			c1 <- 10                           // 채널 c1에 10을 보낸 뒤
			time.Sleep(100 * time.Millisecond) // 100 밀리초 대기
		}
	}()

	go func() {
		for {
			c2 <- "Hello, world!"              // 채널 c2에 Hello, world!를 보낸 뒤
			time.Sleep(500 * time.Millisecond) // 500 밀리초 대기
		}
	}()

	go func() {
		for {
			select {
			case i := <-c1:                // 채널 c1에 값이 들어왔다면 값을 꺼내서 i에 대입
				fmt.Println("c1 :", i) // i 값을 출력
			case s := <-c2:                // 채널 c2에 값이 들어왔다면 값을 꺼내서 s에 대입
				fmt.Println("c2 :", s) // s 값을 출력
			}
		}
	}()

	time.Sleep(10 * time.Second) // 10초 동안 프로그램 실행
}
 
// 결과
c2 : Hello, world!
c1 : 10
c1 : 10
c1 : 10
c1 : 10
c1 : 10
c2 : Hello, world!
c1 : 10
... (생략)
  • case에서는 case i := <-c1:처럼 채널에서 값을 꺼낸 뒤 변수에 바로 저장할 수 있습니다. 만약 꺼낸 값을 사용하지 않는다면 case <-c1:처럼 변수를 생략해도 됩니다.
  • time.After 함수를 사용하면 시간 제한 처리를 할 수 있습니다. time.After는 특정 시간이 지나면 현재 시간을 채널로 보냅니다.


위의 예제와는 다르게  select에서 매번 채널 c1에 값을 보내지만 채널 c2에 값이 들어오면 c2에서 값을 꺼내서 출력합니다.

package main

import (
	"fmt"
	"time"
)

func main() {
	c1 := make(chan int)    // int형 채널 생성
	c2 := make(chan string) // string 채널 생성

	go func() {
		for {
			i := <-c1                          // 채널 c1에서 값을 꺼낸 뒤 i에 대입
			fmt.Println("c1 :", i)             // i 값을 출력
			time.Sleep(100 * time.Millisecond) // 100 밀리초 대기
		}
	}()

	go func() {
		for {
			c2 <- "Hello, world!"              // 채널 c2에 Hello, world!를 보냄
			time.Sleep(500 * time.Millisecond) // 500 밀리초 대기
		}
	}()

	go func() {
		for { // 무한 루프
			select {
			case c1 <- 10:                 // 매번 채널 c1에 10을 보냄
			case s := <-c2:                // c2에 값이 들어왔을 때는 값을 꺼낸 뒤 s에 대입
				fmt.Println("c2 :", s) // s 값을 출력
			}
		}
	}()

	time.Sleep(10 * time.Second) // 10초 동안 프로그램 실행
}
 
//결과
c2 : Hello, world!
c1 : 10
c1 : 10
c1 : 10
c1 : 10
c1 : 10
c2 : Hello, world!
c1 : 10
... (생략)


아래 또 다른 예제는 for 루프 안에 select 문을 쓰면서 두개의 goroutine이 모두 실행되기를 기다리고 있습니다. 첫번째 run1()이 1초간 실행되고 done1 채널로부터 수신하여 해당 case를 실행하고, 다시 for 루프를 돕니다. for루프를 다시 돌면서 다시 select문이 실행되는데, 다음 run2()가 2초후에 실행되고 done2 채널로부터 수신하여 해당 case를 실행하게 됩니다. done2 채널 case문에 break EXIT 이 있는데, 이 문장으로 인해 for 루프를 빠져나와 EXIT 레이블로 이동하게 됩니다. Go의 "break 레이블" 문은 C/C# 등의 언어에서의 goto 문과 다른데, Go에서는 해당 레이블로 이동한 후 자신이 빠져나온 루프 다음 문장을 실행하게 됩니다. 따라서, 여기서는 for 루프 다음 즉 main() 함수의 끝에 다다르게 됩니다.

package main
 
import "time"
 
func main() {
    done1 := make(chan bool)
    done2 := make(chan bool)
 
    go run1(done1)
    go run2(done2)
 
EXIT:
    for {
        select {
        case <-done1:
            println("run1 완료")
 
        case <-done2:
            println("run2 완료")
            break EXIT
        }
    }
}
 
func run1(done chan bool) {
    time.Sleep(1 * time.Second)
    done <- true
}
 
func run2(done chan bool) {
    time.Sleep(2 * time.Second)
    done <- true
}


'언어 > GO' 카테고리의 다른 글

[GO] gin-gonic DELETE 메소드 데이터 호출방법  (0) 2017.03.17
[GO] gin-gonic CORS 문제 해결방법  (0) 2017.03.17
[GO] 채널  (0) 2017.03.02
[GO] 고루틴  (0) 2017.02.22
[GO] 인터페이스 (Interface)  (0) 2017.02.20
[GO] 파이썬 모듈 실행하기  (0) 2017.02.18

개요

고루틴은 가벼운 스레드와 같은 것으로 현재 수행 흐름과 별개의 흐름을 만들어줍니다.  "go" 키워드를 사용하여 함수를 호출하면, 런타임시 새로운 goroutine을 실행합니다. goroutine은 비동기적으로(asynchronously) 함수루틴을 실행하므로, 여러 코드를 동시에(Concurrently) 실행하는데 사용됩니다.

goroutine은 OS 쓰레드보다 훨씬 가볍게 비동기 Concurrent 처리를 구현하기 위하여 만든 것으로, 기본적으로 Go 런타임이 자체 관리합니다. Go 런타임 상에서 관리되는 작업단위인 여러 goroutine들은 종종 하나의 OS 쓰레드 1개로도 실행되곤 합니다. 즉, Go루틴들은 OS 쓰레드와 1 대 1로 대응되지 않고, Multiplexing으로 훨씬 적은 OS 쓰레드를 사용합니다. 메모리 측면에서도 OS 쓰레드가 1 메가바이트의 스택을 갖는 반면, goroutine은 이보다 훨씬 작은 몇 킬로바이트의 스택을 갖습니다(필요시 동적으로 증가). Go 런타임은 Go루틴을 관리하면서 Go 채널을 통해 Go루틴 간의 통신을 쉽게 할 수 있도록 하였습니다.


고루틴을 생성하는 방법은 아주 간단합니다. 

go f(x,y,z)

앞에 go를 붙여 이와 같이 함수를 호출하게 되면 f(x,y,z) 호출과 현재 함수의 흐름은 메모리를 공유하는 논리적으로 별개의 흐름이 됩니다. 여기서 논리적으로 별개의 흐름이라고 한 이유는 물리적으로 별개의 흐름이 되는 것과는 구분되기 때문입니다.

병렬성과 병행성

물리적으로 별개의 흐름이란 정말로 동시에 각각의 흐름이 수행되는 경우를 뜻합니다. 두 사람이 동시에 각각 업무를 보고 있다면 물리적으로 별개로 업무를 수행하는 것입니다. 이것을 병렬성(Parallelism)이라 합니다.

반면 커피를 마시면서 신문을 보고 있는 사람이 있다면 물리적으로 두 흐름이 동시에 수행되는 것은 아닙니다. 커피를 마시기 위하여 신문 보는 것을 짧은 시간 잠시 중단하고 커피를 한 모금 마신 뒤에 다시 신문 보는 일로 돌아오는 경우는 물리적으로 두 흐름이 있지는 않지만, 동시에 두 가지를 하고 있습니다. 이것을 동시성 혹은 병행성(concurrency)라고 합니다. 물리적으로 동시에 수행되지는 않지만 순차적으로 수행되는 것과는 큰 차이가 있습니다. 즉, 커피 한잔을 모두 다 마시는 동안 신문을 전혀 보지 않고, 반드시 커피를 다 마시고 난 다음에 신문을 보기 시작해야 하는 것이 아닙니다. 이 과정에서 신문 기사 10문단을 읽는 사이에 커피 10모금을 마신다고 할 때 2번째 커피 모금을 넘기는 것과 신문 기사의 다섯 번째 문단을 읽는 것중에서 어느 것을 먼저 해야하는지는 그다지 상관이 없는 것이 됩니다.  

이때 동시성이 있는 두 루틴은 서로 의존관계가 없습니다. 

동시성은 병렬성과 다르지만 동시성이 있어야 병렬성이 생기게 됩니다. 서로 어느 것이 먼저 되어야 하는 의존관계가 있는 것은 함께 진행될 수 없습니다. 장갑을 끼면서 동시에 구두를 신을 수는 있지만, 양말을 신으면서 동시에 구두를 신을 수는 없습니다.즉 양말과 구두를 신는 것에 대해 동시성이 없으므로 병렬성이 성립되지 않습니다.


아래 예제에서 In goroutine과 In main routine 중에서 어느 것이 먼저 출력될 지 알 수 없게 됩니다.


func main() {
	go func() {
		fmt.Println("In goroutine")
	}()
	fmt.Println("In main routine")
}

심지어 메인 함수가 끝나버리면 고루틴이 모두 수행되지 않을 수도 있습니다. 즉 위 코드만 실행하면 In goroutine이 실행되지 않을 수도 있습니다.

고루틴 익명함수 (클로저 고루틴으로 실행)

고루틴(goroutine)은 익명함수(anonymous function)에 대해 사용할 수도 있습니다. 즉, go 키워드 뒤에 익명함수를 바로 정의하는 것으로, 이는 익명함수를 비동기로 실행하게 됩니다. 아래 예제에서 첫번째 익명함수는 간단히 Hello 문자열을 출력하는데, 이를 goroutine으로 실행하면 비동기적으로 그 익명함수를 실행하게 됩니다. 두번째 익명함수는 파라미터를 전달하는 예제로 익명함수에 파라미터가 있는 경우, go 익명함수 바로 뒤에 파라미터(Hi)를 함께 전달하게 됩니다.

package main
 
import (
    "fmt"
    "sync"
)
 
func main() {
    // WaitGroup 생성. 2개의 Go루틴을 기다림.
    var wait sync.WaitGroup
    wait.Add(2)
 
    // 익명함수를 사용한 goroutine
    go func() {
        defer wait.Done() //끝나면 .Done() 호출
        fmt.Println("Hello")
    }()
 
    // 익명함수에 파라미터 전달
    go func(msg string) {
        defer wait.Done() //끝나면 .Done() 호출
        fmt.Println(msg)
    }("Hi")
 
    wait.Wait() //Go루틴 모두 끝날 때까지 대기

여기서 sync.WaitGroup을 사용하고 있는데, 이는 기본적으로 여러 Go루틴들이 끝날 때까지 기다리는 역할을 합니다. WaitGroup을 사용하기 위해서는 먼저 Add() 메소드에 몇 개의 Go루틴을 기다릴 것인지 지정하고, 각 Go루틴에서 Done() 메서드를 호출합니다 (여기서는 defer 를 사용). 그리고 메인루틴에서는 Wait() 메서드를 호출하여, Go루틴들이 모두 끝나기를 기다립니다.

고루틴 기다리기

따로 노는 고루틴들을 제어하기 위해 싱크 라이브러리가 제공됩니다. 이는 위에서 이미 사용한 sync.WaitGroup 라이브러리입니다. 위의 코드를 예제로 다시 설명해보겠습니다.

wait 변수에는 기본값이 0으로 맞춰져 있는 카운터가 들어 있습니다.

Wait() 함수는 이 카운터가 0이 될 때까지 기다립니다.

Add()함수는 호출될 때마다 숫자를 더합니다. 

Done()함수는 사실상 Add(-1)과 같다고 보면 되지만 다 되었다는 것을 표현해두는 것이 더 알기 쉽기 때문에 Done()을 쓰는 것이 좋습니다.

그렇기 떄문에 처음에 WaitGroup을 만들자마자 wait을 할 개수만큼 카운터를 증가시키고 각각의 고루틴에서는 작업이 완료될 때마다 카운터를 감소시킵니다. 모든 고루틴이 끝나면 카운터가 0이 되는데 이 상태가 되기 전까지는 Wait() 부분에 멈춰 있게 됩니다. 


미리 고루틴이 몇 개 생길지 알기 때문에 이렇게 작성이 가능하지만, 고루틴이 몇 개 생길지 알기 어렵거나 따로 수를 세어야 알 수 있는 경우에는 고루틴을 띄워보내기 전에 Add(1)을 수행하여 하나씩 카운터를 증가시킬 수 있습니다.

var wg sync.WaitGroup
for _, url := range urls {
	wg.Add(1)
	go func(url string) {
		defer wg.Done()
		if _, err := download(url); err != nil {
			log.Fatal(err)
		}
	}(url)
}
wg.Wait()

주의할 점은 wg.Add(1)을 고루틴 내부에 포함시키면 안되는 점입니다.  대체로 문제없이 동작하겠지만 고루틴 내부의 wg.Add(1)이 수행되기 전에 메인 고루틴이 wg.Wait()을 통과해버릴 가능성이 있기 때문입니다. 이런 상태를 레이스 컨디션(race condition)이라고 합니다.

다중 CPU 처리 (멀티코어 활용)

Go 언어는 CPU 코어를 한 개만 사용하도록 설정되어 있습니다. 즉, 여러 개의 Go 루틴을 만들더라도, 1개의 CPU Go 1.5 버전 이상에서는 기본값으로 물리CPU 개수만큼 사용하도록 설정이 되어(1.5 이하는 CPU 1개 기본값) 물리 CPU개수에서 작업을 시분할하여 처리합니다(Concurrent 처리). 만약 머신이 복수개의 CPU를 가진 경우, Go 프로그램을 다중 CPU에서 병렬처리 (Parallel 처리)하게 할 수 있는데, 병렬처리를 위해서는 아래와 같이 runtime.GOMAXPROCS(CPU수) 함수를 호출하여야 합니다(여기서 CPU 수는 Logical CPU 수를 가리킴). 다음은 시스템의 모든 CPU 코어를 사용하는 방법입니다.

package main

import (
	"fmt"
	"runtime"
)

func main() {
	runtime.GOMAXPROCS(runtime.NumCPU()) // CPU 개수를 구한 뒤 사용할 최대 CPU 개수 설정

	fmt.Println(runtime.GOMAXPROCS(0)) // 설정 값 출력

	s := "Hello, world!"

	for i := 0; i < 100; i++ {
		go func(n int) { // 익명 함수를 고루틴으로 실행
			fmt.Println(s, n)
		}(i)
	}

	fmt.Scanln()
}

runtime.NumCPU() 함수로 현재 시스템의 CPU 코어 개수를 구한 뒤 runtime.GOMAXPROCS() 함수에 설정해줍니다. 이렇게 하면 모든 CPU 코어에서 고루틴을 실행할 수 있습니다. runtime.GOMAXPROCS() 함수는 CPU 코어 개수를 구하지 않고, 특정 값을 설정해도 됩니다. 그리고 runtime.GOMAXPROCS() 함수에 0을 넣으면 설정 값은 바꾸지 않고 현재 설정된 값만 리턴합니다.

공유메모리와 병렬 최소값 찾기

고루틴들은 서로 메모리를 공유합니다. 고루틴이 변수의 포인터를 받아서 해당 변수에 원하는 값을 넣어줄 수도 있습니다. 큰 배열에 계산 결과를 넣어야 한다고 해도 영역별로 슬라이스로 나눠서 여러 고루틴에 각각의 슬라이스들은 나눠주어서 합쳐진 결과값을 받을 수 있습니다. 모든 고루틴이 다 끝났는지는 위에서 배운 WaitGroup과 같은 것을 이용하면 되는 것입니다.

공유 메모리를 이용하여 매우 병렬화가 잘 되는 예제를 설명하겠습니다. 최소값 찾기 예제는 서로 소통할 필요가 거의 없는 종류의 예제입니다. 아주 큰 배열에서 가장 작은 수를 찾고 싶다고 가정합니다. 일단 병렬화하지 않고 가장 작은 수를 찾는 함수부터 설명하겠습니다.

package main

import "fmt"

func Min(a []int) int {
	if len(a) == 0 {
		return 0
	}
	min := a[0]
	for _, e := range a[1:] {
		if min > e {
			min = e
		}
	}
	return min
}

func main() {
	fmt.Println(Min([]int{83, 46, 49, 23, 92, 48, 39, 91, 44, 99, 25, 42, 35, 56, 23}))
}
// 결과
// 23


이제 병렬버젼을 설명하겠습니다. 사람 4명이서 가장 작은 수를 찾는다고 생각해보면, 전체를 넷으로 나눠서 각자 가장 작은 수를 찾은 다음에 그중에서 가장 작은 수를 한번 더 찾으면 됩니다. 그래서 아주 쉬운 병렬(embarrassingly parallel) 문제입니다.
아래 함수에서 매개변수 n은 몇 개의 고루틴을 생성할 것인지를 지정합니다.


package main


import (
	"fmt"
	"sync"
)

func Min(a []int) int {
	if len(a) == 0 {
		return 0
	}
	min := a[0]
	for _, e := range a[1:] {
		if min > e {
			min = e
		}
	}
	return min
}

func ParallelMin(a []int, n int) int {
	if len(a) < n {
		return Min(a)
	}
	mins := make([]int, n)
	size := (len(a) + n - 1) / n
	var wg sync.WaitGroup
	for i := 0; i < n; i++ {
		wg.Add(1)
		go func(i int) {
			defer wg.Done()
			begin, end := i*size, (i+1)*size
			if end > len(a) {
				end = len(a)
			}
			mins[i] = Min(a[begin:end])
		}(i)
	}
	wg.Wait()
	return Min(mins)
}

func main() {
	fmt.Println(ParallelMin([]int{83, 46, 49, 23, 92, 48, 39, 91, 44, 99, 25, 42, 35, 56, 23}, 4))
}
// 결과
// 23

위의 예제에서는 고루틴 내에서 mins[i]에 결과를 넣어줍니다. 고루틴들이 메모리를 공유하는 모델이기 때문에 같은 배열에 접근이 가능한 것입니다. i 번째 고루틴은 i번째에 값을 넣게 됩니다. 


'언어 > GO' 카테고리의 다른 글

[GO] gin-gonic CORS 문제 해결방법  (0) 2017.03.17
[GO] 채널  (0) 2017.03.02
[GO] 고루틴  (0) 2017.02.22
[GO] 인터페이스 (Interface)  (0) 2017.02.20
[GO] 파이썬 모듈 실행하기  (0) 2017.02.18
[GO] AES ECB 모드 암호화 (PKCS5, PKCS7)  (0) 2017.02.17


구조체(struct)가 필드들의 집합체라면, interface는 메서드들의 집합체입니다. interface는 타입(type)이 구현해야 하는 메서드 원형(prototype)들을 정의합니다. 하나의 사용자 정의 타입이 interface를 구현하기 위해서는 단순히 그 인터페이스가 갖는 모든 메서드들을 구현하면 됩니다. 인터페이스의 이름은 보통 변수에 ~er이 붙습니다. ex) printer interface {}

인터페이스는 struct와 마찬가지로 type 문을 사용하여 정의합니다.

type Shape interface {
    area() float64
    perimeter() float64
}

구현

예제 1

인터페이스를 구현하기 위해서는 해당 타입이 그 인터페이스의 메서드들을 모두 구현하면 되므로, 위의 Shape 인터페이스를 구현하기 위해서는 area(), perimeter() 2개의 메서드만 구현하면 됩니다. 예를 들어 Rect와 Circle이라는 2개의 타입이 있을 때, Shape 인터페이스를 구현하기 위해서는 아래와 같이 각 타입별로 2개의 메서드를 구현해 주면 됩니다.

//Rect 정의
type Rect struct {
    width, height float64
}
 
//Circle 정의
type Circle struct {
    radius float64
}
 
//Rect 타입에 대한 Shape 인터페이스 구현 
func (r Rect) area() float64 { return r.width * r.height }
func (r Rect) perimeter() float64 {
     return 2 * (r.width + r.height)
}
 
//Circle 타입에 대한 Shape 인터페이스 구현 
func (c Circle) area() float64 { 
    return math.Pi * c.radius * c.radius
}
func (c Circle) perimeter() float64 { 
    return 2 * math.Pi * c.radius

예제 2

package main

import "fmt"

type MyInt int // int 형을 MyInt로 정의

func (i MyInt) Print() { // MyInt에 Print 메서드를 연결
	fmt.Println(i)
}

type Rectangle struct { // 사각형 구조체 정의
	width, height int
}

func (r Rectangle) Print() { // Rectangle에 Print 메서드를 연결
	fmt.Println(r.width, r.height)
}

type Printer interface { // Print 메서드를 가지는 인터페이스 정의
	Print()
}

func main() {
	var i MyInt = 5
	r := Rectangle{10, 20}

	var p Printer // 인터페이스 선언

	p = i     // i를 인터페이스 p에 대입
	p.Print() // 5: 인터페이스 p를 통하여 MyInt의 Print 메서드 호출

	p = r     // r을 인터페이스 p에 대입
	p.Print() // 10 20: 인터페이스 p를 통하여 Rectangle의 Print 메서드 호출
}

MyInt 자료형, Ractangle 구조체, Printer 인터페이스를 그림으로 표현하면 다음과 같습니다. MyInt를 정의하고, 너비와 높이를 가지는 사각형 구조체 Rectangle을 정의했습니다. 그리고 MyInt, Rectangle 모두 자신의 내용을 출력하는 Print 함수를 구현했습니다. 이제 두 타입 모두 똑같은 Print 함수를 가지고 있습니다(여기서 똑같은 함수라는 것은 함수 이름, 매개변수 자료형, 리턴값 자료형이 모두 같은 상태를 뜻합니다).


사용

인터페이스를 사용하는 일반적인 예로 함수가 파라미터로 인터페이스를 받아들이는 경우를 들 수 있습니다. 함수 파라미터가 interface인 경우, 이는 어떤 타입이든 해당 인터페이스를 구현하기만 하면 모두 입력 파라미터로 사용될 수 있다는 것을 의미합니다.

아래 예제에서 showArea() 함수는 Shape 인터페이스들을 파라미터로 받아들이고 있는데, 따라서 Rect와 Circle 처럼 Shape 인터페이스를 구현한 타입 객체들을 파라미터로 받을 수 있습니다. showArea() 함수 내에서 해당 인터페이스가 가진 메서드 즉 area() 혹은 perimeter()을 사용할 수 있습니다.

func main() { r := Rect{10., 20.} c := Circle{10} showArea(r, c) } func showArea(shapes ...Shape) { for _, s := range shapes { a := s.area() //인터페이스 메서드 호출 println(a) } }

덕 타이핑

이렇게 각 값이나 인스턴스의 실제 타입은 상관하지 않고 구현된 메서드로만 타입을 판단하는 방식을 덕 타이핑(Duck typing)이라 합니다. 이 용어는 다음과 같은 덕 테스트(오리 테스트)에서 유래되었습니다. “만약 어떤 새가 오리처럼 걷고, 헤엄치고, 꽥꽥거리는 소리를 낸다면 나는 그 새를 오리라 부르겠다.” Go 언어로는 다음과 같이 덕 타이핑을 구현할 수 있습니다.

package main

import "fmt"

type Duck struct { // 오리(Duck) 구조체 정의
}

func (d Duck) quack() {     // 오리의 quack 메서드 정의
	fmt.Println("꽥~!") // 오리 울음 소리
}

func (d Duck) feathers() { // 오리의 feathers 메서드 정의
	fmt.Println("오리는 흰색과 회색 털을 가지고 있습니다.")
}

type Person struct { // 사람(Person) 구조체 정의
}

func (p Person) quack() {                           // 사람의 quack 메서드 정의
	fmt.Println("사람은 오리를 흉내냅니다. 꽥~!") // 사람이 오리 소리를 흉내냄
}

func (p Person) feathers() { // 사람의 feathers 메서드 정의
	fmt.Println("사람은 땅에서 깃털을 주워서 보여줍니다.")
}

type Quacker interface { // quack, feathers 메서드를 가지는 Quacker 인터페이스 정의
	quack()
	feathers()
}

func inTheForest(q Quacker) {
	q.quack()    // Quacker 인터페이스로 quack 메서드 호출
	q.feathers() // Quacker 인터페이스로 feathers 메서드 호출
}

func main() {
	var donald Duck // 오리 인스턴스 생성
	var john Person // 사람 인스턴스 생성

	inTheForest(donald) // 인터페이스를 통하여 오리의 quack, feather 메서드 호출
	inTheForest(john)   // 인터페이스를 통하여 사람의 quack, feather 메서드 호출
}


타입이 특정 인터페이스를 구현하는지 검사하려면 다음과 같이 사용합니다.

  • interface{}(인스턴스).(인터페이스)
var donald Duck

if v, ok := interface{}(donald).(Quacker); ok {
	fmt.Println(v, ok)
}
 
# 결과
# {} true

Duck 타입의 인스턴스 donald를 빈 인터페이스에 넣은 뒤 Quacker 인터페이스와 같은지 확인합니다. 첫 번째 리턴값은 검사했던 인스턴스이며 두 번째 리턴값은 인스턴스가 해당 인터페이스를 구현하고 있는지 여부입니다. 구현하고 있다면 true 그렇지 않으면 false입니다.

타입

Go 프로그래밍을 하다보면 흔히 빈 인터페이스(empty interface)를 자주 접하게 되는데, 흔히 인터페이스 타입(interface type)으로도 불립니다. 예를 들어, 여러 표준 패키지들의 함수 Prototype을 살펴보면, 아래와 같이 빈 interface가 자주 등장함을 볼 수 있습니다. 빈 interface는 interface{} 와 같이 표현합니다.

func Marshal(v interface{}) ([]byte, error); func Println(a ...interface{}) (n int, err error);


Empty interface는 메서드를 전혀 갖지 않는 빈 인터페이스로서, Go의 모든 Type은 적어도 0개의 메서드를 구현하므로, 흔히 Go에서 모든 Type을 나타내기 위해 빈 인터페이스를 사용합니다. 즉, 빈 인터페이스는 어떠한 타입도 담을 수 있는 컨테이너라고 볼 수 있으며, 여러 다른 언어에서 흔히 일컫는 Dynamic Type 이라고 볼 수 있습니다. (주: empty interface는 C#, Java 에서 object라 볼 수 있으며, C/C++ 에서는 void* 와 같다고 볼 수 있음)

아래 예제에서 인터페이스 타입 x는 정수 1을 담았다가 다시 문자열 Tom을 담고 있는데, 실행 결과는 마지막에 담은 Tom을 출력합니다.

package main import "fmt" func main() { var x interface{} x = 1 x = "Tom" printIt(x) } func printIt(v interface{}) { fmt.Println(v) //Tom }

Type Assertion

Interface type의 x와 타입 T에 대하여 x.(T)로 표현했을 때, 이는 x가 nil이 아니며, x는 T 타입에 속한다는 점을 확인(assert)하는 것으로 이러한 표현을 "Type Assertion"이라 부릅니다. 만약 x가 nil 이거나 x의 타입이 T가 아니라면, 런타임 에러가 발생할 것이고, x가 T 타입인 경우는 T 타입의 x를 리턴합니다. 즉, 아래 예제에서 변수 j는 a.(int)로부터 int형 변수 j가 됩니다.

func main() { var a interface{} = 1 i := a // a와 i 는 dynamic type, 값은 1 j := a.(int) // j는 int 타입, 값은 1 println(i) // 포인터주소 출력 println(j) // 1 출력 }


'언어 > GO' 카테고리의 다른 글

[GO] 채널  (0) 2017.03.02
[GO] 고루틴  (0) 2017.02.22
[GO] 인터페이스 (Interface)  (0) 2017.02.20
[GO] 파이썬 모듈 실행하기  (0) 2017.02.18
[GO] AES ECB 모드 암호화 (PKCS5, PKCS7)  (0) 2017.02.17
[GO] JSON 사용하기  (0) 2017.02.16

파이썬 모듈을 go자체에서 import 하는 방법은 모르겠고.. exec 패키지를 사용해 리눅스에 파이썬 모듈을 command를 날리는 형식으로 실행합니다.

// testpython 모듈 실행 코드
// testpython 모듈 내의 test_function()을 호출하여 반환 값을 print()하는 코드
 
command := "import testpython; print(testpython.test_function())"

// 리눅스내 파이썬 명령어 실행
cmd := exec.Command("python3", "-c", command)

out, err := cmd.CombinedOutput()
if err != nil {
   fmt.Println(err)
}
// 결과출력
fmt.Println(string(out))


'언어 > GO' 카테고리의 다른 글

[GO] 고루틴  (0) 2017.02.22
[GO] 인터페이스 (Interface)  (0) 2017.02.20
[GO] 파이썬 모듈 실행하기  (0) 2017.02.18
[GO] AES ECB 모드 암호화 (PKCS5, PKCS7)  (0) 2017.02.17
[GO] JSON 사용하기  (0) 2017.02.16
[GO] const와 iota  (0) 2017.02.15


package main
import (
	"bytes"
	"crypto/aes"
	"crypto/cipher"
	"encoding/base64"
	"fmt"
	"strings"
)
func main() {
	/*
	 *src 암호화 대상
	 *key 암호 key, 16bit면 AES-128, 32bit면 AES-256
	 */
	src := "테스트 평문"
	key := "0123456789abcdef"
	crypted := AesEncrypt(src, key)
	AesDecrypt(crypted, []byte(key))
	Base64URLDecode("")
}
func Base64URLDecode(data string) ([]byte, error) {
	var missing = (4 - len(data)%4) % 4
	data += strings.Repeat("=", missing)
	res, err := base64.URLEncoding.DecodeString(data)
	fmt.Println("  decodebase64urlsafe is :", string(res), err)
	return base64.URLEncoding.DecodeString(data)
}
func Base64UrlSafeEncode(source []byte) string {
	// Base64 Url Safe is the same as Base64 but does not contain '/' and '+' (replaced by '_' and '-') and trailing '=' are removed.
	bytearr := base64.StdEncoding.EncodeToString(source)
	safeurl := strings.Replace(string(bytearr), "/", "_", -1)
	safeurl = strings.Replace(safeurl, "+", "-", -1)
	safeurl = strings.Replace(safeurl, "=", "", -1)
	return safeurl
}
func AesDecrypt(crypted, key []byte) []byte {
	block, err := aes.NewCipher(key)
	if err != nil {
		fmt.Println("err is:", err)
	}
	blockMode := NewECBDecrypter(block)
	origData := make([]byte, len(crypted))
	blockMode.CryptBlocks(origData, crypted)
	origData = PKCS5UnPadding(origData)
	fmt.Println("source is :", origData, string(origData))
	return origData
}
func AesEncrypt(src, key string) []byte {
	block, err := aes.NewCipher([]byte(key))
	if err != nil {
		fmt.Println("key error1", err)
	}
	if src == "" {
		fmt.Println("plain content empty")
	}
	ecb := NewECBEncrypter(block)
	content := []byte(src)
	content = PKCS5Padding(content, block.BlockSize())
	crypted := make([]byte, len(content))
	ecb.CryptBlocks(crypted, content)
	fmt.Println("base64 result:", base64.StdEncoding.EncodeToString(crypted))
	fmt.Println("base64UrlSafe result:", Base64UrlSafeEncode(crypted))
	return crypted
}
/*
PKCS 방식에 따라 아래 함수들은 암, 복호화 함수 내에서 변경
*/
func PKCS5Padding(ciphertext []byte, blockSize int) []byte {
	padding := blockSize - len(ciphertext)%blockSize
	padtext := bytes.Repeat([]byte{byte(padding)}, padding)
	return append(ciphertext, padtext...)
}
func PKCS5UnPadding(origData []byte) []byte {
	length := len(origData)
	// 去掉最后一个字节 unpadding 次
	unpadding := int(origData[length-1])
	return origData[:(length - unpadding)]
}
func pkcs7Pad(data []byte, blocklen int) ([]byte, error) {
	if blocklen <= 0 {
		return nil, fmt.Errorf("invalid blocklen %d", blocklen)
	}
	padlen := 1
	for ((len(data) + padlen) % blocklen) != 0 {
		padlen = padlen + 1
	}
	pad := bytes.Repeat([]byte{byte(padlen)}, padlen)
	return append(data, pad...), nil
}
func pkcs7Unpad(data []byte) ([]byte, error) {
	padlen := int(data[len(data)-1])
	// check padding
	pad := data[len(data)-padlen:]
	for i := 0; i < padlen; i++ {
		if pad[i] != byte(padlen) {
			return nil, fmt.Errorf("invalid padding")
		}
	}
	return data[:len(data)-padlen], nil
}
type ecb struct {
	b         cipher.Block
	blockSize int
}
func newECB(b cipher.Block) *ecb {
	return &ecb{
		b:         b,
		blockSize: b.BlockSize(),
	}
}
type ecbEncrypter ecb
// NewECBEncrypter returns a BlockMode which encrypts in electronic code book
// mode, using the given Block.
func NewECBEncrypter(b cipher.Block) cipher.BlockMode {
	return (*ecbEncrypter)(newECB(b))
}
func (x *ecbEncrypter) BlockSize() int { return x.blockSize }
func (x *ecbEncrypter) CryptBlocks(dst, src []byte) {
	if len(src)%x.blockSize != 0 {
		panic("crypto/cipher: input not full blocks")
	}
	if len(dst) < len(src) {
		panic("crypto/cipher: output smaller than input")
	}
	for len(src) > 0 {
		x.b.Encrypt(dst, src[:x.blockSize])
		src = src[x.blockSize:]
		dst = dst[x.blockSize:]
	}
}
type ecbDecrypter ecb
// NewECBDecrypter returns a BlockMode which decrypts in electronic code book
// mode, using the given Block.
func NewECBDecrypter(b cipher.Block) cipher.BlockMode {
	return (*ecbDecrypter)(newECB(b))
}
func (x *ecbDecrypter) BlockSize() int { return x.blockSize }
func (x *ecbDecrypter) CryptBlocks(dst, src []byte) {
	if len(src)%x.blockSize != 0 {
		panic("crypto/cipher: input not full blocks")
	}
	if len(dst) < len(src) {
		panic("crypto/cipher: output smaller than input")
	}
	for len(src) > 0 {
		x.b.Decrypt(dst, src[:x.blockSize])
		src = src[x.blockSize:]
		dst = dst[x.blockSize:]
	}
}


Go에서는 AES의 ECB모드의 보안 취약점때문에 메소드를 지원하지 않습니다. 따라서 위와 같이 직접 구현해야 합니다.

'언어 > GO' 카테고리의 다른 글

[GO] 인터페이스 (Interface)  (0) 2017.02.20
[GO] 파이썬 모듈 실행하기  (0) 2017.02.18
[GO] AES ECB 모드 암호화 (PKCS5, PKCS7)  (0) 2017.02.17
[GO] JSON 사용하기  (0) 2017.02.16
[GO] const와 iota  (0) 2017.02.15
[GO] 구조체 (struct)  (0) 2017.02.15

+ Recent posts