쉽게 읽는 Effective Go 번역 (2)

첫번째에 이어 두번째 번역 이어갑니다.



제어 구문(Control structures)

Go 에서의 제어구문은 C 와 유사하지만 중요한 차이점이 있습니다.
while 이나 do while 반복문은 없구요. for 로 모두 대체할 수 있어요.
switch 구문은 좀 더 유연한데, if 와 switch 구문 모두 for 처럼 초기화 구문을 함께 사용할 수 있어요.
switch 에서 break 와 continue 구문은 옵션으로 사용할 수 있는데요.
어디에서 멈추고 어디에서 계속할지를 좀더 명확하게 할 수 있겠죠.

타입 switch 라든가, 여러 방향의 통신을 위한 multiplxer 인 select 등 새로운 제어 구문도 추가되었습니다.
문법이 역시 조금 다른데요.
괄호(())는 필요없고 중괄호로 body 부분을 감싸줘야 합니다.

If

Go 에서 가장 단순한 if 문은 아래처럼 생겼어요.

if x > 0 {
    return y
}

중괄호를 의무적으로 사용하면 if 구문을 여러 줄에 걸쳐 작성할 때 코드 작성이 단순해집니다.
특히나 body 부분에 return 이나 break 같은 제어 구문을 포함할 때는 그렇게 작성하는게 훨씬 스타일이 살아나죠.

if 나 switch 구문에 초기화 구문을 같이 사용할 수 있기 때문에, 로컬 변수 셋팅을 이렇게 하는 경우가 많습니다.

if err := file.Chmod(0664); err != nil {
    log.Print(err)
    return err
}

Go 라이브러리를 보면 if 구문이 다음 구문으로 연결되지 않을 때 - 즉, body 부분이 break, continue, goto, return 등으로 끝날 때 - 불필요한 else 는 생략이 됩니다.

f, err := os.Open(name)
if err != nil {
    return err
}
codeUsing(f)

아래 코드는 일련의 error 가 발생하는 상황들을 대비하기 위한 코드를 작성하는 일반적인 예를 보여줍니다.
별다른 error 가 없으면 끝까지 성공적으로 잘 진행될 것이고, 중간에 error 가 발생하면 이를 잘 제거해나가겠죠.
error 가 발생하면 return 구문을 통해 이 상황을 끝내버리기 때문에 else 구문이 필요없죠.

f, err := os.Open(name)
if err != nil {
    return err
}
d, err := f.Stat()
if err != nil {
    f.Close()
    return err
}
codeUsing(f, d)

재선언과 재할당(Redeclaration and reassignment)

참고로, 바로 위의 예제는 := 를 이용해서 간략히 선언하는 방식이 어떻게 동작하는지를 잘 보여줍니다.
os.Open 을 호출하는 부분을 보면,

f, err := os.Open(name)

f 와 err 이라는 두 변수를 선언하고 있습니다.
몇 줄 뒤쪽에는 f.Stat 를 호출하는 부분이 있죠.

d, err := f.Stat()

이 부분은 d 와 err 을 선언하고 있는 것 같네요.
err 이라는 변수가 위 두 구문에 모두 나타났죠?
이렇게 중복되서 나와도 문제가 없어요.
err 변수가 첫번째 구문에서 선언이 된 후, 두번째 구문에서는 재할당이 된 것 말이예요.
f.Stat 호출 시에 이미 위에서 선언되어 현재 존재하는 err 변수를 사용하는데, 이 때는 새로운 값을 할당만 하는거예요.

다음의 경우에는 이미 선언되었더라도 := 를 통해 변수 v 가 다시 사용될 수 있어요.

  • 앞의 선언과 현재 하려는 선언이 같은 scope 에 있는 경우(v 가 바깥쪽의 scope 에서 선언되었다면 현재 선언 구문에서는 새로운 변수가 생성될거예요.)
  • 현재 선언에서 사용할 값이 v 에 할당이 가능할 경우
  • 현재 하려는 선언에 v 이외의 변수가 하나 이상 같이 사용될 경우

