Tech

A Tour of Go

euidong 2021. 3. 16. 19:55

Basics

Packages

모든 Go program은 package로 이루어집니다.
그 중에서도 main package를 program의 진입점으로 사용합니다.

package 안에서는 다른 package를 import하여 사용하는 것이 가능합니다.
또한, 기본적으로 사용 시에 사용하는 이름은 import한 파일의 마지막 path와 같습니다.

import를 수행하는 것은 두 가지 방법이 있습니다. 아래 두가지 방법은 완벽하게 동일합니다.

import "fmt"
import "math"
import (
    "fmt"
    "math"
)

또한, package를 외부에 export하기 위해서 해주어야 할 것은 첫글자를 대문자 표시하는 것입니다. (Pascal)
만약, export를 하기 싫다면 첫글자는 소문자로 표기해주면 됩니다.

Function

함수는 0개 이상의 인자를 입력받을 수 있습니다.
또한, 기본적으로, variable 뒤에 type을 선언합니다.
(이렇게 하는 이유는 더 가독성을 높이기 위해서라도 구글이 이야기 합니다. C++의 pointer를 사용할 때나, 함수 pointer를 사용할 때 등 등)

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

또한, 여러 개의 type을 한 번에 지정하는 것도 가능합니다.

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

리턴값 또한 몇 개든 리턴이 가능합니다.

func swap(x, y string) (string, string) {
    return y, x
}

또한, 이름을 통한 리턴도 가능합니다. 하지만, 대게의 경우 가독성을 많이 저해하므로 안쓰는 것이 좋을 거 같습니다.

func split(sum int) (x, y int) {
    x = sum * 4 / 9
    y = sum - x
    return
}

Variables

var를 이용하면, 변수를 선언할 수 있다. 여러 개의 변수를 한 번에 선언하는 것도 가능합니다. 이는 package or function level로 묶여진다.
또한 var에 바로 값을 대입하는 경우에는 type을 쓰지 않아도 무방하다. var의 각 요소는 반드시 하나의 type을 가질 필요는 없다.

package main

import "fmt"

var c, python, java bool
var i, j int = 1, 2

func main() {
    var go, nodeJS = true, "no!"
    fmt.Println(i, c, python, java)
}

function 안 에서는 :=를 이용하여 var와 type을 동시에 생략할 수 있다. 하짐만, 함수 밖에서는 반드시 var를 선언해주어야 한다.

package main

import "fmt"

func main() {
    var i, j int = 1, 2
    k := 3
    c, python, java := true, false, "no!"

    fmt.Println(i, j, k, c, python, java)
}

type

bool

string

int int8 int16 int32 int64
uint uint8 uint16 uint32 uint64

byte // uint8에 속하며, byte값을 저장합니다.

rune // int32에 속하며, unicode값을 저장합니다.

float32 float64

complex64 complex128 // 복소수나 무리수같은 수를 저장할 수 있음.

package main

import (
    "fmt"
    "math/cmplx"
)

var (
    ToBe   bool       = false
    MaxInt uint64     = 1<<64 - 1
    z      complex128 = cmplx.Sqrt(-5 + 12i)
)

func main() {
    fmt.Printf("Type: %T Value: %v\n", ToBe, ToBe)
    fmt.Printf("Type: %T Value: %v\n", MaxInt, MaxInt)
    fmt.Printf("Type: %T Value: %v\n", z, z)
}

각 데이터는 선언과 함께 초기화를 해주지 않으면, zero value가 들어갑니다.
int => 0
bool => false
string => ""

위에서 배운대로 type을 쓰지 않고, 바로 초기화를 수행한 경우 go는 type을 스스로 추론하여 넣습니다. 이때 들어갈 수 있는 값은
int, float64, complex128, string, bool 등이 들어갈 수 있다.

type conversion

type을 변환(형변환)을 수행할 때에는 T(v)를 통해 쉽게 수행이 가능합니다.

package main

