golang - 기본 문법

아래 글은 golang을 공부할 목적으로 웹에서 본 글들을 정리한 것이다.

지원 기능

  • tar, zip, zlib, gzip, bzip2
  • DES, AES, SHA1, SHA256, SHA512
  • PE, ELF, DWARF
  • BASE64, ASN.1, JSON, XML, PEM
  • HTTP, SMTP
  • 패키지 시스템
  • 빌드 시스템
  • 문서 시스템
  • 테스트 프레임워크
  • Google App Engine
  • 등등 많음

크로스 플랫폼 컴파일

  • Windows 32-bit용 바이너리 만들기 $ GOOS=windows GOARCH=386 go build
  • Linux 64-bit용 바이너리 만들기 $ GOOS=linux GOARCH=amd64 go build

특징

  • 강한 형 타입 & 덕 타입핑
  • 쓰기 쉬운 에러 처리
  • 짧게 쓸 수 있으므로 가독성이 좋다
    • 문법이 간단. C와 Python 중간 사이의 문법. 형추론에 의해 짧게 기술
    • 라이브러리 호출이 간단. 디렉토리 구조에 따른 패키지 시스템 외부 라이브러리도 직접 저장소를 지정 풍부한 표준 패키지(147개 이상의 표준 패키지) http://golang.org/pkg
  • GC Mark & Sweep GC
  • 병렬 프로그래밍
  • 사용하지 않는 import나 변수는 컴파일 시 에러가 된다.
  • 실행이 간단
    • 라이브러리 의존 관계를 자동으로 해결
    • 필요한 라이브러리를 자동으로 취즉. Git, Mercurial, Bazaar, Subversion
    • 고속 빌드
    • 정적 실행 바이너리
    • 빌드하지 않고 소스에서 직접 실행 가능 go run main.go
  • go에서 함수는 1급 오브젝트
  • 클로져도 많이 사용한다

리눅스 설치

  • 다운로드 후 압축을 풀고 /usr/local/ 에 이동
wget https://storage.googleapis.com/golang/go<버전>.linux-<아키텍쳐>.tar.gz
tar vxzf go<버전>.linux-<아키텍쳐>.tar.gz
sudo mv go /usr/local/

wget https://storage.googleapis.com/golang/go1.4.2.linux-amd64.tar.gz
tar vxzf go1.4.2.linux-amd64.tar.gz
sudo mv go /usr/local/  
</pre>

