본문 바로가기

[Go] 함수 고급 본문

Dev

[Go] 함수 고급

겨울바람_ 2024. 12. 2. 22:29

가변 인수 함수

fmt.Println()
fmt.Println(1)
fmt.Println(1, 2, 3, 4, 5)

 

fmt 패키지의 Println 함수는 인수 개수가 정해져 있지 않다. 이처럼 인수의 개수가 고정되어 있지 않은 함수를 가벼 인수 함수라고 한다.

 

... 키워드를 사용해서 가변 인수를 생성할 수 있다. 인수 타입 앞에 ...를 붙여서 해당 타입 인수를 여러 개 받는 가변 인수임을 표시하면 된다.

func sum(nums ...int) int {
    sum := 0

    fmt.Printf("nums 타입 : %T\n", nums)
    for _, v := range nums {
        sum += v
    } 
    return sum
}

func main() {
    fmt.Println(sum(1, 2, 3, 4, 5)) // nums 타입 : []int 15
    fmt.Println(sum(10, 20))        // nums 타입 : []int 30
    fmt.Println(sum())              // nums 타입 : []int 0
}

출력되는 타입에서 확인할 수 있듯이 가변 인수는 함수 내에서 슬라이스 타입으로 처리된다.

 

fmt.Println() 함수는 이처럼 동일한 타입의 인수를 가변적으로 받을 수도 있지만, 다른 타입의 인수도 여러 개 받을 수 있다. 이는 빈 인터페이스를 인수로 활용했기 때문이다.

 

...interface{} 타입으로 인수를 받으면 모든 타입의 가변 인수를 받을 수 있다.

func Print(args ...interface{}) string {
    for _, arg := range args {
        switch f := arg.(type) {
        case bool:
            val := arg.(bool)
        case float64:
            val := arg.(float64)
        case int:
            val := arg.(int)
        }
    }
}

defer 지연 실행

함수가 종료되기 직전에 실행해야 하는 코드가 존재할 때 사용하는 defer 에 대해 알아보자. 대표적으로 OS 내부 자원을 사용하는 경우, 사용 후 반드시 OS에 해당 자원을 반환해야 한다.

 

만약 자원을 되돌려주지 않으면 내부 자원이 고갈되어 본래의 기능을 제대로 수행할 수 없기 때문이다. 이러한 경우 자원 반환을 잊지 않고 수행하기 위해 defer를 사용해 해당 기능을 수행할 수 있다.

func main() {
    f, err := os.Create("test.txt")
    if err != nil {
        fmt.Println("failed to create a file")
        return
    }

    defer f.Close()
    defer fmt.Println("defer 동작")

    fmt.Println("파일에 Hello World를 쓴다.")
    fmt.Fprintln(f, "Hello World!")
}

위의 main 함수를 실행하면 생성된 텍스트 파일에 Hello World! 를 작성한 후 함수의 마지막 단계에 "defer 동작" 이라는 문구가 콘솔에 출력되는 것을 확인할 수 있다.

함수 타입 변수

함수 타입 변수란 함수를 값으로 갖는 변수를 의미한다. 포인터는 0과 1로 나타낼 수 있는 숫자를 메모리 주소 값으로 가진다.

 

작성된 코드 중 100번 라인에서 f() 함수가 시작된다고 가정해보자.

 

CPU의 프로그램 카운터(PC)는 다음 실행할 라인을 나타내는 레지스터인데, 코드 실행 중 f() 함수가 호출되면 프로그램 카운터는 f() 함수가 시작되는 라인인 100번 라인을 값으로 가지며, 다음 차례에 100번 라인부터 명령을 실행하게 된다.

 

이를 포인터처럼 라인을 통해 함수의 위치를 가리킨다고 하여 함수 포인터 (function pointer)라고 부른다.

func add(a, b int) int {
    return a + b
}

func mul(a, b int) int {
    return a * b
}

func getOperator(op string) func (int, int) int {
    if op == "+" {
        return add
    } else if op == "*" {
        return mul
    } else {
        return nil
    }
}

func main() {
    var operator func (int, int) int
    operator = getOperator("*")

    var result = operator(3, 4)
    fmt.Println(result) // 12
}

이러한 함수 타입 변수를 사용할 경우 별칭을 사용하여 함수 정의를 줄여 쓸 수 있다.

type opFunc func (int, int) int

func getOperator(op string) opFunc {
    ...
}

함수 리터럴

함수 리터럴은 함수명을 적지 않고 함수 타입 변숫값으로 대입되는 함수값을 의미한다. 익명 함수 혹은 람다 함수라고 불리기도 한다.

return func(a, b int) int {
    return a + b
}

함수 리터럴은 필요한 변수를 내부 상태로 가질 수 있다. 함수 리터럴 내부에서 사용되는 외부 변수는 자동으로 함수 내부 상태로 저장된다.

func main() {
    i := 0

    f := func() {
        i += 10
    }

    i++

    f()

    fmt.Println(i) // 11
}

i는 함수 외부에 선언되어 있는 외부 변수다. 함수 리터럴 내부에서 외부 변수에 접근할 때 필요한 변수를 내부 상태로 가져와서 접근할 수 있도록 한다. 결과적으로 i의 값은 f() 함수에 의해 10이 더해져 11이 된다.

 

함수 리터럴 내부로 외부 변수를 가져오는 것을 캡쳐라고 한다. 캡쳐는 값 복사가 아닌 참조 형태로 가져오는 것에 주의해야 한다.

 

값을 복사하는 것이 아닌 외부 변수의 주소값을 포인터 형태로 함수 리터럴 내부로 가져와 사용하게 된다. 값 참조를 사용할 때 외부 변수에 영향을 미치기 싫다면 해당 값을 변수로 받아 값을 복사하는 것을 권장한다.

type Writer func(string)

func writeHello(writer Writer) {
    writer("Hello World!")
}

func main() {
    f, err := os.Create("test.txt")
    if err != nil {
        fmt.Println("Failed to create a file")
        return
    }

    defer f.Close()

    writeHello(func(msg string) {
        fmt.Fprintf(f, msg)
    })
}

파일에 msg를 쓰는 함수 리터럴을 만들어서 writeHello() 함수의 인수로 사용했다. writeHello() 함수는 함수 리터럴을 "Hello World!" 문자열을 인수로 호출했기 때문에 위 예제를 실행하면 test.txt 파일이 생성되고 해당 문자열이 저장된다.

 

위의 예제는 의존성 주입을 활용한 함수로 writeHello() 함수는 인수로 오는 writer를 호출했을 때 해당 함수가 어떻게 수행될지 알 수 없다. 이렇듯 외부에서 로직을 주입하는 방식으로 객체지향적인 코드 작성이 가능하다.

'Dev' 카테고리의 다른 글

단일 트랜잭션 유지 그리고 PGMQ  (1) 2024.12.15
[Go] 인터페이스  (3) 2024.11.10
[Go] 메소드  (2) 2024.11.09
[Go] 슬라이스  (0) 2024.11.07
그림으로 이해하는 객체 지향 설계 5원칙 [SOLID]  (2) 2024.07.20
Comments