web framework 역할과 종류 에서 Go에서 star별로 조사를 했다.
많은 WebFramework가 있지만 Gin이 star가 가장많기 때문에 선택을 해본다.

gin github
gin-gonic 에 들어가서 다운로드 받고, 배울수 있다.

Gin?

Gin은 golang으로 작성된 HTTP web framework를 말하고, 성능이 빠르다는 장점이 있다.
Logger, Authorization, GZIP, DB와 연동해서 사용할 수 있다.
뭐 다 좋다고 하겠으니 일단 사용해보기로!

어떻게 시작?

무조건 개발은 해보면서 습득하는게 좋으니 gin examples가 있는곳을 살펴보기로한다. Gin으로 작성된 Awesome Project의 리스트도 확인이 가능하다.
Awesome Project에 보면 TF와 함께 개인 사진 저장소를 구현한 페이지 photoprism가 있는데 완성도가 훌륭하다. 데모페이지를 들어가보면 정말 이런걸 만들고 싶다라는 생각이 드는 페이지가 보인다.

Quick Start

quick start를 하고 나면,

  • go get -u github.com/gin-gonic/gin
  • go get github.com/kardianos/govendor

설치를 govendor가 없다고 나온다. command not found
그 이유는 govendor의 환경변수를 지정하지 않아서 생기는 문제~ GOPATHGOBIN의 경로를 ~/.bashrc에 지정해주자

export GOPATH=~/go
export GOBIN=$GOPATH/bin
export PATH=$PATH:$GOBIN

다음과 같이 ~/.bashrc에 입력했다면 source ~/.bashrc를 입력해서 적용하면
govendor를 입력할 수 있을것이다.

예제 코드를 입력하고 http://localhost:8080/ping 실행하면 pong의 응답을 받는 예제이다.

Examples

git clone https://github.com/gin-gonic/examples.git의 소스코드를 내려받고,
https://gin-gonic.com/docs/examples/에서 예제에 대한 설명이 확인 가능하다.
다음에는 예제에서 각각 어떤 내용을 배우는지에 대한 정리를 해볼까한다.

~61까지 살펴보았고 이전 내용 살펴보기
이제는 62부터 살펴보자! Go 언어에서 동시성은 중요한 기능이라고 설명했는데, 어떤 기능이 있을까?

동시성

나는 이미 Concurrency에 대해서 이해를 하고 있는데,
조금더 디테일하게 알고싶으면 golang codewalk 를 참고

Goroutines

_고루틴은 Go 런타임에 의해 관리되는 경량 쓰레드이다.

go f(x, y, z) 의 코드는 새로운 고루틴을 시작시킨다.

현재의 고루틴에서는 f, x, y, z가 평가(evaluation) 되고,
새로운 고루틴 f가 수행(exeuction) 된다.

고루틴은 동일한 주소 공간에서 실행되므로, 공유되는 자원으로의 접근은 반드시 동기화가 되어야 한다.
golang sync에서 유용한 기본 기능을 제공하고 있다.

package main

import (
    "fmt"
    "time"
)

func say(s string) {
    for i := 0; i < 5; i++ {
        time.Sleep(100 * time.Millisecond)
        fmt.Println(s)
    }
}

func main() {
    go say("world")
    say("hello")
}

위 결과를 실행하면 hello, world가 번갈아가면서 수행이 된다.

Channels

채널은 채널 연산자 <-를 이용해 값을 주고 받을 수 있는 타입이 존재하는 파이프이다.
(데이터는 화살표 방향으로 흐른다.)

ch <- v // v를 ch로 보낸다.
v := <- ch // ch로부터 값을 받아서 v로 넘긴다. 

맵이나 슬라이스처럼 채널은 사용되기 전에 생성되어야 한다.

ch := make(chan int)

기본적으로 송/수신은 상대편이 준비될 때까지 블록된다.
이런 특성이 고루틴이 명시적인 락이나 조건 없이 동기화 될 수 있도록 돕는다.

아래 예제에서는 sumc로 보내고, c로 부터 받은 값을 x,y에 넘겨준다.