- 디렉토리 설정
echo "export PATH=$PATH:/usr/local/go/bin" >> .bashrc
source .bashrc
- GOPATH 설정 - project 디렉토리를 기본 디렉토리로 사용한다는 가정하고, 이 아래에 bin, pkg, src 디렉터리를 만든다. - bin: 소스 파일(패키지)를 컴파일하여 실행 파일(바이너리)이 생성되는 디렉터리입니다. - pkg: 패키지를 컴파일하여 라이브러리 파일이 생성되는 디렉터리입니다. pkg 디렉터리 아래에는 "운영체제_아키텍쳐" 형식으로 디렉터리가 생성됩니다. 64비트 리눅스라면 linux_amd64 디렉터리 아래에 라이브러리 파일이 생성됩니다. - src: 내가 작성한 소스 파일과 인터넷에서 자동으로 받아온 소스 파일이 저장되는 디렉터리입니다.
home/jacking/dev/golang/project 이
export GOPATH=/home/jacking/dev/golang/project
- 설정이 잘 되었는지 go evn 명령어로 본다. ### go Tour http://go-tour-kr.appspot.com/#1 ### 기본 구조 ``` // 디렉토리 단위로 패키지 명을 붙인다. package main //import할 패키지 명 import ( "fmt" ) //main 패키지에 main()을 두면 실행 형식이 만들어진다. func main() { // 패키지명.함수명 fmt.Println("Hello, world.") } ``` ![](/images/2018/golang/0005.PNG) ### import github에서 직접 import 할 수 있다. ``` import ( "fmt" "github.com/nabuchi/gosample" "strings" ) ``` ### 함수 - int 타입 인수x, y를 받고 int 타입의 결과를 돌려준다. ``` func add(x int, y int) int { return x + y } ``` - 가변 길이 인수 ``` func fArgs(strArgs ...string) { for index, value := range strArgs { fmt.Println(index, value) } } // 0 Go // 1 Java // 2 Ruby fArgs("Go", "Java", "Ruby") ``` - 복수의 값을 돌려줄 수 있다. → C 언어의 참조 전달을 할 필요가 없다 ``` func swap(x, y string) (string, string) { //x,y가 같은 타입이면 생략 가능 return y, x } c,d := swap("a", "b") ``` - 함수를 데이터로 가질 수 있다. ``` fn := function(a, b int) int { return a * b } ``` - 타입으로 선언(type 키워드) ``` package main // 함수의 형을 선언 type funcTemplate func(string) string func greet(name string) string { return "hello," + name } func x(f funcTemplate) { fmt.Println(fmt.Sprintf("%T", f)) } func main() { //main.funcTemplate가 호출된다 x(greet) } ``` - 익명함수를 함수의 인수로 사용 ``` func sumOf(f func(int) int, n, m int) int { a := 0 for ; n <= m; n++ { a += f(n) } return a } func main() { square := func(x int) int { return x * x } fmt.Println(square(10)) a := func(x int) int { return x * x }(10) fmt.Println(a) fmt.Println(sumOf(square, 1, 100)) fmt.Println(sumOf(func(x int) int { return x * x }, 1, 100)) } ```
출력
100
100
338350
338350
- 클로져를 사용할 수 있다 ``` func main() { x := 5 fn := func() { fmt.Println("x is", x) } fn() x++ fn() } ```
x is 5
x is 6
``` func intSeq() func() int { i := 0 return func() int { i += 1 return i } } func main() { nextInt := intSeq() fmt.Println(nextInt()) fmt.Println(nextInt()) fmt.Println(nextInt()) newInts := intSeq() fmt.Println(newInts()) } ```
1
2
3
1
### 변수 ``` // 타입은 뒤에. x,y,z는 int 타입 var x, y, z int //초기화자(initializer) 지정 시에는 타입을 지정하지 않아도 된다 var c, python, java = true, 3, "no" // 함수 내에서는 :=를 사용해서 var를 지정할 필요가 없다 c, python, java := true, 3, "no" ``` ### 기본 타입 - bool - string - int int8 int16 int32 int64 - uint uint8 uint16 uint32 uint64 - uintptr // 포인터 값을 그 대로 저장하므로 충분한 크기의 부호없는 정수 - byte // uint8 의 별칭 - rune // int32 의 별칭. Unicode 의 코드 포인터를 나타낸다. - float32 float64 - complex64 complex128 ### 상수 ``` // 타입은 지정하지 않는다 const Pi = 3.14 const ( A = 1 B = "test" ) ``` 상수를 정의할 때는 String() 메소드를 정의하는 것이 좋다. ``` type State int const ( Running State = iota Stopped Rebooting Terminated ) func main() { state := Running fmt.Println(state) // --> "0" d이 출력된다 } // 이것은 알기 힘드므로 이런 함수를 준비해 둔다. func (s State) String() string { switch s { case Running: return "Running" case Stopped: return "Stopped" case Rebooting: return "Rebooting" case Terminated: return "Terminated" default: return "Unknown" } } ``` 이렇게 하면 0 대신에 "Running"이 출력된다. 0을 사용하지 않을 때는 ``` const ( _ State = iota Running Stopped Rebooting Terminated ) ``` ### Enum ``` package main import ( "log" ) func init() { log.SetFlags(log.Lshortfile) } type Indexing int func (i Indexing) String() string { switch i { case WITH: return "WITH" case WITHOUT: return "WITHOUT" case NEVER: return "NEVER" } log.Fatal("can't be here") return "" } const ( WITH Indexing = iota WITHOUT NEVER ) func main() { log.Println(WITH, WITHOUT, NEVER) } ``` ``` const ( zero = iota != 1 // true one // false two // true three //false ) ``` ``` const ( zero = "zero" // zero one = iota // 1 two = "弐" // 弐 three = iota // 3 ) ``` ``` const ( iota = "iota" // iota zero = iota // iota one // iota two // iota three // iota ) ``` ### for문 ``` sum := 0 for i := 0; i < 10; i++ { sum += i } // while 문은 아래와 같이 for sum < 55 { } // 무한 루프 for { } // foreach for i, s:= range arr { fmt.Println(s) } ``` foreach 문을 사용할 때 value는 복사로 전달된다. 그러므로 요소의 크기가 큰 경우 조심해야 한다. 만약 foreach 문을 사용하면 요소 값 변경을 원한다면 아래와 같이 한다. ``` var array [10]MyType for idx, _ := range array { array[idx].field = "foo" } ``` For 문 중에서 Select를 사용할 때는 함수로 하는 것이 좋다(Wrap for-select idiom to a function) ``` func main() { L: for { select { case t1 := <-time.After(time.Second): fmt.Println("hello", t1) if t1.Second()%4 == 0 { break L } } } fmt.Println("ending") } ``` 위 코드는 1초마다 타임스탬프를 받고, 초가 4의 배수라면 멈추는 코드이다. 여기에서 라벨("L")을 사용하여 Break 하고 있는 것은 단순하게 Break 하려고 select를 던지는 것으로 For문을 빠져 나올 수 없기 때문이다. 올바른 라벨의 사용 방법은 맞지만 좋지 않다. Goto를 사용하는 것과 비슷하다. ``` func foo() { for { select { case t1 := <-time.After(time.Second): fmt.Println("hello", t1) if t1.Second()%4 == 0 { return } } } } func main() { foo() fmt.Println("ending") } ``` 위 코드 처럼 for 문을 함수안에 넣어서 return을 사용하여 for 문을 빠져 나오는 것이 좋다. ### if문 ``` if x < 0 { return sqrt(-x) + "i" } if a := 3; a < 5 { // if 문 내에 if 범위 내에서 유효한 변수를 선언 } fmt.Println(a) //범위를 벗어났으므로 에러 ``` ### switch문 - 기본으로 case 뒤에 break가 붙어므로 명시적으로 적을 필요가 없다. ``` switch i { case 0: fmt.Println("Zero") case 1: fmt.Println("One") case 2: fmt.Println("Two") case 3: fmt.Println("Three") case i>3: fmt.Println("More") // 평가식도 쓸 수 있다 default: fmt.Println("Unknown") } ``` - 만약 아래의 case문을 실행하고 싶다면 fallthrough 키워드를 사용하면 다음 case문을 실행한다. ``` i := 1 switch i { case 1: fmt.Println("i == 1") fallthrough case 2, 3, 4: fmt.Println("i == 2, 3 or 4") default: fmt.Println("All I know is that i is an integer") } ```
    실행 결과
    i == 1
    i == 2, 3 or 4
    
