Skip to content

Instantly share code, notes, and snippets.

@maczniak
Created January 26, 2014 14:02
Show Gist options
  • Save maczniak/8633140 to your computer and use it in GitHub Desktop.
Save maczniak/8633140 to your computer and use it in GitHub Desktop.

How to Write Go CodeEffective Go를 읽고 내용을 요약한다. Effective Go는 Language Specification, A Tour of Go, How to Write Go Code 문서를 이미 읽었다고 가정한다.

How to Write Go Code

Go 명령어는 작업공간을 나타내는 GOPATH 환경변수를 기준으로 파일을 찾는다. 직접 디렉토리 안에서 Go 명령어를 실행한다면 경로가 필요없다. 작업공간 안에는 bin/, pkg/운영체제_아키텍처/패키지/, src/패키지/ 디렉토리가 있다. 크로스 컴파일을 돕기위해 pkg/.../foo.a 식으로 저장된 코드를 정적링크한다. Go 명령어을 실행해서 아무 출력이 없다면 성공적으로 실행했다는 뜻이다. 패키지나 바이너리를 설치할 때 의존하는 패키지도 함께 설치한다.

실행가능한 명령어는 반드시 main 패키지에 들어가야 한다. 패키지 이름은 경로의 마지막 요소이다 (예, import "crypto/rot13"의 경우 패키지 이름은 rot13).

테스트는 go test 명령어로 한다. 파일명 뒤가 _test.go이고, 테스트 함수는 func TestXXX(t *testing.T) 형식이다. 테스트 함수에서 t.Error()t.Fail()을 호출하면 테스트가 실패한다.

go get 명령어는 자동으로 소스코드를 가져와서 컴파일하고 설치한다.

A list of Go projects.GoDoc에서 다른 Go 프로젝트를 찾을 수 있다.

Effective Go

gofmt는 주석까지 위치를 맞추어 소스코드를 정리한다.

Go는 if, for, switch 뒤에 괄호가 필수가 아니기 때문에 괄호 사용이 줄어든다. 그리고 연산자 우선순위가 달라서 x<<8 + y<<16을 괄호없이 쓸 수 있다.

주석은 패키지 제일 앞에 나오는 패키지 주석과 익스포트하는 전역 이름 앞에 나오는 doc comment가 있다. 패키지를 구성하는 소스코드가 많다면 패키지 주석은 한 파일에만 적는다. doc comment의 첫 문장은 (grep으로 검색할 수 있도록) 선언하는 이름으로 시작한다.

패키지 이름은 일반적으로 밑줄이나 대소문자를 혼합하지 않는다. 익스포트할 이름은 패키지 이름이 앞에 붙을 것을 고려하여 지으면 좋고, 여러 단어가 들어갈 때 밑줄보다는 대소문자 혼합이 좋다. 메소드가 한개인 인터페이스는 메소드 이름 뒤에 -er을 붙여 이름을 짓는다. Read, Write, Close, Flush, String 등 잘 알려진 이름은 의미에 부합할 때만 사용하라. getter는 Owner() 식으로, setter는 SetOwner()와 같이 이름을 짓는다.

if ... // 오류! if 조건 뒤에 세미콜론이 자동으로 들어가기 때문에 불가
{
	...
}

if ... { // ( 과 { 뒤에는 자동으로 세미콜론을 붙이지 않는다 (자세한 내용은 문서 참고)
	...
}


f, err := os.Open(name)
if err != nil { // 보통 이런 식으로 오류를 처리한다
	return err
}
d, err := f.Stat() // 앞에서 선언한 err 변수의 값만 변경
// 새로 선언하는 변수가 (여기서는 d) 한개라도 있기 때문에 := 가능
if err != nil {
	f.Close()
	return err
}
codeUsing(f, d)
for pos, char := range "日本\x80語" { // \x80 is an illegal UTF-8 encoding
	fmt.Printf("character %#U starts at byte position %d\n", char, pos)
}
// pos는 UTF-8 표현시 바이트 위치
// UTF-8로 표현안되는 문자는 U+FFFD (replacement rune) 로 변환된다

