쉽게 읽는 Effective Go 번역 (1)

Go 를 사용하는 사람이라면 반드시 읽어봐야할 글이 있죠.
"Effective Go"

Gitbooks 에 이미 번역된 글이 있지만 개인적으로도 몸소 번역해보고 싶은 욕심이 있어서 시작해 봅니다.
번역작업은 의외로 많은 고민과 생각이 필요합니다.
저자의 의도를 정확히 파악하려는 노력이 많이 필요하죠.
그래서 번역 작업도 마치 글쓰기와 유사합니다.
불필요한 부분 덜어내고, 부족한 부분을 나름대로 채워 넣고, 다듬고 또 다듬고...
그러면서, 글에 녹아있는 저자의 직관도 자연스럽게 내 것이 됩니다.

이 글을 모두 번역한 후 위의 Gitbooks 번역본과 비교해보고 기여를 해볼 수도 있겠네요.

저는 원문을 있는그대로 직역하는 것을 고집하지는 않았어요.
제가 이해한 바를 되도록 자연스러운 한국어로 표현할 수 있도록 노력했습니다.
(대신 원문의 기술적 내용을 완벽하게 이해한 후 번역하려고 노력했습니다.)
뭐든 딱딱하면 재미없으니 어체도 조금 변경했구요.

영어가 자연스러우면 그냥 영어를 사용했어요.



소개 (Introduction)

Go 는 새로운 언어입니다.
비록 기존 언어들로부터 아이디어들을 빌려오긴 했지만, 그래도 다른 친척 언어와 비교했을 때 효율적인 Go 프로그램이 가지는 특별함들이 있습니다.
무턱대고 (언어 직역하듯이) C++ 이나 Java 프로그램을 Go 로 바꾼다고 만족스러운 결과가 나오는 것은 아닙니다.
Java 프로그램은 Java 로 만든거지 Go 로 만든게 아니잖아요.

그래도, Go 의 관점에서 문제들을 생각해보는건 도움이 많이 될거예요.
물론 아주 다른 프로그램이 되겠지만요.

Go 로 프로그램을 짜려면... 뭐랄까요.
그 특성과 관용적인 부분들을 정확히 이해하는 것이 중요합니다.
Go 프로그래밍에 있어서 이미 정립된 관습(Convention)들을 잘 아는 것도 역시 중요하구요.
예를 들면, 네이밍이나 포맷팅, 프로그램의 구조 등등.
이런 것을 잘 지켜주어야만 다른 Go 프로그래머들이 여러분의 프로그램을 이해하기 쉬울거예요.

여러분이 이 문서를 보고 나면 아주 깔끔하고(clear) 관용적인(idiomatic) Go 코드를 작성하는 팁들을 알게 되실거예요.
이 글은 아래 3 개를 보완하는 글이기 때문에 아래 글들을 먼저 읽으시는게 좋습니다.

Examples

Go 패키지 소스는 단지 표준 라이브러리를 제공하기 위한 목적만 있는 것은 아니예요.
여러분에게 이 언어를 어떻게 사용하는지 알려줄 좋은 예(example)를 제공하는게 목적이기도 합니다.
많은 패키지들은 잘 동작하는 Example 들을 자체적으로 아예 포함하고 있어요.
여러분은 그 Example 들을 golang.org 웹 사이트에서 바로 실행시켜볼 수 있죠.
처럼요. (안보이면 Example 글자를 클릭해서 펼쳐보세요)

문제에 어떻게 접근할지 또는 어떻게 구현할지 등 여러분의 고민들에 대해, Go 의 문서와 코드, 그리고 Example 들이 해답과 아이디어를 줄 수 있을거예요.

포맷팅(Formatting)

포맷팅은 가장 논쟁이 많지만 결론은 제일 안나는 이슈입니다.
사람들은 누구나 다른 스타일의 포맷팅에도 적응할 수 있어요. (사람은 적응의 동물)
물론 적응할 필요가 없다면 더 좋겠지만요.
모든 사람이 똑같은 스타일을 추구한다면 이런 주제에 대해 논쟁으로 인한 시간 낭비는 좀 덜하겠죠.
문제는 유토피아에 다가가기 위해 길고 긴 지루한 스타일 가이드(style guide)를 따라야한다는 것인데요.

Go 에서는 좀 특별한 접근법을 취하고 있습니다.
컴퓨터가 대부분의 포맷팅 이슈를 처리하도록 하는건데요.