import (
    "fmt"
    "math"
)

func main() {
    var x, y int = 3, 4
    var f float64 = math.Sqrt(float64(x*x + y*y))
    var z uint = uint(f)
    fmt.Println(x, y, z)
}

constants

변하지 않는 상수값을 지정하고자 할 때, 사용할 수 있으면 var 대신에 const를 사용한다.
이때에는 :=를 사용할 수 없다는 것을 알 수 있을 것이다.

package main

import "fmt"

const Pi = 3.14

func main() {
    const World = "世界"
    fmt.Println("Hello", World)
    fmt.Println("Happy", Pi, "Day")

    const Truth = true
    fmt.Println("Go rules?", Truth)
}

또한, 숫자형을 표현하는 constants는 매우 큰 값도 표현할 수 있다.

package main

import "fmt"

const (
    // Create a huge number by shifting a 1 bit left 100 places.
    // In other words, the binary number that is 1 followed by 100 zeroes.
    Big = 1 << 100
)

func needFloat(x float64) float64 {
    return x * 0.1
}

func main() {
    fmt.Println(needFloat(Big))
}

Flow Control

For

Go는 loop문이 for밖에 없습니다.
for는 3개로 구성되어지며 이들은 ;으로 구분됩니다. (괄호 없는 거 빼고 C++과 동일합니다.)

  • First: 최초 진입 시에 수행되는 초기화 구문입니다. (optional)
  • Second : 매 반복 이전에 조건을 확인하는 구문입니다. (optional)
  • Third : 매 반복 이후에 수행할 동작입니다. (optional)
package main

import "fmt"

func main() {
    sum := 0
    // 다음과 같은 모든 형태를 이용하는 것이 가능합니다.
    for i := 0; i < 10; i++ {
        sum += i
    }
    for ; sum < 100 ; {
        sum += sum
    }
    // while문과 같은 형태
    for sum < 1000 {
        sum += sum
    }
    // 만약에, for문에 조건을 주지않으면 영원히 동작합니다.
    for {
        sum += sum
    }
    fmt.Println(sum)
}

If

조건문을 표현합니다. for문과 같이 소괄호(())는 존재하지 않고, 중괄호({})만 존재합니다.
for문과 같이 if-block 안에서만 사용할 변수를 할당할 수 있습니다.
if와 else if 그리고 else를 사용하여 조건문을 모두 컨트롤할 수 있습니다.

package main

import (
    "fmt"
    "math"
)

func sqrt(x float64) string {
    if x < 0 {
        return sqrt(-x) + "i"
    }
    return fmt.Sprint(math.Sqrt(x))
}

func pow(x, n, lim float64) float64 {
    if v := math.Pow(x, n); v < lim {
        return v
    } else {
        fmt.Printf("%g >= %g\n", v, lim)
    }
    // can't use v here, though
    return lim
}

func main() {
    fmt.Println(sqrt(2), sqrt(-4))
    fmt.Println(
        pow(3, 2, 10),
        pow(3, 3, 20),
    )
}

Switch

동일한 변수에 대한 조건문을 수행할 때, switch문은 if-else 구문을 더 짧게 만들 때 사용된다.
다른 언어들과는 달리 Go는 break를 자동적으로 포함한다.
또한, 조건으로 들어오는 값이 constants일 필요는 없으며, 조건없이 바로 표현하는 것도 가능하다.

package main

import (
    "fmt"
    "runtime"
    "time"
)

func main() {
    fmt.Print("Go runs on ")
    switch os := runtime.GOOS; os {
    case "darwin":
        fmt.Println("OS X.")
    case "linux":
        fmt.Println("Linux.")
    default:
        // freebsd, openbsd,
        // plan9, windows...
        fmt.Printf("%s.\n", os)
    }

    t := time.Now()
    switch {
    case t.Hour() < 12:
        fmt.Println("Good morning!")
    case t.Hour() < 17:
        fmt.Println("Good afternoon.")
    default:
        fmt.Println("Good evening.")
    }
}