package main

import "fmt"

func sum(a []int, c chan int) {
    sum := 0
    for _, v := range a {
        sum += v
    }
    c <- sum // send sum to c
}

func main() {
    a := []int{7, 2, 8, -9, 4, 0}

    c := make(chan int)
    go sum(a[:len(a)/2], c)
    go sum(a[len(a)/2:], c)
    x, y := <-c, <-c // receive from c

    fmt.Println(x, y, x+y)
}

동시성을 지원하기 때문에 채널이 필요한건가...
위에 예제에서는 a라는 리스트의 모든 값을 더하는 과정인데,
두개의 고루틴을 이용해서 합을하고, 마지막에 두 값을 합치는 예제이다.
각각의 스레드에서의 결과는 c에 저장되고 종료되면 x, y로 가니 최종적으로 x+y를 계산
일단 사용성만 이해하는걸로라고 하고 넘어가려고 하니

65에서 버퍼링되는 채널의 주제가 나왔다.
채널이 버퍼링 될수가 있다고 하는데,

ch := make(chan int, 100) 의 용량을 지정할 수 있는데,
용량만큼 버퍼링되는 채널을 생성할 수 있다.
버퍼링되는 채널로의 송신은 버퍼가 꽉 찰 때까지 블록된다.
수신측은 버퍼가 빌 때 블록이된다.
언제 어떻게 써야하는지는 모르겠음 일단 버퍼 용량을 넣을 수 있다는 정도로 이해
웹에서 UI 업데이트를 동시에 하기 위해서 사용하려나 모르겠음.

Range와 Close

데이터 송신측은 더이상 보낼 값이 없다는 것을 알리기 위해 채널을 close할 수 있다.
수신측에서는 v, ok := <-ch 다음과 같이 두번째 인자를 줌으로써 채널이 닫혔는지 테스트 가능
채널이 이미 닫혔으면 false

주의 송신측만 채널을 닫을 수 있다. 수신측에서는 불가능 (이미 닫힌 채널에 데이터를 보내면 응?)
채널은 파일과 다르다. 항상 닫을 필요가 없다. range의 루프를 종료시키는 느낌과 유사함
더이상 값을 보낼게 없다고 말할때

package main

import (
    "fmt"
)

func fibonacci(n int, c chan int) {
    x, y := 0, 1
    for i := 0; i < n; i++ {
        c <- x
        x, y = y, x+y
    }
    close(c) // channel을 닫음
}

func main() {
    c := make(chan int, 10)
    go fibonacci(cap(c), c)
    for i := range c {
        fmt.Println(i)
    }
}

Select

고루틴이 다수의 통신 동작으로부터 수행 준비를 기다릴 수 있게 한다.
selectcase구문으로 받은 통신 동작들 중 하나가 수행될 수 있을때까지 수행을 블록
다수의 채널이 동시에 준비되면 그 중 하나를 무작위로 선택

package main

import "fmt"

func fibonacci(c, quit chan int) {
    x, y := 0, 1
    for {
        select {
        case c <- x:
            x, y = y, x+y
        case <-quit:
            fmt.Println("quit")
            return
        }
    }
}

func main() {
    c := make(chan int)
    quit := make(chan int)
    go func() {
        for i := 0; i < 10; i++ {
            fmt.Println(<-c)
        }
        quit <- 0
    }()
    fibonacci(c, quit)
}

select, case를 같이 쓰는 예제인것 같은데
이것도 어디에 어떻게 쓰일지... 감이 안오는데
여러가지 조건이 있을때 특정 값을 업데이트를 할때 사용?
웹에서 이점이 있을것 같은데.. 일단 이렇게 쓰이는구나... 정도만 이해

switch와 동일하게 case에서 default를 사용하게 되면
블로킹 없이(비동기적인) 송/수신을 하고자 할 때 default를 사용하면 된다.

select {
case i := <-c:
    // i를 사용
default:
    // c로부터의 수신은 블록된 상태
}

뒤에 연습문제도 있지만 집중력이 떨어짐
이후에 더 살펴보기 위해서는 레퍼런스, 튜토리얼, 비디오 자료갸 있는 Go 문서를 참고