### 구조체 - 기본 ``` type Vertex struct { // public한 Vertex 타입 X int // public한 필드 변수 X, Y Y int } func main() { v := Vertex{1, 2} //v.X = 1; v.Y = 2로 초기화 v.X = 4 fmt.Println(v.X) } ``` - 구조체 변수 초기화 ``` type T struct { Foo string Bar int } //t := T{"example", 123} t := T{Foo: "example", Bar: 123} // 이렇게 하는 것이 유지보수에 좋다. ``` - 구조체 초기화는 변수가 많을 때는 여러 줄에 나누도록 한다. ``` T{Foo: "example", Bar:someLongVariable, Qux:anotherLongVariable, B: forgetToAddThisToo} T{ Foo: "example", Bar: someLongVariable, Qux: anotherLongVariable, B: forgetToAddThisToo, } ``` - 구조체에 &를 사용하여 초기화 하거나, new 키워드를 사용하면 포인터 형이 된다. ``` //Key:Value로 초기화. u4는 포인터 형 u4 := &User{name: "taro", age: 30} // var u4 *User fmt.Println("name:%s", u4.name) //new로 초기화. u5는 포인터 형 u5 := new(User) // var u5 *User u5.name = "taro" fmt.Println("name:%s", u5.name) ``` - 다른 구조체 포함 ``` type Foo struct { Bar name string fooProp string } type Bar struct { barProp string } foo := Foo{Bar{"barProp"}, "foo","fooProp"} fmt.Println(foo.name) // foo fmt.Println(foo.barProp) //barProp fmt.Println(foo.Bar.barProp) //barProp ``` 만약 Foo와 Bar가 같은 이름의 필드를 가지고 있는 경우 Bar의 필드에 접근하고 싶다면 꼭 foo.Bar.name으로 접근해야 한다. - 메소드 정의
func (수신자 이름  수신자의 타입) 함수명(인수) (반환 값)
``` type User struct { name string age int } func (u User) greet() string { return "hello," + u.name; } u := User {"taro",30} fmt.Println(u.greet()) ``` 같은 이름의 메소드라도 수신자가 다르면 다른 것으로 취급한다. ``` type Vertex struct { X, Y float64 } // Vertex 타입의 포인터 타입으로 Abs 함수를 정의 func (v *Vertex) Abs() float64 { // math 패키지 import 필요 return math.Sqrt(v.X*v.X + v.Y*v.Y) } func main() { v := &Vertex{3, 4} fmt.Println(v.Abs()) // 구조체 변수.함수 이름 으로 함수를 호출 } ``` 동일 패키지 상의 타입(기본 타입 이외)에 함수를 정의할 수 있다. 포인터 타입으로 해서 각 함수 호출 시 값 복사를 방지한다 또 포인터 타입이라면 struct의 값 변경을 할 수 있다. ### pointer ``` type Vertex struct { X int Y int } func main() { p := Vertex{1, 2} q := &p // p의 포인터를 할당 q.X = 1e9 // q는 p의 포인터이므로 p.X가 바뀐다. fmt.Println(p) } var ( p = Vertex{1, 2} // has type Vertex q = &Vertex{1, 2} // has type *Vertex r = Vertex{X: 1} // Y:0 is implicit s = Vertex{} // X:0 and Y:0 ) var q *Vertex = new(Vertex) // 포인터 변수를 만든다 ``` ### slice ``` // slices는 값 배열을 참조 // 길이((length)도 포함 // 배열도 있지만 slice로 대체할 수 있으므로 자제 func main() { p := []int{2, 3, 5, 7, 11, 13} // int의 slice fmt.Println("p ==", p) for i := 0; i < len(p); i++ { fmt.Printf("p[%d] == %d\n", i, p[i]) } } ``` - slicing → p[0:3] - 크기 지정 ``` a := make([]int, 5) // len(a) = 5, cap(a) = 5 a := make([]int, 0, 5) // len(a) = 0, cap(a) = 5 ``` - slice의 각 요소 처리 ``` for i, v := range p { // i는 인덱스, v는 값 } ``` - 인덱스를 사용하지 않는다면 _ 를 사용 ``` for _, v := range p { } ``` - append ``` b := append(a, 8) // a와 같은 타입으로 요소에 8을 추가한 slice를 반환한다. ``` ### map map은 키와 값을 관련짓는다 map의 초기화에는 make를 사용한다. ``` type Vertex struct { Lat, Long float64 } var m map[string]Vertex func main() { // 키가 string, 값이 Vertex인 map을 만든다 m = make(map[string]Vertex) m["Bell Labs"] = Vertex{ 40.68433, -74.39967, } fmt.Println(m["Bell Labs"]) } ``` #### 빈 맵을 만든는 패턴 ``` // 1: var variable map[ key_type ] value_type = map[ key_type ] valutetype_ var languages map[string]string = map[string]string{} // 실행 결과 //map[string]string(nil) //2: variable := make(map[ key_type ] value_type ) languages := make(map[string]string) // 실행 결과 // map[string]string{} // 3: variable := map[ key_type ] value_type {} languages := map[string]string{} // 실행 결과 // map[string]string{} ``` #### 요소가 있는 맵을 만드는 패턴 ``` // 1. var languages map[string]string = map[string]string{"go":"golang", "rb":"ruby", "js":"javascript"} // 실행 결과 // map[string]string{"go":"golang", "rb":"ruby", "js":"javascript"} //2. languages := map[string]string{"go":"golang", "rb":"ruby", "js":"javascript"} // 실행 결과 // map[string]string{"go":"golang", "rb":"ruby", "js":"javascript"} //3. 각 요소를 개행 하는 경우는 1 요소마다 컴마(,)를 붙이지 않으면 패스 에러가 된다 languages := map[string]string{ "go":"golang", "rb":"ruby", "js":"javascript", } // 4. make로 만들 때는 키를 지정해서 대입한다 languages := make(map[string]string) languages["go"] = "golang" languages["rb"] = "ruby" languages["js"] = "javascript" ``` ``` m := make(map[string]int) m["Answer"] = 42 fmt.Println("The value:", m["Answer"]) // 42를 출력 m["Answer"] = 48 fmt.Println("The value:", m["Answer"]) // 48를 출력 delete(m, "Answer") fmt.Println("The value:", m["Answer"]) // 0(int의 초기 값) v, ok := m["Answer"] // 키 유무 조사를 ok(bool)로 판정 fmt.Println("The value:", v, "Present?", ok) for k, v := range m { // 키와 값 조합으로 루프를 순회한다 fmt.Println(“Key:”, k, “, Value:”, v) } ``` #### 데이터 삭제하기 ``` a := map[strin]int { "Hello":10, "world":20 } delete(a, "world") ``` ### Intefaces - 인터페이스는 메소드를 모아놓고 인터페이스를 통해서 오브젝트의 행동을 정의할 수 있다. - 메소드를 모두 구현하고 있는 구조체라면 어떤 타입이라도 인터페이스의 대상으로 다룰 수 있다.
type <타입> interface {
	메소드 명(인수 타입, ...) (반환 값 타임, ...)
	...
}
</pre>  