defer

해당 block안의 함수들의 값이 return 될 때까지 동작을 연기합니다.
해당 defer는 stack의 형태로 누적되며 가장 먼저 defer 선언된 것이 가장 늦게 수행된다.

package main

import "fmt"

func main() {
    defer fmt.Println("world")

    fmt.Println("hello")
}
hello
world

More types

Pointer

data의 메모리 주소를 소유하고 있는 변수를 pointer라고 합니다.
기본적으로 *T의 형태로 사용하고, 이것의 zero value는 nil입니다. (nothing이라는 뜻)

특정 변수의 주소값을 받기 위해서는 &를 이용합니다. 또한, pointer가 가르키는 변수의 값을 받기 위해서는 *을 사용합니다.

var p *int

i := 42
p = &i
j := *p

Struct

Struct는 여러 개의 타입을 모아서 보관할 수는 있는 또 하나의 type을 만들어냅니다.
Struct를 type으로 하는 변수에서 하위 데이터에 접근할 때에는 .을 이용합니다.

package main

import "fmt"

type Vertex struct {
    X int
    Y int
}

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

Array

[n]T의 형태로 표기합니다. array의 길이는 type에 속하기 때문에 이를 변경하는 것은 불가능하다.

package main

import "fmt"

func main() {
    var a [2]string
    a[0] = "Hello"
    a[1] = "World"
    fmt.Println(a[0], a[1])
    fmt.Println(a)

    primes := [6]int{2, 3, 5, 7, 11, 13}
    fmt.Println(primes)
}

slice

  • 기본적으로 []T와 같이 처음에 크기를 선언해주지 않은 배열을 slice라고 명명하는데, 이는 동적인 사이즈를 가질 수 있다.
  • 선언 시에는 특정 배열의 시작점과 끝점을 지정함으로서 할당이 가능하다. 이는 half-open range로 시작점은 포함하고, 끝점은 포함하지 않는다.
  • 또한, 이는 원본 배열을 기반으로 함으로 원본이 바뀌거나 slice 데이터가 바뀐다면, 서로 영향을 받습니다.
  • 시작점에 0이 들어가는 경우는 생략할 수 있고, 끝점에 원본 배열의 크기가 들어가는 경우에도 이를 생략하는 것이 가능합니다.
  • slice의 length와 capacity는 다른 값을 가집니다. 기본적으로 length는 실제 slice의 길이를 의미하며, capacity는 시작점을 기준으로 하여 최상단의 원본 array의 담는 공간을 의미합니다. 시작점이 n이라면, len(array) - n으로 적용합니다.
  • slice는 reference 기반인 만큼 원본 array가 없는 경우, zero value는 nil입니다.
package main

import "fmt"

func main() {
    primes := [6]int{2, 3, 5, 7, 11, 13}

    var s []int = primes[1:4] // {3, 5, 7}
    fmt.Println(s)
}

만약에, slice를 다음과 같이 선언한다면, 데이터를 만들고, 그것에 대한 reference를 이용해서 slice를 만든다는 것을 의미합니다.

q := []int{2, 3, 5, 7, 11, 13}

// w := [6]int{2, 3, 5, 7, 11, 13}
// q := []int = w[:]

동적으로 slice를 만들기 위해서, make라는 built-in 함수를 이용할 수 있습니다.
make는 2 ~ 3개의 인자를 받을 수 있습니다.
3개의 인자를 받을 때에는,
첫 번째는 slice의 type이 들어가고,
두 번째에는 slice의 length가 들어가고,
세 번째에는 slice의 capacity가 들어갑니다.
만약 인자를 2개 받을 때에는 length = capacity라는 의미의 함축형으로 하나의 값만을 받습니다.
그 안에 들어가는 데이터는 각 slice의 기반 type의 zero value가 들어갑니다.

package main

import "fmt"

