시작하며

Go 함수는 단순한 입출력 처리를 넘어 일급 객체(First-Class Citizen)로 동작한다. 가변인수, defer, 함수 리터럴(람다), 클로저 캡처 등의 기능을 통해 더 유연하고 표현력 있는 코드를 작성할 수 있다.

Function 심화

가변인수 함수(Variadic Function) 만들기

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

가변인수 함수와 interface{}

모든 타입을 받을 수 있는 가변인수는 빈 인터페이스(interface{})를 사용한다. 모든 타입은 interface{}를 포함하고 있으므로 interface{}를 가변인수로 받으면 모든 타입을 받을 수 있다.

  func alltypes(args ...interface{}) {
    for _, v := range args {
      fmt.Printf("타입: %T, 값: %v\n", v, v)
    }
  }

Defer란 무엇인가?

함수 종료 전에 실행되는 Defer

defer는 함수의 종료 전 반드시 실행되어야 하는 구문을 표현할 때 사용된다. 예를 들어 운영체제에서 사용한 자원을 반환해야 할 때 사용하면 유용하다. 특이한 점은, 작성한 defer의 역순으로 실행된다는 점이다.

  defer fmt.Println("반드시 호출됩니다.")  // 1
  defer f.Close()                     // 2
  defer fmt.Println("파일을 닫았습니다.") // 3
  // 실행 순서: 3 -> 2 -> 1

함수 타입 변수

함수 타입 변수

상황에 따라 다른 함수를 호출해야 하는 경우, 함수 타입 변수를 사용한다. 리턴 타입이 함수인 것. 함수 또한 프로그램 카운터(PC)에 의해 표시되는 주소이다.

  func add(a, b int) int {
    return a + b
  }
 
  func mul(a, b int) int {
    return a * b
  }
 
  // calculation의 리턴 타입은 `func (int, int) int` 이다.
  func calculation(op string) func(int, int) int {
    if op == "+" {
      return add
    }
    // ...
  }
  // 매개변수로 요약한다면
  // type calReturnType func (int, int) int
  // func calculation(op string) calReturnType { ... }

Function Literal이란 무엇인가?

익명함수, Lambda라는 용어로 더 익숙하다. 함수명이 없는 형태로, 함수명으로 호출하지 않고 함수 타입 변수로만 호출된다.

  func calculation(op string) func(int, int) int {
    if op == "+" {
      return func(a, b int) int {
        return a + b
      }
    }
    // ...
  }
 
  // 사용 형태 1)
  function := func(a, b int) int {
    return a + b
  }
  res := function(1,2)
 
  // 사용 형태 2) 즉시 실행
  function := func(a, b int) int {
    return a + b
  }(1,2)

Function Literal 내부 상태 (클로저 캡처)

함수 리터럴은 필요한 외부 변수를 내부 변수로 가질 수 있다. 함수 리터럴 내부에서 사용되는 외부 변수는 자동으로 함수 내부 상태로 저장된다. 이렇게 함수 리터럴 외부 변수를 함수 내부로 가져오는 것을 캡쳐(capture)라고 한다. 캡쳐는 값 복사가 아닌 참조 형태로 가져오게 된다. 따라서 아래 코드를 그대로 실행하면 3,3,3이 리턴된다. i에 대한 참조 복사가 일어나기 때문이다. 이를 방지하려면 주석처럼 값을 임의 변수에 대입해 해당 변수를 함수 리터럴 내부에서 사용한다.

  func CaptureOne() {
    f := make([]func(), 3)
    fmt.Println("this is refer capt")
    for i := 0; i < 3; i++ {
      // 캡쳐에서 값 복사를 원하는 경우
      // v := i  -> 이 경우, for문이 반복될 때마다 새로운 v가 할당된다.
      f[i] = func() {
        fmt.Println(i)
        // 캡쳐에서 값 복사를 원하는 경우
        // fmt.Println(v)
      }
    }
 
    for i := 0; i < 3; i++ {
      f[i]()
    }
  }

Function Literal 예시 - 파일 핸들

아래 코드에서는 writeHello 안에 들어가는 파라미터를 함수 리터럴 형태로 구현했다. writeHello 입장에서는 넘어오는 writer가 어떤 방식으로 동작할지 모르게 된다. 이런 방식으로 외부에서 로직을 주입하는 형태의존성 주입(Dependency Injection)이라고 한다.

  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.Fprintln(f, msg)
    })
  }

정리하며

Go 함수는 일급 객체로서 변수에 할당하거나 다른 함수의 인수로 전달하고 반환값으로 사용할 수 있다. defer는 자원 해제를 보장하는 강력한 도구이며, 클로저 캡처를 이용하면 외부 상태를 함수 안으로 자연스럽게 끌어들일 수 있다. 의존성 주입 패턴과 결합하면 테스트와 확장에 유리한 코드 구조를 만들 수 있다.

연습 문제

Q1. 가변인수 키워드, int형 가변인수의 리턴 타입, 모든 타입을 받는 방법

// 1. ...
// sub1. []int (슬라이스)
// sub2. 에러가 발생한다. 함수 입력을 슬라이스로 입력하면 정상 동작한다.
// sub3. interface{} 를 사용한다.

Q2. 함수 리터럴이란? 외부 변수를 내부로 가져오는 것을 무엇이라 부르는가?

1) 함수명이 없는 형태로, 함수명으로 호출하지 않고 함수 타입 변수로만 호출하는 형태 (lambda)
2) capture

Q3. 아래 코드에서 의존성 주입을 설명하고, 인터페이스 형태로도 구현하라.

  type Writer func(string)
 
  func writeHello(writer Writer) {
    writer("Hello World")
  }
 
  // 인터페이스 형태로 변환한다면
  // type Writer interface{
  //   Write(msg string)
  // }
  // func writeHello(writer Writer) {
  //   writer.Write("Hello World")
  // }