```
// 인터페이스 정의
type Car interface {
	run(int) string
	stop()
}

// 구조체 정의
type MyCar struct {
	name  string
	speed int
}

// 메소드 구현
func (u *MyCar) run(speed int) string {
	u.speed = speed
	return strconv.Itoa(speed) + "km로 달린다"
}
func (u *MyCar) stop() {
	fmt.Println("정지한다")
	u.speed = 0
}

// 포인터
myCar := &MyCar{name: "내차", speed: 0}

var i Car = myCar
fmt.Println(i.run(50))
i.stop()

//실행 결과
//50km로 달린다
//정지한다
```  

새로운 구조체를 추가하는 경우 run과 stop를 정하면서 Car형으로 다룰 수 있다.  
  
- 빈 인터페이스  
빈 인터페이스는 하나라도 메소드를 정의하지 않는다  
인 인터페이스 자체는 아무런 기능이 없지만 어떤 타입이라도 대입할 수 있다.  
```  
// 빈 인터페이스 정의
var x interface{}
num := 0
str := "hello"
// 어떤 타입이라도 대입 가능
x = num
x = str
```  
이것을 이용하면 함수가 빈 인터페이스를 반환하는 것으로 임의의 타입의 값을 반환 할 수 있다.  
```
// 빈 인터페이스
type Element interface{}
// 적절한 값을 설정
var element Element = "hello"

// 어떤 타입에 매치할지 조사
if value, ok := element.(int); ok
{
  fmt.Println("int value:%d", value)
}
else if value, ok := element.(string); ok
{
  fmt.Println("string value:%d", value)
}
else {
  fmt.Println("other")
}
```  
'value, ok = element.(T)'로 하면 value는 변수 값이 저장되고, ok에는 bool 타입을 반환한다.  
만약 element 값이 인수의 타입에 매치된다면 ok에는 true가 반환된다.  
switch문을 사용할 수도 있다.  
```
switch value := element.(type) {
  case int:
	fmt.Println("int value:%d", value)
  case string:
	fmt.Println("string value:%d", value)
  default:
	fmt.Println("other")
}
```  
  
  
### Errors