func main() {
    a := make([]int, 5)
    printSlice("a", a) // a len=5 cap=5 [0 0 0 0 0]

    b := make([]int, 2, 5)
    printSlice("b", b) // b len=2 cap=5 [0 0]

    c := b[:2]
    printSlice("c", c) // c len=2 cap=5 [0 0]

    d := c[2:5]
    printSlice("d", d) // d len=3 cap=3 [0 0 0]
}

func printSlice(s string, x []int) {
    fmt.Printf("%s len=%d cap=%d %v\n",
        s, len(x), cap(x), x)
}

이중 slice 역시 동일하게 생성이 가능합니다.

package main

import (
    "fmt"
    "strings"
)

func main() {
    // Create a tic-tac-toe board.
    board := [][]string{
        []string{"_", "_", "_"},
        []string{"_", "_", "_"},
        []string{"_", "_", "_"},
    }

    // The players take turns.
    board[0][0] = "X"
    board[2][2] = "O"
    board[1][2] = "X"
    board[1][0] = "O"
    board[0][2] = "X"

    for i := 0; i < len(board); i++ {
        fmt.Printf("%s\n", strings.Join(board[i], " "))
    }
}

만약, slice에 새로운 데이터를 추가하고자 한다면, append를 사용할 수 있습니다. append를 수행하면, capacity가 충분하다면, 기존 배열에 해당 값을 덮어씌우는 연산이 이루어지게 되고, capacity가 부족하다면, 아예 새로운 array를 생성하고, 그것으로 reference가 변경되며 연산이 이루어집니다.

func append(s []T, vs ...T) []T

다음과 같이 value를 여러 개 넣을 수 있으며, 하나라도 capacity를 벗어나면, 아예 새로운 array가 생성되며 원본 값은 바뀌지 않습니다.
또한, 유의해서 볼 점은 생성되는 새로운 array의 capacity가 1을 제외하고는 2의 배수로만 이루어진다는 것이다.

package main

import "fmt"

func main() {
    var a [3]int
    var s []int = a[:0]
    var t []int
    printSlice(s) // len=0 cap=3 []

    // append works on nil slices.
    s = append(s, 0)
    printSlice(s) // len=1 cap=3 [0]

    // The slice grows as needed.
    t = append(s, 1)
    printSlice(s) // len=1 cap=3 [0]
    printSlice(t) // len=2 cap=3 [0 1]

    // We can add more than one element at a time.
    s = append(s, 2, 3, 4)
    printSlice(s) // len=4 cap=6 [0 2 3 4]
    printSlice(t) // len=2 cap=3 [0 1]
    fmt.Printf("%d\n", a[1]); // 1
}

func printSlice(s []int) {
    fmt.Printf("len=%d cap=%d %v\n", len(s), cap(s), s)
}

Range

slice or map을 이용할 시에 for loop에서 range를 사용할 수 있다.
이를 이용하게 되면 매 반복마다 두 개의 값을 얻을 수 있는데, 하나는 배열의 index값이고, 하나는 해당 배열의 값이다.
원치 않는 데이터는 _ 로 대체할 수 있다. (value는 skip도 가능)

package main

import "fmt"

var pow = []int{1, 2, 4, 8, 16, 32, 64, 128}

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

Maps

  • map은 key와 value를 mapping하는 자료구조입니다.
  • map의 zero value는 nil입니다.
  • nil 상태의 map은 key가 없으며, 값을 추가할 수도 없습니다.
  • make함수를 통해서 타입을 지정하고, 값을 초기화하여 사용할 준비를 해야합니다.
package main

import "fmt"

type Vertex struct {
    Lat, Long float64
}

var m map[string]Vertex

func main() {
    m = make(map[string]Vertex)
    m["Bell Labs"] = Vertex{
        40.68433, -74.39967,
    }
    fmt.Println(m["Bell Labs"])
}

또는, make없이 바로 정의를 할 수도 있습니다.

package main

import "fmt"