gofmt(패키지 레벨에서는 go fmt 로도 사용 가능) 프로그램은 Go 프로그램을 읽어서 들여쓰기나, 수직 줄맞춤, 주석의 포맷팅 등을 표준 스타일로 변경해줍니다.
여러분이 뭔가 새로운 layout 을 다루어야하는 상황을 만났을 때는 gofmt 를 실행해보세요.
(만약 gofmt 결과가 틀린 것 같으면 gofmt 버그로 올려주시고, 그냥 여러분의 뜻대로 프로그램을 작성하세요. 뭔가 다르게 처리해야하나 고민하지 마시구요.)

예를 하나 보죠.
여러분은 구조체의 각 필드에 대한 주석을 작성할 때 줄을 맞추려고 시간을 쓰실 필요가 없어요.
Gofmt 가 알아서 해줄거거든요.

type T struct {
    name string // name of the object
    value int // its value
}

아래를 보시면 gofmt 이 각 컬럼의 줄을 맞춰주는걸 알 수 있어요.

type T struct {
    name    string // name of the object
    value   int    // its value
}

standard library 에 있는 모든 Go 코드들은 gofmt 으로 포맷팅 과정을 거친거예요.

포맷팅에 대해 얘기해볼 수 있는 약간 더 깊은 내용들이 몇가지 있는데요.
아주 간략하게 적어보면:

들여쓰기(Indentation)
들여쓰기를 할 때는 탭(tab)을 사용해요. gofmt 도 기본적으로 탭을 사용하구요.
반드시 Space 를 사용해야하는 경우에는 그렇게 하세요.

줄 길이(Line length)
Go 에서는 한 줄의 길이에 대한 제한은 없어요.
overflow 를 걱정할 필요는 없구요. 줄이 너무 길다고 느껴지면 감싸면 되고 추가 탭으로 들여쓰기를 하면 됩니다.

괄호(Parentheses)
Go 는 C 나 Java 보다 괄호가 덜 필요해요.
제어문(if, for, switch)은 괄호도 필요없어요.
연산자들의 우선순위 계층도 단순하고 명확해요.

x<<8 + y<<16

다른 언어와는 다르게 Space 가 의미하는 바가 큽니다.

주석(Commentary)