좀 특이하긴 하지만 꽤 실용적이죠.
예를 들면, if-else 구문들을 여러 개 연속해서 사용할 경우 err 변수 하나로만 사용하면 되잖아요. 쉽죠.
아마 이런 경우를 자주 보게 될거예요.

여기서 주목해야하는 부분이 있는데요.
함수의 인자들이나 리턴 값들은 함수의 body 를 감싸는 중괄호 바깥에 위치하는데도 불구하고, body 부분과 같은 scope 에 있는 것으로 취급해야한다는 거예요.

For

Go 의 for 반복문은 C 와 비슷하긴 하지만 완전히 같지는 않습니다.
for 와 while 은 합쳐버렸고, do-while 문은 제공하지 않아요.
아래처럼 3 가지 형식으로 사용하는데요.
그 중에서 한 가지만 세미콜론을 사용하네요.

// Like a C for
for init; condition; post { }

// Like a C while
for condition { }

// Like a C for(;;)
for { }

for 구문에서 변수를 간단하게 선언해서 사용할 수 있는데요.
이 때문에 반복문에서 index 선언을 쉽게 할 수 있습니다.

sum := 0
for i := 0; i < 10; i++ {
    sum += i
}

배열, 슬라이스, 맵, 채널을 이용하여 반복문을 수행할 경우에는 range 구문을 이용할 수 있습니다.

for key, value := range oldMap {
    newMap[key] = value
}

range 를 사용할 때 첫번째 요소(key 또는 index)만 사용하고자 할 경우에는 두번째 요소는 그냥 버리면 됩니다.

for key := range m {
    if key.expired() {
        delete(m, key)
    }
}

반대로 두번째 요소만 필요할 때는 blank identifier, 즉 언더스코어(_)를 사용해서 첫번째 요소는 버리면 됩니다.

sum := 0
for _, value := range array {
    sum += value
}

뒤쪽에서 살펴보겠지만 blank identifier 는 여러가지 용도로 사용될 수 있어요

range 가 string 과 함께 쓰이게 되면 더 많은 일을 해주게 되는데요.
UTF-8 로 파싱하여 각각 Unicode 문자 단위로 쪼개줍니다.
잘못 인코딩된 문자인 경우 첫번째 바이트는 무시한 후 rune 문자인 U+FFFD 로 대체해 줍니다.
(rune 은 Go 용어인데요. 자세한건 여기에서 스펙을 참고하세요.)

for pos, char := range "日本\x80語" { // \x80 is an illegal UTF-8 encoding
    fmt.Printf("character %#U starts at byte position %d\n", char, pos)
}

위의 코드를 수행하면 아래와 같이 찍어줍니다.

character U+65E5 '日' starts at byte position 0
character U+672C '本' starts at byte position 3
character U+FFFD '�' starts at byte position 6
character U+8A9E '語' starts at byte position 7

Go 는 콤마(comma) 연산자를 지원하지 않고, ++ 과 -- 는 표현식(expression)이 아니라 명령문(statement)입니다.
그래서, for 문에서 다수의 변수를 사용하려면 병렬 할당 방식(parallel assignment)을 사용해야 합니다.(++ 와 -- 를 배제하더라도요)

// Reverse a
for i, j := 0, len(a)-1; i < j; i, j = i+1, j-1 {
    a[i], a[j] = a[j], a[i]
}

Switch

Go 언어의 switch 구문은 C 보다 좀 더 일반적으로 사용할 수 있습니다.
표현식이 상수일 필요도 없어요. 정수일 필요도 없구요.
매치되는 case 가 나올 때까지 위에서부터 아래로 차례대로 비교합니다.
switch 구문에 표현식이 없으면 true 로 처리되기 때문에, if-else-if-else 와 같이 체인으로 작성된 것을 switch 로 변경하는 것이 가능하고, 또 일반적으로 이렇게 합니다.