type Vertex struct {
    Lat, Long float64
}

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

func main() {
    fmt.Println(m)
}

기본적인 사용 방법은 다음과 같습니다.

- 값을 업데이트하거나 새로운 key, value를 삽입하기
    ```go
    m[key] = elem
    ```
- 특정 key의 값을 가져오기
    ```go
    elem = m[key]
    ```
- 특정 key의 값을 삭제하기
    ```go
    delete(m, key)
    ```
- 해당 key가 존재하는지 확인하기 (존재한다면, key = true이고, 존재하지 않는다면 false이며 elem은 zero value가 된다.)
    ```go
    elem, ok = m[key]
    ```

Function values

함수도 변수이다. 다른 변수처럼 취급하는 것이 가능하다.

package main

import (
    "fmt"
    "math"
)

func compute(fn func(float64, float64) float64) float64 {
    return fn(3, 4)
}

func main() {
    hypot := func(x, y float64) float64 {
        return math.Sqrt(x*x + y*y)
    }
    fmt.Println(hypot(5, 12))

    fmt.Println(compute(hypot))
    fmt.Println(compute(math.Pow))
}

함수를 변수로 저장하게 되면, 이는 closure를 지원할 수 있습니다.
c++에서는 static을 이용하여, 변수를 공유하는 등의 작업을 할 수 있는데, closure를 이용하면, 특정 함수에게 데이터를 저장하도록 만들 수 있습니다.

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),
        )
    }
}

다음과 같은 예시에서 pos와 neg라는 함수 변수는 기본적으로 sum에 인자로 들어오는 값을 더 하여 return하는 함수입니다. 하지만, 해당 함수는 sum이 무엇인지를 기본적으로 알지 못합니다. 그런데, go에서는 sum이라는 변수를 메모리에 저장하여, 해당 함수 변수에 구속할 수 있습니다. 해당 문서에서는 closure를 함수 바깥의 변수를 참조하는 함수의 변수라고 정의합니다. 위의 예시를 다시보며 의미를 곱씹으면 이해할 수 있을 것입니다.

Method & Interface

Method

  • Go는 Class가 존재하지 않지만, type에 method를 정의할 수 있다.
  • 특별한 receiver 인자를 가진 함수를 통해서 이를 구현할 수 있는데, 그 receiver는 `func` keyword와 method의 이름 사이에 존재한다.
  • 해당 함수(Method)는 receiver를 제외한다면 평범한 함수이다.
  • receiver를 pointer로 전달하는 것과 value로 전달하는 것은 원본을 변경하느냐 하지않느냐 라는 면에서 다르다. value로 보내는 경우에는 해당 receiver를 복사한 값을 이용해서 연산을 처리하는 것이고, pointer를 사용하는 것은 직접 적으로 원본에 개입하게 된다. (여기서 pointer receiver랑 receiver가 호출할 때는 동일한 형태인 거에 의문을 가질 수 있는데 이는 Go가 이를 자동으로 처리하기 때문에 동일하게 사용할 수 있는 것이다. -> 우리가 모르게 작용을 해주고 있었던 것)
  • pointer를 사용하지 않는 경우는 immutable과 같은 속성을 지켜주고 싶을 때, 사용할 수 있지만, receiver의 크기가 커질 수록 부담이 될 수도 있다는 것을 고려해야 한다.
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 (v *Vertex) Scale(f float64) {
    v.X = v.X * f
    v.Y = v.Y * f
}

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

Interface

  • interface type은 method의 집합을 선언할 수 있다.
  • 해당 interface type의 변수는 선언한 모든 함수를 구현한 어떤 변수든 할당할 수 있습니다.
  • 여기서 유의해야 하는 것은 변수의 receiver를 value와 pointer를 명확하게 mapping해주어야 한다는 것입니다.
  • 명시적으로 특정 type이 특정 interface를 구현한다는 것을 드러낼 필요없다.
  • interface value는 실제 값과 해당 값의 type을 포함한 tuple로 볼 수 있다.
