본문 바로가기
Language/Go

Effective Go - Functions

by ocwokocw 2022. 5. 7.

- 출처: https://go.dev/doc/effective_go#functions

- Multiple return values

Go의 특이한 점중 하나는 함수와 메소드가 여러 값을 반환할 수 있다는 것이다. Go 에서 Write 메소드는 count과 error를 반환하는데 이 덕분에 "일부 bytes를 쓰긴했지만 어떤 error 때문에 전부 쓴것은 아니다."와 같은 정보를 알 수 있게 된다. os package의 Write 메소드 시그니처를 살펴보자.
 
func (file *File) Write(b []byte) (n int, err error)
 
위의 함수는 n != len(b) 일 때 쓰여진 byte의 수와 nil이 아닌 error를 반환한다. Go에서는 이런 형태를 많이 사용하고 있다.
 
multiple 반환을 이용하면 참조 parameter를 시뮬레이션하기 위해 반환값에 포인터를 넘겨줄 필요가 없다. 아래 코드는 byte slice에서 특정 position이 주어졌을 때 숫자를 추출하고, 해당 숫자와 다음 position을 반환하는 함수이다.
 
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
}
 
위의 함수를 input slice "b"에서 숫자를 스캔하기 위해 아래와 같이 사용할 수 있다.
 
 for i := 0; i < len(b); {
        x, i = nextInt(b, i)
        fmt.Println(x)
}
 

- Names result parameters

Go 함수의 반환이나 결과 "parameter"(여기서 parameter라 함은 함수의 인자로 들어오는 parameter가 아니라 반환하는 parameter를 말한다.)  들에는 이름을 부여할 수 있으며, 인자와 마찬가지로 일반 변수처럼 사용이 가능하다. 이름을 붙여졌을 때 함수가 시작되면 해당 type에 대한 zero value로 초기화된다. 만약 함수가 인자없이 return 문을 실행하면(return a,b와 같이 지정하지 않고 return만 사용하는 경우) 결과 parameter 들의 현재 값은 반환되는 값으로 사용된다.
 
이름 값은 필수는 아니지만 코드를 더 짧고 간결하게 만들어 준다. 위에서 예로든 함수 nextInt의 결과 parameter들은 모두 int형인데 이름을 붙여주면 반환된 int가 어떤 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 문은 defer를 실행하는 함수가 반환하기 직전에 해당 함수를 수행하도록 스케줄링한다. 일반적인 프로그램의 흐름은 아니지만 함수가 언제 끝나는지(어느 시점에 return문이 실행 되는지) 상관없이 반드시 해제해야하는 자원을 존재하는 경우와 같은 상황에서 매우 유용하다.
 
// Contents returns the file's contents as a string.
func Contents(filename string) (string, error) {
    f, err := os.Open(filename)
    if err != nil {
        return "", err
    }
    defer f.Close()  // f.Close will run when we're finished.

    var result []byte
    buf := make([]byte, 100)
    for {
        n, err := f.Read(buf[0:])
        result = append(result, buf[0:n]...) // append is discussed later.
        if err != nil {
            if err == io.EOF {
                break
            }
            return "", err  // f will be closed if we return here.
        }
    }
    return string(result), nil // f will be closed if we return here.
}
 
위의 코드에서 Close 와 같은 함수 호출을 연기(Deferring)하는 것은 2 가지 측면에서 이점이 있다. 우선 나중에 코드를 수정할 때 defer가 없다면 새로운 return 경로를 추가하는 경우 해당 return 이전에 file을 닫는 처리를 추가해줘야 한다. 하지만 이런 부분은 까먹기 쉬운데 defer를 사용하면 이런 행동을 보증해주기 때문에 편리하다.
 
또한 가독성 측면에서도 좋은데 file을 닫는 행위를 open한 바로 다음 줄에 위치시키면 서로 연관된 행위와 관련된 코드를 집약시킬 수 있다. 만약 defer가 없다면 함수 맨 끝에 반환하기 전에 file을 닫는 행위가 나타나야 한다.
 
deferred 함수에 넘기는 인자는 호출 할때가 아니라 defer를 실행하는 시점에 평가된다. 
 
for i := 0; i < 5; i++ {
    defer fmt.Printf("%d ", i)
}
 
위의 코드에서 개발자가 defer문을 사용한 의도는 각 for loop 마다 마지막에 i값을 출력하는 의도로 작성했을 가능성이 높지만 실제로는 함수를 지연시켰다가 for loop이 전부다 종료된 다음에 실행된다. 
 
Deferred된 함수로 넘기는 인자가 실제 함수가 호출될 때 평가된다면 위의 마지막으로 설정된 i 값만 5번 출력될것이다. 하지만 defer를 실행하는 순간에 평가되기 때문에 각 for loop 마다 defer를 실행한 순간의 i 값을 인자로 갖고 있다.
 
Deferred 함수는 LIFO(Last in, first out) 순서를 갖기 때문에 함수가 반환되기 이전에 "4 3 2 1 0"을 출력하게 된다.
 
만약 프로그램에서 실행되는 함수를 추적하는 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....
}
 
위의 함수도 동작이 명확하고 딱히 나무랄데는 없지만 "deferred 함수에 넘겨지는 인자의 평가가 실제 호출시점이 아니라 defer를 실행하는 시점에 평가된다"는 사실을 이용하면 좀더 우아하게 작성이 가능하다.
 
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 함수의 인자가 실제 호출될 때 평가된다면 entering와 leaving이 in a나 in b 다음에 함수가 끝나기 전에 출력될것이다. 하지만 defer를 실행할 떄 평가되기 때문에 우리가 의도한 순서대로 출력됨을 알 수 있다.
 
다른 프로그래밍 언어를 사용해본 경험이 있다면 block 수준에서 자원을 사용하고 해제하는 구문에 익숙할 수도 있다. 하지만 강력한 application은 기능은 block 기반이 아니라 function 기반에서 그 힘이 나온다. 
 
 

'Language > Go' 카테고리의 다른 글

Effective Go - Array and Slice  (0) 2022.05.08
Effective Go - New 와 Make  (0) 2022.05.07
Go - Reading files  (0) 2022.03.13
Go - Context  (0) 2022.02.27
Go - Sync  (0) 2022.01.20

댓글