이미 핫해진 GO 언어를 배워보자! 무작정 언어만 공부하기 보다는 웹사이트를 만들어 보기로한다.
우리가 함께하는 첫 공부가 재밌었으면 좋겠다.

2019.08.31 토요일 오후 4시 운동하고 와서
바람이 살랑 살랑 부는 LAPINE 카페에서 같이 공부를 시작한다.
(잠시 모카는 ... 후기를 작성중...)

요즘에는 node.js에서 GO로 웹프로그래밍이 많이 넘어가고 있고,
이미 구글에서 핫하게 밀고 있는 언어이고
배우기 쉽고, 빠르고 등등.. 그냥 장점만 들어서 일단 해보기로!

GO 시작하기

구글에서 제공하는 A Tour of GO를 통해서 한번
예제를 따라해보기로 한다. 영어를 좋아한다면 그대로 하지만 우리는 한글이 좋으니 한글 (A Tour of GO) 를 살펴보자~

다른 언어처럼 학습하는게 뭐 packages, variables, flow, types, method에 대해서
한번 살펴보고, Go에서 concurrency features에 대해서 살펴보면 위 사이트에서 얻을것은 끝!

다운로드는 여기서 맞춰서 받으면 된다.
다운로드를 받으면 아래 go가 실행 가능해진다.

$ go

일단 온라인으로 A Tour of GO를 수행해보고 오길 (offline으로도 가능한데 그냥 온라인으로 고고)

Packages, Import

packages를 추가하기 위해서는 디렉토리 경로의 마지막 이름을 사용하는게 규칙 이다.
예를 들어 "path/filepath" 라면 filepath가 패키지 명이다.

import를 하는 방법은 아래 두가지! 주의해야할 점은 여러개를 나열해도 ,를 입력하지 않는다.

import "fmt"
import "math"

또는

import (
  "fmt"
  "mat"
)

함수

함수는 int의 타입 매개변수를 두개 받고, 반환할 타입(int)를 지정하면 된다.
java, c와 다르게 뒤에 매개변수의 타입을 지정한다는게 다른점! 코드를 왼쪽에서 오른쪽으로 읽을때 자연스럽게 읽으라고 이렇게 했다는데...
그냥 따르지 왜 바꾼거니?

동일한 매개변수는 하나만 명시하고 나머지는 생략이 가능하다는점
named results의 경우에는 반환 값에 이름을 부여하여 변수처럼 사용이 가능하다.
즉 반환하는 변수에 매개변수의 타입 말고도 변수명을 함께 넘길 수 있음

func add(x int, y int) int {
    return x + y
}

// 또는

func add(x, y int) int {
    return x + y
}

// 여러개의 결과를 반환할때

func add(x, y int) (int, int) {
    return x, y
}

// Named results
func add(x, y int) (x, y int) {
    return x, y
}

함수 클로져 (Function closures) 아래 adder의 함수는 클로져를 반환한다.
각각의 클로져는 자신만의 sum 변수를 가진다.
neg, pos를 각각 sum에 대해서 업데이트가 필요하기 때문에 함수 클로져를 사용.
피보나치를 구현할때 사용한다.

package main

import "fmt"

func adder() func(int) int {
    sum := 0
    return func(x int) int {
        sum += x
        return sum
    }
}

func main() {
    pos, neg := adder(), adder()
    for i := 0; i < 10; i++ {
        fmt.Println(
            pos(i),
            neg(-2*i),
        )
    }
}

// fibonacci

// fibonacci is a function that returns 
// a function that returns an int.
func fibonacci() func() int {
    first, second := 0, 1
    return func() int {
        ret := first
        first, second = second, first+second
        return ret
    }
}

func main() {
    f := fibonacci()
    for i := 0; i < 10; i++ {
        fmt.Println(f())
    }
}

Vairbales

변수는 var를 사용하고 매개변수는 타입의 문장 끝에 명시한다.
특이하게 변수를 설정한다.