Go 는 C 스타일의 블럭 주석(/* */)과 C++ 스타일의 라인 주석(//)을 제공합니다.
라인 주석은 일반적으로 사용되는 방식이고, 블럭 주석은 Go 의 패키지 주석으로서 많이 사용되는 방식인데요.
블럭 주석은 go 의 표현식(expression) 안에서 주석을 넣거나, 코드의 일정 구역을 통째로 막아버리고 싶을 때도 유용합니다.

godoc 프로그램(web server 포함)은 패키지 코드가 포함하고 있는 내용을 자동으로 문서화하여 노출시키는 작업을 수행해 줍니다.
top-level 의 선언부 이전에 주석이 나오면, godoc 은 이를 선언부와 함께 추출해서 그 item 에 대한 설명 문서로 제공해 줍니다.
그러니까, 여러분이 추가하는 주석의 스타일이나 특성이 godoc 이 만들어내는 문서의 질을 좌우하는거죠.
(godoc 으로 만들어질 문서를 생각하고 주석 잘 달라는 얘기)

모든 패키지는 패키지 구문(package ...) 이전에 "패키지 주석"을 가지는게 좋습니다.
만약 여러 개의 파일로 구성된 패키지일 경우는 하나의 파일에만 주석을 기록하는게 좋아요. 어떤 파일이든 좋아요.
패키지 주석 에서는 패키지에 대한 소개와 전반적으로 패키지 관련된 정보를 제공해야 합니다.
패키지 주석은 godoc 페이지의 첫 부분에 나오게 되니, 이후 나오게 될 상세 문서에 대한 셋업의 역할도 하는거죠.

/*
Package regexp implements a simple library for regular expressions.

The syntax of the regular expressions accepted is:

    regexp:
        concatenation { '|' concatenation }
    concatenation:
        { closure }
    closure:
        term [ '*' | '+' | '?' ]
    term:
        '^'
        '$'
        '.'
        character
        '[' [ '^' ] character-ranges ']'
        '(' regexp ')'
*/
package regexp

패키지가 간단한 것이면 패키지 주석도 간단하게 작성하면 되요.

// Package path implements utility routines for
// manipulating slash-separated filename paths.

별표를 이용해서 광고판처럼 만드는 부가적인 포맷팅 같은건 필요없어요.
그 output 이 고정폭의 폰트로 출력된다는 보장도 없으므로, 괜히 align 을 맞춘다고 space 를 추가하거나 그런 것들에 의존하지 마세요.
gofmt 처럼 godoc 이 다 처리해준다구요.
주석은 (컴파일러나 godoc 등에 의한) 번역되지 않는 plain text 입니다.
그러니까 HTML 이나 _this_ 같은 주석은 원래 작성한 그대로 나와요.
그렇게 작성하지 마세요.
godoc 이 한가지 조정해주는 것이 있는데, 그건 들여쓰기로 작성된 텍스트를 고정폭의 폰트로 보여주는 거예요.
프로그램 코드 조각을 보여줄 때 적합하죠.
fmt 의 패키지 주석에서 이 방법을 잘 써먹고 있어요.
(역자: srcdoc 파일을 비교해보면 이해될 것임)

godoc 이 문맥에 따라서 주석을 전혀 리포맷팅을 안할 수도 있어요.
그러니, 문서가 좋아보이도록 만드는 것은 여러분의 몫입니다.
올바른 철자를 사용하고, 구두점을 잘 찍고, 문장구조를 잘 잡고, 긴 라인은 적당히 잘라주고 하는 등의 것들은 여러분이 신경쓰셔야 합니다.

패키지 내에서 선언부 바로 위의 주석들은 그 선언부에 대한 문서 주석("doc comment")의 역할을 합니다.
그 선언부에 대한 설명을 작성하는 부분이죠.
첫문자가 대문자인, 즉 외부로 공개되는 이름들에 대해서는 모두 "doc comment"를 작성해야 합니다.

문서 주석(doc comment)은 완전한 문장으로 작성하는 것이 가장 좋습니다.
그래야만 자동화되어 다양한 형식으로 보여줄 때도 모두 괜찮게 보이거든요.
첫번째 문장은 한줄 요약(Summary)의 역할을 하고, 그 맨 앞 단어는 선언부의 이름으로 시작하도록 합니다.

// Compile parses a regular expression and returns, if successful,
// a Regexp that can be used to match against text.
func Compile(str string) (*Regexp, error) {

설명하려는 선언부의 이름으로 모든 문서 주석을 시작하면 좋은 점이 있어요.
여러분이 go doc 명령과 grep 을 함께 사용할 수 있는데요.
만약 여러분이 "Compile" 이라는 이름은 생각이 안나는데, 뭔가 parsing 하는 함수를 regexp 패키지에서 찾고 싶다고 생각해보죠.
그래서, 아래처럼 명령어를 입력했죠.

go doc -all regexp | grep -i parse

만약에 패키지의 문서 주석을 모두 "This function..."으로 시작하게 작성했다면, 아마 grep 은 여러분이 "Compile" 이라는 이름을 떠올리도록 도와주지 못했을 거예요.
문서 주석을 전부 그 item 의 이름으로 작성했으니까, 여러분은 아래처럼 그 이름을 떠올릴 수 있는 결과를 얻을 수 있는거죠.
(역자: grep 결과의 첫 단어가 Compile 로 떡하니 나와 있잖아요. This function 이 아니고...)

$ go doc -all regexp | grep -i parse
    Compile parses a regular expression and returns, if successful, a Regexp
    MustCompile is like Compile but panics if the expression cannot be parsed.
    parsed. It simplifies safe initialization of global variables holding
$

Go 에서는 선언할 때 group 으로 묶어서 할 수도 있어요.
하나의 문서 주석(doc comment)이 관련된 상수나 변수의 group 을 소개할 수 있어요.
이런 경우는 모든 선언은 다 나오는데 주석은 부실해지기 쉽죠.

// Error codes returned by failures to parse an expression.
var (
    ErrInternal      = errors.New("regexp: internal error")
    ErrUnmatchedLpar = errors.New("regexp: unmatched '('")
    ErrUnmatchedRpar = errors.New("regexp: unmatched ')'")
    ...
)

group 으로 묶는 것은 각 item 들 간의 관계를 나타내기도 해요.
아래 코드를 보면 변수들이 하나의 mutex 에 의해 보호된다는 것을 짐작할 수 있죠.

var (
    countLock   sync.Mutex
    inputCount  uint32
    outputCount uint32
    errorCount  uint32
)

이름(Names)

다른 언어와 마찬가지로 go 에서도 이름이란건 중요해요.
이름 자체가 의미(semantic effect)에 영향을 주기도 하죠.
첫번째 문자가 대문자냐 아니냐에 따라 패키지 외부에서 그 item 을 볼 수 있느냐 없느냐를 결정합니다.
그래서, Go 프로그램에서는 이름짓는 규칙(convention)을 논의하기 위한 시간을 좀 덜 쓸 수 있어요.

패키지 이름(Package names)

패키지가 import 될 때, 패키지 이름 자체가 해당 패키지의 내용(content)에 대한 접근자로서 사용됩니다.
예를 들어, 아래와 같이 bytes 패키지를 import 한 뒤에는

import "bytes"

이를 import 한 패키지는 bytes.Buffer 와 같이 코드를 작성할 수 있는거죠.
이 패키지를 사용하는 모든 사람이 그 Content 를 가리킬 때 같은 이름을 사용할 수 있다면 그건 좋은 일이죠.
그건 곧 패키지 이름 자체가 유용하게 사용될 수 있다는 의미잖아요?
짧고 간결하고 누구나 쉽게 알 수 있으니까요.
패키지 이름은 관습적으로 소문자를 사용합니다. 되도록 하나의 단어만을 사용하구요.
언더스코어를 사용하거나 대소문자를 섞어쓸 필요가 없어야 합니다.
모든 사람이 이름을 이용해서 여러분의 패키지를 사용하게 되니까 간결해야 하잖아요.
그리고, 이름 충돌에 대한 걱정은 하지 마세요.
패키지 이름은 단지 import 를 위한 default 이름일 뿐이구요.
모든 소스 코드를 통틀어 유일할 필요는 없거든요.
드문 경우이긴 하지만 이름 충돌이 발생할 경우에는 패키지를 import 할 때 로컬용으로 다른 이름을 선택하여 사용할 수가 있어요.
어떤 경우에든 별로 혼동스럽지 않아요.
왜냐면 import 구문에 주는 파일 이름이 사용될 패키지를 결정하게 되거든요.
(역자: import 구문에서 파일의 full path 를 주기 때문에 충돌이 발생할 수가 없음)

또 다른 관습이 하나 있는데요.
패키지 이름이 곧 소스 디렉토리의 base 이름이라는거예요.
"src/encoding/base64" 디렉토리에 있는 패키지는 "encoding/base64" 로 import 됩니다.
하지만, 패키지 이름은 base64 이죠.
encoding_base64 라든가 encodingBase64 가 아니구요.

패키지를 import 하는 입장에서는 패키지의 content 를 참조하기 위해 그 패키지 이름을 사용할텐데요.
그래서, 패키지에서 노출(export)되는 이름의 경우 깔끔하게 표현하기 위해 이러한 사실(내용 참조를 위해 패키지 이름을 사용하는 것)을 이용할 수 있습니다.
예를 들면, bufio 패키지의 buffered reader 타입은 "BufReader" 가 아니라 "Reader" 라고 불리지요.
왜냐하면, 사용자는 이를 bufio.Reader 로 보게 될텐데 이게 훨씬 깔끔하고 명확하잖아요.
더우기, import 된 객체들이 항상 그들의 패키지 이름을 가리키고 있으니까, bufio.Reader 가 io.Reader 와 충돌할 일도 없겠죠.
ring.Ring 의 새로운 인스턴스를 만드는 함수 이름에 대한 경우도 마찬가지인데요.
NewRing 이라고 이름 지을 수도 있겠지만, 이게 패키지에서 export 된 유일한 타입이기 때문에 그냥 New 로 할 수 있는거죠.
패키지를 이용하는 쪽에서는 그냥 ring.New 를 사용하면 되잖아요.
지금까지 본 것처럼 좋은 이름을 선택하고 싶다면 패키지의 구조를 이용하세요.

또 다른 예로는 once.Do 가 있어요.
once.Do(setup) 을 once.DoOrWaitUntilDone(setup) 으로 사용한다고 별로 나아지는건 없어요.
이름이 길다고 자연적으로 가독성이 좋아지는건 아니죠.
차라리 문서를 작성하는게 이름 길게 짓는 것보다 나은 경우가 많아요.

Getters

Go 는 자체적으로 getter 와 setter 를 제공하지는 않습니다.
여러분이 직접 getter 와 setter 를 제공해도 상관은 없어요.
때로는 그게 좋은 방법일 때도 있구요.
하지만, getter 이름에 Get 을 넣는 것은 관용적으로 사용되는 방법도 아니고 별로 필요하지도 않아요.
만약 owner 라는 필드가 있으면(소문자, 패키지 밖으로 노출안됨), getter 메소드는 GetOwner 가 아니라 Owner(대문자, 패키지 밖으로 노출됨)로 사용하는게 좋아요.
필요하면 setter 메소드는 SetOwner 라고 부를 수 있겠죠.
두 이름 모두 실용적으로 잘 읽히는 이름이네요.

owner := obj.Owner()
if owner != user {
    obj.SetOwner(user)
}

인터페이스 이름(Interface names)

관습적으로 하나의 메소드를 가진 인터페이스의 이름은 메소드 이름에 "-er" 접미사를 붙이거나 그와 유사한 방식으로 이름짓게 됩니다.
Reader, Writer, Formatter, CloseNotifier 등과 같이 말이죠.

그런 형태의 이름들이 엄청 많은데, 그 인터페이스들과 그 인터페이스들이 포함하고 있는 함수 이름들에 대해 존중해주는게 좋습니다.
Read, Write, Close, Flush, String 등은 각자 고유한 사용법과 의미를 가지고 있는데요.
혼동을 피하려면 여러분은 되도록 메소드 이름을 지으실 때 이런 이름들은 피하시는게 좋겠죠.
사용법이 똑같고(signature) 의미도 같은 경우라면 같은 이름을 사용해도 됩니다만.
즉, 여러분의 타입이 어떤 유명한 타입의 메소드와 똑같은 의미를 가진 메소드를 구현하려면 여러분은 '같은 이름'과 '같은 signature'를 사용해야 합니다.
여러분의 string 변환기는 ToString 이 아니라 String 메소드라고 이름지어야 하는거예요.

대소문자 혼합(Mixed Caps)

마지막으로 Go 에서는 여러 단어로 이름지을 때 언더스코어보다는 MixedCaps 또는 mixedCaps 형식을 사용합니다.

세미콜론(Semicolons)

Go 도 C 처럼 한 구문(statement)을 종료할 때 세미콜론을 사용합니다.
하지만, C 와는 달리 소스 코드에 나타나지는 않아요.
컴파일할 때 Lexer 가 코드를 스캔하면서 자동으로 세미콜론을 넣어주는 간단한 규칙을 사용하거든요.
그래서, 소스 코드를 작성할 때 이런 부분이 자유롭죠.

Lexer 가 사용하는 간단한 규칙은 이런거예요.

만약 newline('\n') 앞의 마지막 Token 이 (int, float64 같은 단어를 포함한) 식별자이거나, 숫자나 문자 같은 기본 리터럴, 또는 아래 토큰들 중 하나일 경우, Lexer 는 항상 토큰 뒤에 세미콜론을 추가합니다.

break continue fallthrough return ++ -- ) }