package main

import (
    "fmt"
    "math"
)

type Abser interface {
    Abs() float64
}

func main() {
    var a Abser
    f := MyFloat(-math.Sqrt2)
    v := Vertex{3, 4}

    a = f  // a MyFloat implements Abser
    a = &v // a *Vertex implements Abser

    // ! 해당 라인에서는, v 는 Vertex이고, *Vertex 이므로, 이는 에러를 일으킨다.
    a = v

    fmt.Println(a.Abs())
}

type MyFloat float64

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

type Vertex struct {
    X, Y float64
}

func (v Vertex) Abs() float64 {
    return math.Sqrt(v.X*v.X + v.Y*v.Y)
}
  • 또한, 일반적으로 타 언어에서는 interface value가 가르키는 값이 null인 값을 활용하는 것은, null pointer exception을 유발하지만, Go에서는 nil형태의 interface value가 일반적이라고 여긴다.
  • 하지만, 가르키는 값 자체가 없는데 이를 활용하는 것은 runtime error를 유발한다.
package main

import "fmt"

type I interface {
    M()
}

type T struct {
    S string
}

func (t *T) M() {
    if t == nil {
        fmt.Println("<nil>")
        return
    }
    fmt.Println(t.S)
}

func main() {
    var i I
    describe(i)
    i.M() 

    var t *T
    i = t
    describe(i)
    i.M() // run time error 발생

    i = &T{"hello"}
    describe(i)
    i.M()
}

func describe(i I) {
    fmt.Printf("(%v, %T)\n", i, i)
}
  • 완전 비어있는 interface는 무엇이든 담을 수 있는 변수가 된다.
package main

import "fmt"

func main() {
    var i interface{}
    describe(i)

    i = 42
    describe(i)

    i = "hello"
    describe(i)
}

func describe(i interface{}) {
    fmt.Printf("(%v, %T)\n", i, i)
}

Type assertion

interface value의 type을 확인할 수 있는 방법은 t, ok := i.(T)를 수행해보는 것이다.
만약, T의 type과 interface value의 type이 동일하다면, ok = true 그렇지 않다면, false가 return 되며 t에는 zero value가 들어간다. 여기서 또 주목할 것은 만약 ok return값을 받아주지 않으면 type이 맞지 않으면 에러를 발생시킨다.

package main

import "fmt"

func main() {
    var i interface{} = "hello"

    s := i.(string)
    fmt.Println(s)

    s, ok := i.(string)
    fmt.Println(s, ok)

    f, ok := i.(float64)
    fmt.Println(f, ok)

    f = i.(float64) // panic
    fmt.Println(f)
}
  • switch 구문을 이용해서 interface의 type 마다 다른 행동을 수행하도록 할 수 있다.
package main

import "fmt"

func do(i interface{}) {
    switch v := i.(type) {
    case int:
        fmt.Printf("Twice %v is %v\n", v, v*2)
    case string:
        fmt.Printf("%q is %v bytes long\n", v, len(v))
    default:
        fmt.Printf("I don't know about type %T!\n", v)
    }
}

func main() {
    do(21)
    do("hello")
    do(true)
}

Stringer

Stringer는 가장 흔하게 쓰이는 interface이다. 이는 fmt package 안에 정의되어 있다.
이는 fmt에서 특정 type을 출력할 때, 이를 이용해서 어떻게 출력할지를 지정할 수 있다.

type Stringer interface {
    String() string
}
package main

import "fmt"

type Person struct {
    Name string
    Age  int
}

func (p Person) String() string {
    return fmt.Sprintf("%v (%v years)", p.Name, p.Age)
}

func main() {
    a := Person{"Arthur Dent", 42}
    z := Person{"Zaphod Beeblebrox", 9001}
    fmt.Println(a, z)
}

Error