for i := 0; i < 10; i++ { // 가능

for i, j := 0, len(a)-1; i < j; i++, j-- { // 불가! ++와 --는 연산자가 아니라 문장

for i, j := 0, len(a)-1; i < j; i, j = i+1, j-1 { // 대신 이렇게 가능

if else 가 반복하는 코드를 switch { case ... } 형식으로 옮길 수 있다. (switch 조건식을 생략하면 true이므로 case 문을 순서대로 평가하여 제일 먼저 true가 되는 구문을 실행한다.) C에서 case a: case b: 코드는 Go에서 case a, b:가 된다.

함수 반환값에 이름을 붙이면, 함수가 시작할 때 그 이름을 가진 초기화된 변수가 생긴다.

defer 함수는 defer를 실행할때 eval된다.

func trace(s string) string {
	fmt.Println("entering:", s)
	return s
}

func un(s string) {
	fmt.Println("leaving:", s)
}

func a() {
	defer un(trace("a"))
	// a()가 시작하자마자 defer 함수를 평가하려고 아규먼트인 trace()를 먼저 실행하지만,
	// un()은 함수가 끝날 때 실행하도록 예약한다.
	fmt.Println("in a")
}

붙밖이 함수 new()make()를 사용하여 메모리를 할당한다. new()는 zero value 상태인 포인터(*T)를 반환한다. 값을 초기화해야 한다면 아래와 같이 composite literal을 사용한다. 반면 make()는 사용하기 전에 자료구조를 초기화해야 하는 slice, map, 채널을 만들 때만 사용한다. 적절하게 초기화한 후 포인터가 아닌 T를 반환한다.

func NewFile(fd int, name string) *File {
	if fd < 0 {
		return nil
	}
	f := File{fd, name, nil, 0}
	return &f
	//  아니면 아래와 같이 지역변수의 주소를 반환해도 함수 밖에서 사용할 수 있다.
	//  composite literal의 주소를 요청하면 새로 인스턴스를 할당한다.
	// return &File{fd, name, nil, 0}
	//  이렇게 일부 필드만 초기화하면 나머지 필드는 zero value
	// return &File{fd: fd, name: name}
}

Go 배열은 C 배열과 달리 대입하거나 함수에 넘길 때 값 전체를 복사하고, 배열 크기도 자료형의 일부이기 때문에 배열 크기가 서로 다르면 다른 자료형이다. 배열의 포인터를 함수에 넘겨줄 수 있지만, Go는 보통 배열 대신 slice를 사용한다. slice는 안에 숨겨진 배열의 참조만 저장한다. 배열의 일부분만을 가리킬 수 있으며 이 성질을 사용하여 C에서 포인터 연산할 부분에 slice를 사용한다. map도 slice 처럼 참조만 저장하기 때문에 값 전체를 복사하지 않는다.

fmt 패키지의 출력함수는 C stdio를 본딴 Print/Fprint/Sprint()Print/Println/Printf()이 조합을 이룬다.

  • Print/Println()"%v" 형식으로 출력하고, Print()는 출력할 항목들 사이에 공백을 넣는다.
  • map을 출력할 때 "%v"는 필드 값들만 연속해서, "%+v"는 필드 이름도 함께, "%#v"는 완벽한 Go 문법으로
  • 문자열을 출력할 때 "%q"는 qouted string으로, "%#q"는 가능하면 backquote로
  • 16진수로 출력하는 "%x"는 정수 외에 string, byte 배열과 byte slice도 가능, "% x"는 바이트 사이에 공백 추가
  • "%T"는 자료형을 출력
type MyString string

// 자료형을 출력하기위한 함수 정의. Java의 String toString() 역할
// (m MyString) 대신 (m *MyString) 같은 포인터 형식이 더 효율적
func (m MyString) String() string {
	// fmt.*Print*()가 fully reentrant하기 때문에 String() 함수 안에서 호출해도 되지만,
	//  영원히 반복해서 호출되지 않도록 조심한다.
	// 그래서 아래처럼 string으로 형변환하거나 %s 대신 %f 등을 사용하여 문제를 방지한다.
	return fmt.Sprintf("MyString=%s", string(m))
}

func Println(v ...interface{}) {
	// 가변길이 아규먼트를 v... 식으로 fmt.*Print*()에 받은 그대로 넘길 수 있다.
	// 그냥 v만 적으면 slice 한개를 아규먼트로 함수를 호출한다.
	std.Output(2, fmt.Sprintln(v...))  // Output takes parameters (int, string)
}

인터페이스에 필요한 메소드를 자료형마다 모두 구현하기 보다 이미 구현한 자료형으로 형변환하여 사용하는 편이 바람직하다.

init()는 import한 패키지를 모두 초기화(init())하고 모든 변수 선언을 마친 이후 호출된다. 보통 빠진 설정값이 있는지 확인하거나 기본값을 설정하는 등 프로그램을 제대로 시작하기 전에 프로그램 상태를 점검하고 바로잡으려고 사용한다.

메소드 수신자는 T와 *T 모두 가능하지만, *T인 경우 원래 값을 변경할 수 있다. 메소드 수신자가 *T이면 T는 안되고 *T 자료형 변수만 호출가능하다고 나오는데 실제 예제 코드를 만들어봐도 잘 이해가 안된다. T와 *T 모두 호출가능해 보인다.

프로그램을 부풀리고 컴파일이 느려지기 때문에 사용하지 않은 임포트를 막는다. 계산을 낭비하고 더 큰 버그의 징조일 수 있기 때문에 사용하지 않는 변수를 막는다. 그러나 활발하게 개발할 때는 이 제한이 번거로울 수 있다. 패키지는 var _ = fmt.Printf 식으로, 변수는 _ = fd 식으로 컴파일이 되게 한다.

인터페이스는 다른 인터페이스를 내장하여(embedding) 메소드를 상속(?)할 수 있지만, struct는 아래와 같이 포인터를 포함하고 초기화할 때 필드를 채워야 한다. 아래와 같이 필드 이름을 생략하면 필드 자료형이 필드 이름 역할을 한다. 나중에 필드를 추가해도 기존 코드에 문제가 생기지 않도록 필드 이름이 중복될 경우 두가지 규칙을 적용한다. 먼저 포함한 깊이가 다르다면, 최상위에 가까운 필드가 우선한다. 깊이가 같으면 오류이지만, 외부에서 필드를 직접 언급하지 않으면 오류가 아니다.

type ReadWriter struct {
	*Reader  // *bufio.Reader
	*Writer  // *bufio.Writer
}

Go는 스레드들이 직접 값을 공유하지 않고 채널로 값을 주고받아서 동시성(concurrency) 문제를 피한다. goroutine은 기존의 coroutine 개념과 다르다. 자세한 내용은 문서를 참고하라. goroutine은 closure이기 때문에 실제 실행할 때 변수를 참조할 수 있어야 한다. Go는 아직 기본적으로 병렬 실행하지 않는다. GOMAXPROCS 환경변수나 runtime.GOMAXPROCS(runtime.NumCPU())이 필요하다. 앞으로 스케쥴링과 런타임이 향상되면 이 작업이 필요없어 질 것이다.

채널로 barrier나 mutex/semaphore를 구현할 수 있다. 아니면 for 문으로 같은 채널을 기다리는 정해진 개수의 goroutine을 만들어서 semaphore를 피할 수도 있다. 채널을 통해 채널을 받을 수 있다. 문서에서 요청을 처리한 후 응답을 보낼 채널을 요청과 함께 받는 예제를 참고하라.

panic()을 사용하면, 현재 goroutine의 역순으로 차례차례 defer 함수를 호출한 후 프로그램을 종료한다. recover()는 defer 함수가 직접 호출해야 한다. defer 함수에서 호출한 다른 함수가 recover()를 호출하면 nil을 반환하며 프로그램 종료를 막지 않는다.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment