처음에 A Tour of Go를 본 이후 Language Specification을 읽으며 중요하다고 생각하는 내용을 요약한다.
소스코드는 UTF-8로 인코딩한다.
주석은 C++과 동일하다. (즉, //
과 /* ... */
)
대부분의 줄 마지막에 세미콜론이 자동으로 추가된다. 아래 셋은 같다. 이렇게 import
와
const
와 type
은 여러 항목을 괄호로 묶어서 같이 적을 수 있다.
import "fmt"
import "unicode"
import (
"fmt"
"unicode"
)
import ( "fmt"; "unicode" )
identifier는 유니코드문자 혹은 _
로 시작하고 뒤에는 유니코드문자와 유니코드숫자와
_
이 나올 수 있다. 단, _
만 쓰면 (blank identifier) 자리를 차지하며 무시한다.
x, y, z 값 중 y 값만 필요하면 _, y, _ := coord(p)
와 같이 적을 수 있다. 사용하지
않는 변수 때문에 컴파일이 안되는 현상을 막는다.
새로운 프로그래밍 언어를 볼 때 키워드를 제일 먼저 살펴본다. 먼저, C의 typedef
역할을
하는 type
은 자료형에 이름을 부여하며 자료형 switch문에 특이하게 사용되기도 한다. 다른
것들을 묶어서 자료형을 정의하는 struct
와 interface
도 키워드이다. chan
과 map
은
각각 채널과 해시맵 자료형을 위한 키워드이다. Go에는 goto
가 있고, break
와
continue
가 활용할 수 있다. Java의 finally
역할을 하는 defer
란 키워드도 있다.
new()
, make()
, len()
, delete()
등은 키워드가 아니라
붙밖이(built-in, predefined) 함수이다.
type T1 string
type T2 T1 // T2와 T1 모두 string 자료형. 그러나 서로 대입할 때는 명시적인 형변환 필요
문자 하나는 'a'
로 나타낸다. C의 escape 문자를 대부분 지원하지만, ''
안에서는
\"
를 사용할 수 없다. 또, 숫자값으로 표현할 경우 반드시 정해진 개수를 맞추어야 한다.
\777
(8진수 3개), \xff
(16진수 2개), \uffff
(16진수 4개), 'uffffffff
(16진수 8개)
문자열은 `...`
(raw string literal) 그리고 "..."
형태가 가능하다. raw
string literal 안에서는 escape 문자를 사용할 수 없지만 줄바꿈이 가능하다.
"..."
안에서는 \'
를 제외한 escape 문자를 사용할 수 있지만 줄바꿈이 불가하다.
bool 자료형은 true
와 false
가 (predefined identifier) 있다.
정수형은 [u]int[8|16|32|64]
가 있고, 크기를 명시하지 않은 uint
와 int
는 32비트
혹은 64비트 중 하나이다. byte
는 uint8
이고, 유니코드 문자를 나타내는 rune
은
int32
이다. 부동소수점과 복소수 자료형은 float[32|64]
, complex[64|128]
이다.
포인터 크기를 나타내는 uintptr
자료형도 있다. 앞에 나온 자료형 이름은 모두
predefined identifier이다.
배열 자료형은 [32]byte
같이 길이와 기본 자료형을 함께 사용한다. 두 배열은 길이와
기본 자료형 모두 동일해야 같은 자료형이다. [6]int{1, 2, 3, 5}
같이 초기화할 수 있고
(뒤에 두 항목은 int 자료형의 zero value인 0으로 초기화),
[...]string{"Sat", "Sun"}
처럼 길이를 생략하면 자동으로 계산된다. slice 예제이긴
하지만, [][]int{{1, 2, 3}, {4, 5}}
와 같이 중첩하여 초기화할 수 있다
([][]int{[]int{1, 2, 3}, []int{4, 5}}
와 동일).
zero value는 변수에 명시적으로 값을 초기화하지 않을 때 변수가 가지는 값으로, bool은
false
, 정수형은 0
, 부동소수점형은 0.0
, string은 ""
, 기타 포인터, 함수,
interface, slice, 채널, map은 nil
이다.
배열과 비슷하게 보이는 slice는 []byte
와 같이 길이가 없기 때문에 기본 자료형만 같아도
동일한 자료형이다. 보통 이미 만든 배열의 일부분을 지칭하는데 []int{1, 2, 3}
와 같이
바로 slice를 만들 수 있다 (tmp := [3]int{1, 2, 3}; tmp[0:3]
과 동일). slice는
배열은 물론이고 string도 감쌀 수 있다. slice[low:high]
에서 시작과 끝 모두 생략할
수 있다. slice[low:high:max]
형태는 cap(slice) = max - low 이고, 시작값을 생략할
수 있다. cap은 append()
등을 사용할 때 메모리를 크게 다시 할당할 필요없이 뒤에 값을
추가하기위한 용도이다. s[:]
는 slice 값을 그대로 반환한다. s[:cap(s)]
를 사용하면
cap까지 모두 포함한다.
string은 기본적으로 변경불가 []byte이다. 유니코드 값을 얻으려면 먼저
[]rune(문자열)
로 형변환한다. 정수형과 byte나 rune의 slice를 string으로 현변환할
수 있다. 반대로 string을 byte나 rune의 slice로 형변환할 수 있다. string(-1)
같이
유효하지 않은 값을 변환하려고 하면 "\ufffd"
(unicode replacement character)
결과값이 된다.
*Point(p) // *(Point(p))
(*Point)(p) // p 를 *Point 자료형으로 형변환
제한된 상황에서 unsafe
패키지를 사용하여 정수와 포인터 사이를 형변환할 수 있다.
unsafe.AlignOf()
, unsafe.Offsetof()
, unsafe.Sizeof()
결과값이 uintptr
과
unsafe.Pointer(&f)
의 결과값인 Pointer 사이에 형변환이 가능하다.
함수를 정의할 때 파라미터 목록 마지막에 쉼표를 더 적어도 된다. 반환값이 한개라면 반환값
선언을 감싸는 괄호를 안적어도 된다. 함수가 값을 반환한다면 마지막 문장은 반드시
(return
, goto
, panic()
등) terminating statement이어야 한다. 다른 프로그래밍
언어로 함수를 구현한 경우 등이라면 함수 body를 생략하고 함수 signature만 적어도 된다.
자료형 T를 receiver로 가지는 메소드는 T와 *T 자료형 변수가 호출할 수 있다
(iptr.foo
는 (*iptr).foo
의 줄임). 자료형 *T를 receiver로 가지는 메소드는 *T
자료형 변수만 호출할 수 있다 (확인 필요). t.Mv(...)
은 T.Mv(t, ...)
와 같은 뜻이다.
객체를 상속하는(?) 방법이 다른 객체지향 프로그래밍 언어와 다른데 struct와 interface 안에 다른 struct와 interface 이름을 포함하는 식이다.
type NewMutex Mutex
// 자료형이 struct라면, NewMutex는 Mutex를 receiver로 가지는 함수를 가져가지 않음
// 자료형이 interface라면, NewMutex는 Mutex를 receiver로 가지는 함수를 똑같이 가짐
type NewMutex struct {
Mutex
}
// NewMutex struct는 Mutex struct를 receiver로 가지는 함수를 똑같이 가짐
// 이 경우 newmutex.Mutex.foo 대신 보통 newmutex.foo 로 필드를 지칭
사실 메소드는 기존 객체지향 프로그래밍 언어와 다른 점이 많다. 기존 객체지향 프로그래밍 언어에서 메소드는 클래스 안에 묶여있지만, Go의 메소드는 struct와 느슨하게 연결된 느낌이다. Go는 struct를 receiver로 받는 메소드는 물론이고 정수형 등 다른 자료형에 메소드를 붙일 수 있고 (그래서 자료형마다 해당 자료형을 파싱하는 메소드를 붙일 수 있다), 심지어 아래와 같이 함수 자료형에도 메소드를 붙일 수 있다. 또, 메소드 안에서 receiver를 변경할 수도 있다 (예, json 패키지).
// 출처: http://jordanorelli.tumblr.com/post/42369331748/function-types-in-go-golang
type binFunc func(int, int) int
func add(x, y int) int {
return x + y
}
func (f binFunc) Error() string {
return "binFunc error"
}
func main() {
var err error // error interface가 되려면 Error() string 함수가 필요하다
err = binFunc(add)
fmt.Println(err)
}
map은 키를 비교하려고 ==
과 !=
를 사용하기 때문에 함수, map, slice를 키로
사용할 수 없다. len(map)
는 map에 들어있는 항목 개수이다. 붙밖이 함수
delete(m, k)
는 map이 nil
이거나 m[k]
가 존재하지 않아도 오류가 발생하지 않는다.
채널은 동시성 함수의 실행을 동기화하거나 서로 통신하기위해 사용한다. 보통 chan
으로
양방향 채널을 만들지만, 채널 사용을 제한하기위해 송신(chan<-
) 혹은 수신(<-chan
)
전용 채널로 형변환할 수 있다. left associative이기 때문에 chan<-chan T
는
chan<- (chan T)
의미이다. make(chan T, capacity)
로 만들며 두번째 아규먼트를
생략하거나 0
을 쓰면 동기 통신을 하여 송신자와 수신자가 모두 준비될 때만 통신이
성공한다. 0
이 아닌 값을 주면 그 크기만큼 버퍼를 만들어서 비동기 통신을 한다.
nil 채널로 값을 쓰거나 nil 채널에서 값을 읽으면 영원히 기다린다. nil 채널을
close()
하면 런타임 패닉이 발생한다. close()
한 채널에서 값을 읽을 때 아직 안꺼낸
값이 있다면 그 값을 반환하고, 비었다면 즉시 해당 자료형의 zero value를 반환한다. 닫은
채널에 값을 보내거나 닫은 채널을 다시 close()
하면 런타임 패닉이 발생한다.
v, ok := <-ch
형태로 채널이 닫혔고 동시에 비었는지 알 수 있다. 채널에 len()
과
cap()
함수를 사용할 수 있다. 채널로 함수를 전달하여 호출할 수도 있다.
유니코드 대문자(Lu class, Letter + uppercase)로 시작하는 identifier는 package
밖으로 익스포트된다 (C의 extern
). 서로 다른 package의 익스포트안된 identifier는
충돌하지/겹치지 않는다. 자료형을 비교할 때 다른 package에서 struct와 interface 안의
소문자로 시작하는 필드는 서로 다르다. 주의! 한글은 유니코드 대문자가 아니다.
package main
import ( "fmt"; "unicode" )
func main() {
fmt.Println(unicode.IsUpper('가')) // 한글은 "false" 출력
fmt.Println(unicode.IsUpper('π')) // 소문자 파이는 "false" 출력
fmt.Println(unicode.IsUpper('Π')) // 대문자 파이는 "false" 출력
}
nil
은 (자바의 NULL
) 포인터, 함수, slice, map, 채널, interface 자료형에 대입할
수 있다. x.f
에서 x가 nil
이라면 런타임 패닉이 발생한다. 단, nil map은 읽을 수 있다.
그러나 nil map에 값을 쓰면 런타임 패닉이 발생한다.
상수 표현은 크기 제한없이 정확히 계산한다.
const Huge = 1 << 100 // uint64 에도 담지 못할 큰 값
const Four int8 = Huge >> 98 // Four = 4
// Huge를 직접 출력하는 등 사용하지 않으면 문제가 없다
상수를 선언할 때 iota
라는 predefined identifier가 유용하다.
const (
Sunday = iota // iota는 1부터 시작하여 정수를 한개씩 순서대로 반환한다. 즉, Sunday = 1
Monday // 값을 생략하면 첫번째 내용을 그대로 가져오므로 Monday = iota 의미. Monday = 2
Tuesday // 줄 사이에 세미콜론이 자동으로 붙음
)
const ( // iota 내부값이 초기화되어 다시 1부터 시작
a = 1 << iota // a = 1 << 1
b // b = 1 << 2
c // c = 1 << 3
)
위와 같이 var
나 const
에 자료형을 명시하지 않으면, :=
를 사용한 것처럼 컴파일러가
추측한다. :=
(short variable declaration)은 함수 안에서만 사용할 수 있고, 전역에서
사용할 수 없다.
함수나 아규먼트 그리고 전역변수는 정의하고 사용하지 않을 수 있지만, 함수 내 변수와 import 그리고 goto 라벨을 정의하고 사용하지 않으면 컴파일러 구현에 따라 오류가 난다.
보통 형변환은 T(x)
형식이지만, T가 인터페이스라면 x.(T)
도 가능하다 (type
assertion). x를 T 인터페이스로 형변환하지 못하면 런타임 패닉이 발생한다. 런타임 패닉을
피하려면 v, ok := x.(T)
형식으로 두번째 반환값으로 형변환 가능 여부를 확인할 수 있다.
연산자를 보면 특이하게 &
(bitwise and) + ^
(bitwise not)인 &^
이 있다.
그래서 &^=
도 가능하다. C에 있는 >>>
연산자는 없다. 연산자에 사용하는 정수형이
부호가 있는지 없는지에 따라 >>
연산자가 부호를 유지할지 여부가 결정된다. +
연산자는
두 string을 연결할 수 있다. 배열과 struct에 ==
연산자를 사용하면 내용이 같은지 여부를
검사한다. 즉, 주소가 같은지만 비교하지 않는다.
표현식을 문장으로 사용할 수 있지만 (expression statement), len()
같은 붙밖이 함수는
함수 자료형이 아니기 때문에 그것만으로 문장이 될 수 없다. 그래서 go
와 defer
문장에
붙밖이 함수를 사용할 수 없다. Go에서 ++
과 --
는 표현식이 아니라 문장이고, 변수
뒤에만 사용할 수 있다. 대입할 때는 양쪽을 모두 미리 준비하고 대입한다.
a, b = b, a // a와 b를 동시에 바꿈. a = b; b = a 식으로 순서대로 대입하지 않음
i = 0
i, x[i], i = 1, 2 // x[0] = 2, i = 1
if
, for
, switch
바로 뒤에 나오는 조건 앞뒤에 괄호는 보통 생략하지만, 사용할
수도 있다.
switch 문은 expression switch와 type switch가 있다. expression switch는 아래와 같이 흔히 보는 형식이다.
switch tag {
default: s3() // default는 아무 곳에 두어도 된다
case 0, 1, 2, 3: s1() // 여러 값을 모을 수 있다
case 4, 5, 6, 7: s2()
}
switch x := f(); { // switch 조건 앞에 문장을 적을 수 있다 (그리고 문장 뒤에 세미콜론)
// 또, 이 경우와 같이 조건을 생략하면 true
case x < 0: return -x // 상수가 아닌 조건식을 적어도 된다
default: return x
}
type switch는 자료형을 찾는다. 조건에 v.(type)
을 쓰고, case
뒤에는 자료형 이름과
nil
을 사용한다. if i, isInt := v.(int); isInt { ... }
같은 type assertion을
연결한듯이 실행한다. type switch에서는 fallthrough
를 사용할 수 없다.
for 문은 (C의 while
같은) 조건, C의 for
같은 세가지 표현식, range
, 이렇게
세가지 형식이 가능하다. 조건을 생략하면 true이다. range
에는 배열, slice, string,
map, 채널을 사용할 수 있다. 배열과 slice, string은 색인과 값을 반환하고, map은 키와
값을 반환한다. 유일하게 채널은 (채널이 닫힐 때까지) 값만 반환한다.
goroutine을 만드는 go
뒤에는 함수나 메소드가 나온다.
select
는 통신용 switch이다. 채널로 값을 쓰거나 채널에서 값을 읽는 내용을 case에
적는다. 당장 통신이 가능한 case를 실행하며 당장 통신이 가능한 case가 여러개라면 임의로
한개를 선택한다. 당장 통신이 가능한 case가 없다면 default를 실행하고, default마저
없다면 case 중 하나가 통신이 가능해질 때까지 계속 기다린다.
Go에는 try/catch/finally를 사용한 구조적 예외처리문이 없다. 대신 defer 함수
혹은
defer 메소드
를 사용하여 코드를 함수를 반환하기 직전에 실행할 수 있다. 즉, finally
역할을 자원해제 등에 활용할 수 있다. 붙밖이 함수 panic()
은 마치 예외를 생성하는 역할을
하는 런타임 패닉을 발생한다 (정확히는 panic(runtime.Error)
). 런타임 패닉이 발생하면
함수 호출을 거슬러 따라가면서 함수별 defer 코드를 실행한 이후 프로그램을 종료한다.
여러번 defer하면 뒤부터 역순으로 실행한다. 또, defer로 실행하는 코드가 함수의 반환값을
변경할 수도 있다. try/catch 역할이 필요하면 붙밖이 함수 recover()
를 사용한다.
defer로 실행하는 코드에서 (새로 런타임 패닉을 발생하지 않고) recover()
를 호출하면
런타임 패닉으로 인한 프로그램 종료를 막는다.
붙밖이 함수 몇가지를 살펴보자. slice, map, 채널을 만들 때 make()
를 사용한다. *T
자료형을 반환하는 new()
와 달리 make()
는 T 자료형을 반환한다. 필요하다면
(즉, cap(s)
가 새로 값을 추가하기 충분하지 않다면) 새로 메모리를 더 크게 할당하여
slice s 뒤에 값을 추가하는 append(s S, x ...T)
에서 T는 slice S의 기본 자료형이다
(즉, type S []T
). 그러나 특이하게 append([]byte, string...)
형식이 가능하다.
마찬가지로 copy()
는 보통 copy(dst, src []T)
이지만,
copy(dst []byte, src string)
형식이 가능하다. copy()
는 복사한 항목 개수를
(len(src)
와 len(dst)
중 작은 값) 반환한다. 첫번째 아규먼트가 대상이고, 두번째
아규먼트가 원본인 점을 주의하라. complex()
는 복소수 자료형 값을 만들고, real()
과
imag()
는 각각 실수부와 허수부를 추출한다.
소스코드 제일 앞에 package 구문과 import 구문이 순서대로 나온 이후 나머지 내용이 나온다. 컴파일러 구현에 따라 같은 패키지에 속하는 소스코드가 모두 같은 디렉토리에 위치해야 한다고 강제할 수 있다. import 구문의 경로를 해석하는 방식은 컴파일러 구현자가 결정할 수 있다.
// import 구문별 lib/math 패키지에 있는 Sin() 함수를 지칭하는 방식
import "lib/math" // math.Sin
import m "lib/math" // m.Sin
import . "lib/math" // Sin
import _ "lib/math" // 직접 참조하지 않지만, init() side effect 등 때문에 import할 때 사용
// (그래서 컴파일 오류 피함)
func init()
는 패키지를 초기화하는 코드이다. 이 코드는 패키지를 import할 때 자동으로
호출되고, 프로그래머가 직접 호출하지 못한다. 패키지 안에 심지어 같은 파일 안에서도 여러번
정의할 수 있고, 이 경우 어떤 코드가 먼저 실행될지 보장하지 못하지만 한 init()
가 완전히
끝난 이후 다른 init()
가 실행된다.
main()
함수가 끝나면, 다른 goroutine 종료를 기다리지 않고 프로그램을 종료한다.
다행히도 1.1과 1.2 버전의 문법상 변화는 미미하다.
이제 다른 Go 문서와 규일이 소개한 Network programming with Go를 읽겠다.