[Golang] Function 심화

2021-11-21 hit count image

Golang에서 Function(함수)를 좀 더 자세히 살펴보고, 함수를 활용하는 다양한 방법에 대해서 알아봅시다.

개요

이번 블로그 포스트에서는 Golang에서 Function(함수)에 대해 좀 더 자세히 알아보고 함수를 활용하는 다양한 방법에 대해서 알아봅시다. 이 블로그 포스트에서 소개하는 코드는 다음 링크를 통해 확인하실 수 있습니다.

함수 기초

Golang에서 함수를 다루는 기초적인 방법은 이전 블로그 포스트를 확인해 주시기 바랍니다.

가변 인수 함수

Golang에서는 함수에서 다음과 같이 가변 인수(Variable argument list)를 사용할 수 있습니다.

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

Golang에서는 다음과 같이 ... 키워드를 사용하여 가변 인수 함수를 만들 수 있습니다.

func FUNCTION_NAME(param ...TYPE) TYPE {
  // Code block
}

이를 확인하기 위해 main.go 파일을 생성하고 다음과 같이 수정합니다.

package main

import "fmt"

func sum(nums ...int) int {
  fmt.Printf("nums type: %T\n", nums)

  sum := 0
  for _, num := range nums {
    sum += num
  }
  return sum
}
func main() {
  fmt.Println(sum(1, 2, 3, 4, 5))
  fmt.Println(sum(10, 20))
  fmt.Println(sum())
}

이를 실행하면 다음과 같은 결과를 얻을 수 있습니다.

# go run main.go
nums type: []int
15
nums type: []int
30
nums type: []int
0

defer

Golang은 defer라는 지연 실행 구문을 제공합니다. defer는 함수 종료전에 실행됨을 보장합니다.

defer STATEMENT

이를 확인하기 위해서 main.go 파일을 다음과 같이 수정합니다.

package main

import "fmt"

func print() {
  defer fmt.Println("World!")
  fmt.Println("Hello")
}

func main() {
  print()
}

이를 실행하면 다음과 같은 결과가 출력되는 것을 확인할 수 있습니다.

# go run main.go
Hello
World!

defer는 파일을 읽기 위해 파일을 열고 닫는 등, 주로 OS 자원을 반환할 때 사용됩니다. 이를 확인하기 위해 main.go 파일을 다음과 같이 수정합니다.

package main

import "os"

func readFile() {
  f, err := os.Open("file.txt")

  if err != nil {
    return
  }

  f.Close()
}

func readFileDefer() {
  f, err := os.Open("file.txt")
  defer f.Close()

  if err != nil {
    return
  }
}

func main() {
  readFile()
  readFileDefer()
}

defer를 사용하지 않는 경우, Close 함수 전에 오류가 발생하면, Close 함수는 호출되지 않습니다.

func readFile() {
  f, err := os.Open("file.txt")

  if err != nil {
    return
  }

  f.Close()
}

하지만 defer를 사용하면 오류가 발생하더라도, Close 함수의 호출이 보장됩니다.

func readFileDefer() {
  f, err := os.Open("file.txt")
  defer f.Close()

  if err != nil {
    return
  }
}

함수 타입 변수

Golang에서는 함수도 타입으로 사용할 수 있습니다. 여기서 함수 타입 변수란 함수를 값으로 갖는 변수를 말합니다. 함수도 메모리에 시작 주소값을 가지고 있기 때문에 변수에 할당을 할 수 있습니다.

함수 타입은 다음과 같이 함수 시그니쳐(Function signature)로 표현합니다.

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

// Signature
func (int int) int

이를 확인하기 위해 main.go 파일을 다음과 같이 수정합니다.

package main

import "fmt"

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

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

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

func main() {
  var op func(int, int) int

  op = getOperator("+")
  result := op(10, 20)
  fmt.Println(result)

  op = getOperator("*")
  result = op(10, 20)
  fmt.Println(result)
}

이를 실행하면 다음과 같은 결과를 확인할 수 있습니다.

# go run main.go
30
200

함수 시그니쳐의 별칭 타입

함수 시그니쳐에 별칭 타입을 사용하여 좀 더 간단하게 함수 타입을 사용할 수 있습니다.

// Function signature alias type
type OperateFunc func(int, int) int

func getOperator(op string) OperateFunc {
}

이를 확인하기 위해 main.go 파일을 다음과 같이 수정합니다.

package main

import "fmt"

type OperateFunc func(int, int) int

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

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

func getOperator(op string) OperateFunc {
  if op == "+" {
    return add
  } else if op == "*" {
    return multiple
  } else {
    return nil
  }
}

func main() {
  var op OperateFunc

  op = getOperator("+")
  result := op(10, 20)
  fmt.Println(result)

  op = getOperator("*")
  result = op(10, 20)
  fmt.Println(result)
}

이를 실행하면 다음과 같은 결과를 얻을 수 있습니다.

# go run main.go
30
200