변수를 초기화할때는 변수 타입을 생략할 수 있다.
GO에서 알아서 초기화 하고 자하는 값에 따라 타입이 결정된다.

var x, y, z int
var c, python, java bool

var x, y, z int = 1, 2, 3
var c, python, java = true, false, "no"

함수 내에서는 :=을 사용하면 var과 명시적인 타입 (e.g int, bool)을 생략 할 수 있음
(그러나 함수 밖에서는 사용할 수 없음)

func main() {
    c, python, java := true, false, "no!"
}

상수 (Constants)는 문자, 문자열, 부울, 숫자 타입 중의 하나가 될 수 있다.
숫자형 상수 (Numeric Constnats)는 정밀한 값을 표현할 수 있음
타입을 지정하지 않고 문맥에 따라 타입을 가지게 된다.

const World = "안녕"
const True = true / false

반복문

Go 언어는 반복문이 for 밖에 없다. 사용할때 소괄호 없이 중괄호만 사용

sum := 0
for i := 0; i < 10; i++ {
  sum += i
}

// while 처럼 쓰기
sum :=1
for sum < 1000 {
    sum += sum
}

// 무한 루프
for {}

조건문

iffor와 동일하게 소괄호를 사용하지 않는다.

if x < 0 {
    return x
} else {
    return y
}

자료형

기본 자료형은 아래와 같다.

bool

string

int  int8  int16  int32  int64
uint uint8 uint16 uint32 uint64 uintptr

byte // uint8의 다른 이름(alias)

rune // int32의 다른 이름(alias)
     // 유니코드 코드 포인트 값을 표현합니다. 

float32 float64

complex64 complex128

구조체 (Structs)는 필드들의 조합이고,
속한 필드는 dot(.)으로 접근한다.

type Vertex struct {
    X int
    Y int
}

func main() {
    v := Vertex {1, 2}
    v.X = 4
} 

간단하고 쉽다며... C때 나왔던 포인터가 있다.
구조체 변수는 구조체 포인터를 이용해 접근할 수 있다.
포인터답게 수정하면 실제 구조체에도 영향을 미치겠지요?

func main() {
    p := Vertex{1,2}
    q := & p
    q.X = 1e9
}

구조체 리터럴은 필드와 값을 나열해서 구조체를 새로 할당하는 방법
원하는 필드를 {Name: value}구문을 통해서 할당 할 수 있음
필드 순서는 상관 없음!

r = Vertex{X: 1}

new(T)는 모든 필드가 0(zero value)이 할당된 T 타입의 포인터를 반환

var t *T = new(T)
t := new(T)
func main() {
    v := new(Vertex)
}

Slicing slices

여기서 슬라이스는 우리가 흔하게 알고 있는 리스트를 말한다.
슬라이스는 재분할 할 수도 있고, 같은 배열을 가리키는(point) 새로운 슬라이드를 만들 수도 있음

s[lo:hi] 위 표현은 lo에서 hi-1의 요소를 포함하는 슬라이스

p := []int {2,3,6,8,1}
p[1:4] # 3,6

슬라이스를 만들기 위해서는 make함수를 이용해서 만들수 있다.

a := make([]int, 5) // len(a) = 5

make 함수의 세번재 매개변수로 용량(capacity)를 제한 할 수 있음

b := make([]int, 0, 5)

var z []int를 통해 빈 슬라이스를 생성할 수 있는데,
zero value는 nill 이다. (언어마다 왜 다르게 하는거야...)

Range

for 반복문에서 range를 사용하면 슬라이스나 맵을 순회할 수 있다.
아래와 같이 하면 i에는 index (0,1,2,3,4,5,6,7), v에는 (1,2,4,8,16,32,64,128)

_을 사용하면 index 또는 value를 무시할 수 있다.

var pow = []int {1,2,3,4,5}
for i, v := range pow {
    fmt.Printf("2**%d = %d\n", i, v)
}

for _, v := range pow {
    fmt.Printf("%d\n", v)
}

Maps

맵은 키와 값을 넣는데, 맵은 반드시 사용하기 전에 make를 명시해야 한다.
뭔가 명시하고 넣는게 복잡해...