Go는 error value로 error 상태를 표현한다. error type은 built-in으로 포함된 interface이다.
이를 이용해서 다양한 에러 메세지를 표현할 수 있다. 대게 go에서는 함수는 error value를 같이 return하여 해당 함수의 error 여부를 표기하며, 이때 error value가 nil이라면 error가 없다고 간주한다.

type error interface {
    Error() string
}
package main

import (
    "fmt"
    "time"
)

type MyError struct {
    When time.Time
    What string
}

func (e *MyError) Error() string {
    return fmt.Sprintf("at %v, %s",
        e.When, e.What)
}

func run() error {
    return &MyError{
        time.Now(),
        "it didn't work",
    }
}

func main() {
    if err := run(); err != nil {
        fmt.Println(err)
    }
}

 

Concurreny

Goroutine

Go routine은 Go runtime에 의해서 관리되는 경량 thread 쓰레드이다.

특정 함수 앞에 go를 붙임으로서 goroutine을 실행시킬 수 있다.

go f(x, y, z)

goroutine은 동일한 주소 공간에서 동작하기에, 메모리를 공유한다 그렇기에 동기화를 수행해주는 것이 매우 중요하다.
sync package는 동시성에 대한 기능을 제공합니다.

Channel

<-라는 operator를 통해서 데이터를 주고 받을 수 있는 통로 type입니다. 특정 데이터를 channel에 넣는 것을 send라고 하며, channel로부터 데이터를 받는 것을 receive라고 합니다. 기본적으로 send와 receive는 반대편이 준비되기 전까지는 block됩니다. 즉, 받는 측에서 준비가 되지 않으면 수행되지 않습니다.

package main

import "fmt"

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

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

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

    fmt.Println(x, y, x+y)
}
  • Buffered Channel : channel에 크기를 지정하고, 해당 크기가 꽉 찰 때까지는 block하지 않습니다.
package main

import "fmt"

func main() {
    ch := make(chan int, 2)
    ch <- 1
    ch <- 2
    fmt.Println(<-ch)
    fmt.Println(<-ch)
}
  • Range and Close : sender는 channel을 닫을 수 있다. channel closed 되었다면, 더 이상 데이터의 변경은 일어나지 않는다. 이때 receiver는 해당 channel 닫혔는지를 다음과 같이 알 수 있다.
    v, ok := <- ch

또한, channel로 부터 데이터를 받기 위해서는 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)
}

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

Select

select 문은 goroutine이 여러 개의 동작이 있을 때, 동작을 제어할 수 있게 하낟.

select 구문 각 case를 기술하고, 해당 case를 만족하는 경우가 올 때까지 block된다. 만약, 여러 개가 ready되었다면, random으로 실행한다. default는 아무것도 ready가 되지 않았을 때, blocking이 아닌 다른 동작을 수행하도록 한다.

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)
}

Sync.Mutext

만약, 서로 정보 교환이 필요하지 않고, 오직 충돌을 피하기만을 원한다면, mutex를 사용할 수 있다.
이것을 mutex exclusion이라고 부르며, 이는 Go에서 Lock과 Unlock으로 구현된다.

package main

import (
    "fmt"
    "sync"
    "time"
)

// SafeCounter is safe to use concurrently.
type SafeCounter struct {
    mu sync.Mutex
    v  map[string]int
}

// Inc increments the counter for the given key.
func (c *SafeCounter) Inc(key string) {
    c.mu.Lock()
    // Lock so only one goroutine at a time can access the map c.v.
    c.v[key]++
    c.mu.Unlock()
}

// Value returns the current value of the counter for the given key.
func (c *SafeCounter) Value(key string) int {
    c.mu.Lock()
    // Lock so only one goroutine at a time can access the map c.v.
    defer c.mu.Unlock()
    return c.v[key]
}

func main() {
    c := SafeCounter{v: make(map[string]int)}
    for i := 0; i < 1000; i++ {
        go c.Inc("somekey")
    }

    time.Sleep(time.Second)
    fmt.Println(c.Value("somekey"))
}