func unhex(c byte) byte {
    switch {
    case '0' <= c && c <= '9':
        return c - '0'
    case 'a' <= c && c <= 'f':
        return c - 'a' + 10
    case 'A' <= c && c <= 'F':
        return c - 'A' + 10
    }
    return 0
}

각각의 case 들에 대해 차례대로 비교하는 기능은 없어지겠지만, 아래처럼 case 들을 묶어서 콤마로 연결하여 리스트로 작성할 수도 있습니다.

func shouldEscape(c byte) bool {
    switch c {
    case ' ', '?', '&', '=', '#', '+', '%':
        return true
    }
    return false
}

C 처럼 널리 사용되지는 않지만 Go 에서도 break 를 이용해서 switch 구문을 일찍 끝낼 수 있어요.
그런데, 때로는 switch 를 감싸고 있는 반복문을 빠져나가고 싶은 경우도 있겠죠.
switch 만 빠져나가는게 아니구요.
이럴 때 Go 에서는 반복문에 라벨(label)을 달아두고 그 라벨로 빠져나갈 수 있어요.

위의 두가지 예를 아래 코드로 확인해보세요.

Loop:
	for n := 0; n < len(src); n += size {
		switch {
		case src[n] < sizeOne:
			if validateOnly {
				break
			}
			size = 1
			update(src[n])

		case src[n] < sizeTwo:
			if n+1 >= len(src) {
				err = errShortInput
				break Loop
			}
			if validateOnly {
				break
			}
			size = 2
			update(src[n] + src[n+1]<<shift)
		}
	}

물론 continue 구문도 label 과 함께 사용될 수 있는데, 이는 반복문에만 해당되는 얘기겠죠.

이 섹션을 끝내기 전에 두개의 switch 구문을 이용해서 byte slice 를 비교하는 코드를 투척해 봅니다.

// Compare returns an integer comparing the two byte slices,
// lexicographically.
// The result will be 0 if a == b, -1 if a < b, and +1 if a > b
func Compare(a, b []byte) int {
    for i := 0; i < len(a) && i < len(b); i++ {
        switch {
        case a[i] > b[i]:
            return 1
        case a[i] < b[i]:
            return -1
        }
    }
    switch {
    case len(a) > len(b):
        return 1
    case len(a) < len(b):
        return -1
    }
    return 0
}

Type Switch

switch 구문을 interface 변수의 동적 타입을 알아내는데 사용할 수도 있어요.
이런 것을 type switch 라고 부르는데요.
type switch 는 괄호안에 type 이라는 키워드를 사용해서 type assertion 을 수행합니다. (역자: t.(type) 와 같이 사용하는 것을 type assertion 이라고 함)
switch 가 표현식에서 변수를 선언하면, 그 변수는 각각의 case 절에서 대응되는 타입을 가지게 될거예요.
아래처럼 switch 표현식에서 변수 이름을 재사용하는 것도 일반적으로 사용되는 방법이예요.
비록 이름은 같지만 각각 다른 타입으로 새로운 변수를 선언하는 것과 같은 효과이거든요.

var t interface{}
t = functionOfSomeType()
switch t := t.(type) {
default:
    fmt.Printf("unexpected type %T\n", t)     // %T prints whatever type t has
case bool:
    fmt.Printf("boolean %t\n", t)             // t has type bool
case int:
    fmt.Printf("integer %d\n", t)             // t has type int
case *bool:
    fmt.Printf("pointer to boolean %t\n", *t) // t has type *bool
case *int:
    fmt.Printf("pointer to integer %d\n", *t) // t has type *int
}

함수(Functions)

다중 반환 값(Multiple return values)