```
import (
    "fmt"
    "strconv"
)

func main() {
    // 에러 가능성이 있는 함수는 복수의 값을 반환하는 함수로 된다.
    i, err := strconv.Atoi("102")
    if err != nil { // 변환 실패 시의 처리
    fmt.Println("Cannot parse : ", err)
    return
    }
    fmt.Println("Parsed: ", i)
}

// error 인터페이스는 아래처럼
type error interface {
    Error() string
}
```  
  
- panic/recover 라는 사양도 있지만 치명적인 에러인 경우에만 사용한다.  
  
  
### goroutine
go someFunc() // someFunc가 다른 goroutine에서 실행된다.  
  
- go 스테이트먼트는 독립된 goroutine(스레드)으로 함수 또는 메소드(구조체 함수)를 동일 주소 공간내에서 실행한다.
- 반환 값은 얻을 수 없다(파괴된다)
  
  
### channel
- go에 의해 병렬 처리 실행 자체는 쉽게 되었다.
- 병렬 처리한 결과를 모으기 위한 부분은 channel를 사용한다.
- channel은 채널 오퍼레이터 <- 을 이용하여 값의 송수신 할 수 있는 직통 루트 타입
- 액터 모델이라는 메일박스, Java의 LinkedBlockingQueue 등…
- 락에 의한 배타적 제어도 가능하지만 보통 사용하지 않는다.  
    ```
    c := make(chan int) // int 채널을 만든다
    c <- 3 // 3을 채널에 보낸다. c의 송신 준비가 끝날될까지 블럭
    v := <-c //c 에 값이 올때까지 블럭하여 값이 오면 받아들인다
    c := make(chan int, 100) // 수신 버퍼를 최대 100으로 한다. 이렇게하면 100까지 다 채워질 때까지 송신은 블럭되지 않는다.
    ```  
  
  