m = make(map[string]Vertex)
m["bell Labs"] = Vertex {40.78, -23}

맵 리터럴은 구조체와 비슷하지만 반드시 key를 지증해서 넣어야 한다.
리터럴에서 타입명(Vertex)를 생략해도 된다.

var m = map[string]Vertex{
    "Bell Labs": Vertex{
        40.68433, -74.39967,
    },
    "Google": Vertex{
        37.42202, -122.08408,
    },
}

var m = map[string]Vertex{
    "Bell Labs": {40.68433, -74.39967},
    "Google":    {37.42202, -122.08408},
}

map의 요소를 삽입하거나 수정하는 방법은

m[key] = value
value = m[key]
delete(m, key)
value, ok = m[key]

위의 ok값이 true이면 존재, 아니면 false

Switch

다른 언어에도 switch가 있지만, 다른 언어와 다른 점은 case의 코드 실행을 마치면 알아서 break
fallthrough로 끝나는 case는 스스로 break하지 않는다.

    switch os := runtime.GOOS; os {
    case "darwin":
        fmt.Println("OS X.")
    case "linux":
        fmt.Println("Linux.")
    default:
        // freebsd, openbsd,
        // plan9, windows...
        fmt.Printf("%s.", os)
    }

Methods

여기까지 50번째를 하고 있는데 대략 1시간 정도 소요 했다.
Go 에는 클래스가 없다. 하지만 메소드를 구조체에 붙일 수 있다.
메소드 리시버(method receiver)는 func 키워드와 메소드의 이름 사이에 인자로 들어간다.
Go 언어에서 처음보는 방식이다.

Vertex라는 구조체가 있고, 구조체에 함수(Abs)를 붙이고
그 붙인 함수를 v.Abs()를 통해 접근이 가능하다.

딱 한번에 봤을때 직관적이지 않지만, 클래스를 대체할 수 있을것 같다.

package main

import (
    "fmt"
    "math"
)

type Vertex struct {
    X, Y float64
}

func (v *Vertex) Abs() float64 {
    return math.Sqrt(v.X*v.X + v.Y*v.Y)
}

func main() {
    v := &Vertex{3, 4}
    fmt.Println(v.Abs())
}

구조체가 아닌 모든 변수에 붙일 수 있다.
MyFloat의 type을 정의하고, 그 타입에 Abs의 함수를 붙이면
f.Abs()로 접근이 가능하다.

package main

import (
    "fmt"
    "math"
)

type MyFloat float64

func (f MyFloat) Abs() float64 {
    if f < 0 {
        return float64(-f)
    }
    return float64(f)
}

func main() {
    f := MyFloat(-math.Sqrt2)
    fmt.Println(f.Abs())
}

포인터 리시버를 사용하는 이유는 두가지

  • 메소드가 호출될 때 마다 값이 복사되는 것 (큰 구조체 타입인 경우 값이 복사되는 것은 비효율적)을 방지하기 위해서
  • 메소드에서 리시버 포인터가 가르키는 값을 수정하기 위함

아래 예제에서 v를 Vertex 타입으로 받으면 Scale 메소드가 더 이상 동작하지 않는다.
Scale의 함수내에서는 v의 값을 변경하지만, v가 포인터가 아니기 때문에 원래의 값은 변경되지 않음.
그렇기 때문에 &Vertex의 포인터로 넘겨줘 값을 복사하지 않고, 메소드 리시버 포인터가 가르키는 값을 수정해야 한다.

package main

import (
    "fmt"
    "math"
)

type Vertex struct {
    X, Y float64
}

func (v *Vertex) Scale(f float64) {
    v.X = v.X * f
    v.Y = v.Y * f
}

func (v *Vertex) Abs() float64 {
    return math.Sqrt(v.X*v.X + v.Y*v.Y)
}

func main() {
    v := &Vertex{3, 4}
    v.Scale(5)
    fmt.Println(v, v.Abs())
}

너무 기네... 53부터는 다음에 이어서 하도록 하겠다. 53은 인터페이스! 이어서 하기

+ Recent posts