Go 언어의 독특한 기능 중 하나는 함수나 메소드가 여러 개의 값을 반환할 수 있다는 거예요.
이런 기능은 C 에서 주로 사용했던 몇 가지 짜증나는 방식을 개선할 수 있습니다.
예를 들면 EOF 를 나타내기 위해 -1 을 리턴한다든가, 인자로 주소값을 넘겨서 수정 가능하게 한다든가 하는 방법 말이예요.

C 에서는 쓰기 에러가 발생할 때 (뭔가 비밀스럽게 임시 장소에 기록하는 에러 코드와 함께) 음수 Count 반환 값으로 이를 알립니다.
Go 에서는 쓰기(Write)는 count 와 error 를 함께 반환할 수 있어요.
아래 정보를 한꺼번에 전달할 수 있는거죠.
"당신이 요청한 Write 는 일부 데이터를 쓰기는 했지만 다 쓰지는 못했어요. 왜냐면 기억 장치가 꽉 찼거든요."
아래는 os 패키지에서 제공하는 Write 메소드 원형(signature)인데요.

func (file *File) Write(b []byte) (n int, err error)

패키지 문서에서 알려주듯이 이 메소드는 기록된 데이터 byte 의 크기와 함께 n != len(b)일 경우 nil 이 아닌 error 를 반환합니다.
이게 일반적인 스타일이예요.
좀더 많은 예를 보고 싶으시면 에러처리(error handling) 섹션을 보세요.

reference 인자처럼 사용되는 변수를 리턴하기 위해 포인터를 넘길 필요도 없습니다.
다음 코드는 바이트 슬라이스에서 숫자를 찾은 다음 그 숫자와 다음 위치를 반환하는 단순한 함수입니다.

func nextInt(b []byte, i int) (int, int) {
    for ; i < len(b) && !isDigit(b[i]); i++ {
    }
    x := 0
    for ; i < len(b) && isDigit(b[i]); i++ {
        x = x*10 + int(b[i]) - '0'
    }
    return x, i
}

입력으로 b 라는 슬라이스를 받아서 숫자들을 찾아내는건데 아래처럼 이용할 수 있겠네요.

    for i := 0; i < len(b); {
        x, i = nextInt(b, i)
        fmt.Println(x)
    }

이름이 부여된 결과 인자(Named result parameters)

Go 함수에서는 반환 인자에 이름을 줄 수 있는데, 이를 일반 변수로서 사용할 수 있어요.
마치 함수의 입력 인자처럼요.
그렇게 반환 인자에 이름을 주면 함수가 시작할 때 그 타입의 zero 값에 해당하는 값으로 초기화됩니다.
함수를 리턴할 때 아무 인자없이 그냥 return 문을 주면, 이름을 준 인자들의 현재 값을 리턴하게 됩니다.

이름을 주는 것이 필수는 아니지만 이를 사용하면 코드를 더 짧고 명확하게 할 수 있죠.
문서의 효과를 얻을 수 있는거죠.
위의 코드에서 nextInt 함수의 반환 값들에 이름을 주게 되면 어떤 int 값을 반환하고 있는건지 명확해지겠죠.