별칭 타입을 이용하면 함수 시그니쳐를 좀 더 간단하게 사용할 수 있음을 알 수 있습니다.

함수 리터럴

Golang에서도 함수 리터럴(익명 함수)을 사용할 수 있습니다.

f := func(a, b int) int {
}

이를 확인하기 위해 main.go 파일을 다음과 같이 수정합니다.

package main

import "fmt"

type OperateFunc func(int, int) int

func getOperator(op string) OperateFunc {
  if op == "+" {
    return func(a, b int) int {
      return a + b
    }
  } else if op == "*" {
    return func(a, b int) int {
      return a * b
    }
  } else {
    return nil
  }
}

func main() {
  var op OperateFunc

  op = getOperator("+")
  result := op(10, 20)
  fmt.Println(result)

  op = getOperator("*")
  result = op(10, 20)
  fmt.Println(result)
}

이를 실행하면 다음과 같은 결과를 얻을 수 있습니다.

# go run main.go
30
200

Golang에서 일반적으로 함수는 상태를 가질 수 없지만, 함수 리터럴은 내부 상태를 가질 수 있습니다. 이를 확인하기 위해 main.go 파일을 다음과 같이 수정합니다.

package main

import "fmt"

func main() {
  i := 0

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

  i++

  f()

  fmt.Println(i)
}

이를 실행하면 다음과 같은 결과를 얻을 수 있습니다.

# go run main.go
11

일반 함수는 외부에 선언된 변수에 접근할 수 없지만, 함수 리터럴은 외부에 선언된 변수에 접근(캡쳐)하여 사용할 수 있습니다. 여기서 캡쳐(Captured)된 값은 값 복사가 아닌 레퍼런스 복사가 이루어집니다. 즉, 포인터가 복사된다고 할 수 있습니다.

이를 확인하기 위해 main.go 파일을 다음과 같이 수정합니다.

package main

import "fmt"

func CaptureLoop1() {
  f := make([]func(), 3)
  fmt.Println("CaptureLoop1")

  for i := 0; i < 3; i++ {
    f[i] = func() {
      fmt.Println(i)
    }
  }

  for i := 0; i < 3; i++ {
    f[i]()
  }
}

func CaptureLoop2() {
  f := make([]func(), 3)
  fmt.Println("CaptureLoop2")

  for i := 0; i < 3; i++ {
    v := i
    f[i] = func() {
      fmt.Println(v)
    }
  }

  for i := 0; i < 3; i++ {
    f[i]()
  }
}

func main() {
  CaptureLoop1()
  CaptureLoop2()
}

이를 실행하면 다음과 같은 결과를 확인할 수 있습니다.

# go run main.go
CaptureLoop1
3
3
3
CaptureLoop2
0
1
2

CaptureLoop1for의 변수 i의 인스턴스를 직접 참조하고 있으므로, i의 마지막 값인 3이 모두 출력됩니다. 하지만, CaptureLoop2i의 값을 v로 복사한 후, v의 인스턴스를 참조하고 있고 v는 for 루프에서 매번 새롭게 생성되므로, 0~2까지 모든 값들이 출력되는 것을 확인할 수 있습니다.

의존성 주입

Golang에서는 함수 타입을 이용하여 의존성 주입(DI - Dependency Injection)을 사용할 수 있습니다. 의존성 주입이란 비즈니스 로직이 내부에 존재하는 것이 아니라 외부로부터 로직을 주입받는 것을 의미합니다.

package main

import "fmt"

type PrintConsole func(string)

func PrintHello(p PrintConsole) {
  p("Hello, World!")
}

func main() {
  PrintHello(func(msg string) {
    fmt.Println(msg)
  })
}

이를 실행하면 다음과 같은 결과를 얻을 수 있습니다.

# go run main.go
Hello, World!

PrintHelloPrintConsole이 어떤 역할(비즈니스 로직)을 하는지 알지 못합니다. PrintHelloPrintConsole을 통해 의존성이 주입되었다고 할 수 있습니다.

완료

이것으로 함수에 대해서 좀 더 깊게 알아보고, 함수를 활용하는 방법에 대해서 알아보았습니다. 여기서 소개한 방법들을 활용하여 좀 더 멋진 코드를 작성해 보시기 바랍니다.

제 블로그가 도움이 되셨나요? 하단의 댓글을 달아주시면 저에게 큰 힘이 됩니다!

앱 홍보

책 홍보

블로그를 운영하면서 좋은 기회가 생겨 책을 출판하게 되었습니다.

아래 링크를 통해 제가 쓴 책을 구매하실 수 있습니다.
많은 분들에게 도움이 되면 좋겠네요.

스무디 한 잔 마시며 끝내는 React Native, 비제이퍼블릭
스무디 한 잔 마시며 끝내는 리액트 + TDD, 비제이퍼블릭
[심통]현장에서 바로 써먹는 리액트 with 타입스크립트 : 리액트와 스토리북으로 배우는 컴포넌트 주도 개발, 심통
Posts