### slelect
- 복수의 채널에서 수신을 기다릴 경우 select를 사용한다.
- switch 문과 비슷한 문법이지만 case가 실행되는 것은 지정 channel이 송수신 할 수 있을 때
- 그때까지는 블럭  
```
select {
case c <- x: // channel x 에서 수신이 끝난 경우는 아래를 계산
 x, y = y, x+y

case <-quit: // channel quit 에서 수신이 끝났을 때는 처리 종료
 fmt.Println("quit")
 return
}
```  
  
  
### defer
- defer 뒤에 연속된 식을 쓴다  
```
// second()는 deferTest()에서 반환하기 직전에 실행된다
func deferTest() {
 defer second()
 first()
}

// 이런 경우 편리
f, _ := os.Open(filename)
defer f.Close() // 처리 종료 후에 확실하게 닫는다
```  
  
  
### 테스트
- 동일 디렉토리 내에 파일 이름을 xxx_test.go 로 두는 것이 일반적.
- 테스트 함수는 public
- 테스트 실행은 go test [파일 이름에서 .go를 제외한 것]
- t.Error 또는 t.Fatal 가 실행되면 그 테스트는 실패라고 판단  
```
import (
 "testing"
)

func TestFizzBuzz(t *testing.T) {
 if result := FizzBuzz(1); result != 1 {
 t.Error("Expected : 1, Actual : ", result)
 }
 if result := FizzBuzz(3); result != "Fizz" {
 t.Error("Expected : 3, Actual : ", result)
 }
}
```  
  
  
### 타입 assert

```
package main

import (
    "fmt"
)

func main() {
    check("Hello")
}

func check(i interface{}) {
    a, b := i.(string)
    fmt.Println(a)
    fmt.Println(b)
}
```  
결과
Hello
true
위 코드는 인수로서 받은 인터페이스형 i가 string 형인지 assert 하고 있다. assert한 결과 2개의 반환 값이 돌아오는데 위에서 말하면 a에는 string형으로 변환된 i가 저장되고 b에는 assert에 성공했는지를 bool 형의 값으로 저장. main에서는 string형의 문자열 "Hello"를 주고 있으므로 fmt.Println에서 출력 결과가 Hello와 true로 되어 있다. 타입 assert를 사용하면 인수로서 받은 인터페이스 형의 메소드 호출 앞에 그 메소드가 구현한 타입이 무엇인지를 조사할 수 있다. ``` func matchOS(any interface{}) bool { var envs []string if as, ok := any.([]string); ok { envs = as } else if s, ok := any.(string); ok { envs = []string{s} } else { return false } if has(envs, runtime.GOOS) { return true } return false } ``` 인수로 받은 인터페이스형 any가 string 배열형이면 그대로 envs에 대입, string형이라면 string배열형으로 변환해서 envs에 대입하고 있다 ``` if err != nil { if _, ok := err.(privKeyError); ok { log.Println("refusing to upload") } log.Fatal(err) } ``` err의 타입 assert 하여 err의 타입 조사를 해서 로깅 처리를 처리하고 있다. ### package import 하기 - ~/src/golang-book/chapter11/math 디렉토리에 main 이외의 패키지 파일을 만들었음. 이것을 main에서 호출하려면 ``` package main import "fmt" import "golang-book/chapter11/math" func main() { xs := []float64{1,2,3,4} avg := math.Average(xs) fmt.Println(avg) } ``` ### GO 언어 라이브러리 만들기 - https://hojunpark.wordpress.com/2015/06/30/go-%EC%96%B8%EC%96%B4-%EB%9D%BC%EC%9D%B4%EB%B8%8C%EB%9F%AC%EB%A6%AC-%EB%A7%8C%EB%93%A4%EA%B8%B0/ ### 메모리 모델 - The Go Memory Model - https://code.google.com/p/golang-korea/wiki/MemoryModel ### Go언어 관련 자주 묻는 질문 - https://code.google.com/p/golang-korea/wiki/GoFAQ

이 글은 2018-09-29에 작성되었습니다.