func nextInt(b []byte, pos int) (value, nextPos int) {

이름이 부여된 반환 인자들이 초기화된 후 가진 값을 그대로 리턴하게 되므로, 이는 명확할 뿐 아니라 아주 단순하기도 하죠.
아래 코드는 이를 잘 보여주는 io.ReadFull 함수입니다.

func ReadFull(r Reader, buf []byte) (n int, err error) {
    for len(buf) > 0 && err == nil {
        var nr int
        nr, err = r.Read(buf)
        n += nr
        buf = buf[nr:]
    }
    return
}

Defer

Go 언어의 defer 문은 이를 감싸고 있는 함수가 리턴하기 직전에 함수 호출(지연된 함수)를 수행하도록 스케줄링하는 역할을 합니다.
이는 좀 특이한 방법이긴 한데, 함수의 수행경로가 어떻든지 간에 반드시 해제되어야 하는 리소스를 효과적으로 다룰 수 있는 방법이라고 할 수 있어요.
가장 대표적인 예가 mutex 를 해제(unlock) 한다거나 file 을 닫는(close) 경우예요.

// Contents 는 파일의 내용을 string 으로 반환합니다.
func Contents(filename string) (string, error) {
    f, err := os.Open(filename)
    if err != nil {
        return "", err
    }
    defer f.Close()  // 함수가 끝날 때 f.Close 가 호출될거예요.

    var result []byte
    buf := make([]byte, 100)
    for {
        n, err := f.Read(buf[0:])
        result = append(result, buf[0:n]...)
        if err != nil {
            if err == io.EOF {
                break
            }
            return "", err  // 여기서 리턴하면 f 가 close 될거예요.
        }
    }
    return string(result), nil // 여기서 리턴하면 f 가 close 될거예요.

Close 와 같은 함수 호출을 지연시키면 두가지 장점이 있는데요.
첫번째는, file 을 닫는걸 깜빡하는걸 막아준다는 거예요.
여러분이 만약 함수에 새로운 반환 경로를 추가하게 되었을 때 close 를 깜빡하기 쉽잖아요.
두번째는, close 가 open 과 아주 가까이 있다는건데요.
이는 함수의 끝에 위치시키는 것보다 훨씬 깔끔하고 명확하잖아요.

지연된 함수에서 사용하는 인자의 값(메소드일 경우 receiver 포함)은 defer 함수 자체가 실행될 때 평가된 값으로 사용해야해요.
지연된 함수가 호출될 때가 아니구요.
(역자: defer func() 구문이 호출된 시점의 값을 사용한다는 얘기)
그리고, 함수가 실행될 때 변수 값이 변하는 것을 걱정할 필요가 없는데요.
이는 하나의 defer 호출 위치에서 여러 개의 함수 실행을 지연시킬 수 있는걸 의미합니다.
좀 유치한 예가 아래에 있네요.
(역자: 아래와 같이 defer 구문 한줄로 여러개의 지연 함수를 등록할 수 있잖아요. 값도 이 때 이미 결정되기 때문에 나중에 i 값이 변경되거나 하는걸 걱정할 필요가 없다는 뜻이예요.)

for i := 0; i < 5; i++ {
    defer fmt.Printf("%d ", i)
}

지연 함수는 LIFO 에 의해 실행되기 때문에 위 코드는 함수가 반환될 때 4 3 2 1 0 의 순서로 출력하게 됩니다.
좀 더 그럴듯한 예는 아래와 같이 프로그램을 따라 함수를 trace 하는 방법이예요.
아래처럼 간단하게 trace 하는 코드를 넣을 수 있죠.

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

// Use them like this:
func a() {
    trace("a")
    defer untrace("a")
    // do something....
}

지연된 함수의 인자값이 defer 가 수행될 때 값이 결정된다는 사실을 이용해서 좀 더 좋은 방법을 사용해볼 수 있는데요.
trace 루틴이 untrace 루틴의 인자값을 미리 셋업해둘 수 있어요.
아래 예를 보세요.

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"))
    fmt.Println("in a")
}

func b() {
    defer un(trace("b"))
    fmt.Println("in b")
    a()
}

func main() {
    b()
}

이는 아래처럼 출력됩니다.

entering: b
in b
entering: a
in a
leaving: a
leaving: b

다른 언어의 블록 수준 리소스 관리에 익숙한 프로그래머에게는 defer가 특이 해 보일 수 있어요. 하지만, 제일 재밌고 강력한 애플리케이션은 블록 기반이 아니라 함수 기반으로부터 오는 것입니다.
우리는 panic 과 recover 에 관한 섹션에서 그 가능성에 대한 또 다른 예를 보게 될거예요.


"쉽게 읽는 Effective Go (3)" 계속해서 읽으실거죠? ^^

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