간략히 말하면, "구문(statement)을 끝낼 수 있는 토큰 뒤에 newline('\n')이 오면 세미콜론을 삽입한다"라고 할 수 있겠네요.

세미콜론은 닫는 중괄호(}) 앞에서도 생략될 수 있습니다.
그래서, 아래와 같은 구문의 경우 세미콜론이 필요가 없어요.

go func() { for { dst <- <-src } }()

보통 Go 프로그램에서는 세미콜론을 for loop 구문에서 initializer, condition, 그리고 continue 요소를 구분할때에만 사용합니다.
한 라인에서 여러 구문(statement)를 구분할 때도 세미콜론이 필요한데, 코드도 그런 식으로 작성해야 합니다.

세미콜론 삽입 규칙 중 또 하나는, 제어 구문(if, for, switch, select)의 여는 중괄호({)를 다음 라인에 작성하면 안된다는거예요.
그렇게 하면 세미콜론이 자동으로 그 중괄호 앞에 들어가서 원하지 않는 결과가 나올 수도 있어요.
아래처럼 사용하세요.

if i < f() {
    g()
}
if i < f()  // wrong!
{           // wrong!
    g()
}

You may also like...

0 0 votes
Article Rating
Subscribe
Notify of
guest
1 Comment
Oldest
Newest Most Voted
Inline Feedbacks
View all comments

[…] [첫번째], [두번째], [세번째] 에 이어 네번째 Effective Go 번역 이어갑니다. […]

1
0
Would love your thoughts, please comment.x
()
x