[Go] 14. 함수 고급편

2024. 2. 4. 16:57Go

반응형

1. 가변 인수 함수

함수의 인수 개수를 고정하지 않는 함수를 가변 인수 함수라고 합니다.

fmt 패키지의 Println은 아래와 같이 사용 가능합니다.

fmt.Println("Hi")
fmt.Println(23, "apple")
fmt.Println(3.14, "ABC", 10)

Println 함수를 살펴보면, 아래와 같이 ...interface{} 타입을 인수값으로 가지고 있습니다.

type any = interface{}

func Println(a ...any) (n int, err error) {
	return Fprintln(os.Stdout, a...)
}

... 키워드를 이용하면 가변 인수를 처리할 수 있습니다.

func sum(nums ...int) int {
	result := 0
	for _, num := range nums {
		result += num
	}
	return result
}

func main() {
	fmt.Println(sum(2))
	fmt.Println(sum(5, 6, 10))
	fmt.Println(sum(22, 99))
}

...키워드로 설정한 가변 인수는 함수 내부에서 슬라이스타입으로 처리됩니다.

sum 함수에 설정한 nums 인수의 경우, []int 로 처리됩니다.

만일, 모든 타입을 받을 수 있는 가변인수를 함수에 이용하고 싶다면,
Println 함수와 같이 ...interface{} 타입을 받으면 됩니다


2. defer 지연 실행

함수가 종료되기 직전에 반드시 실행되어야하는 코드가 있다면,
defer 지연 실행을 이용하면, 종료되기 직전에 해당 명령을 실행할 수 있습니다.

defer 명령문

지연실행이 필요한 대표적인 예로는 파일이나 소켓과 같은 OS 내부 자원을 사용하는 함수가 있습니다.
파일을 생성하거나 읽는 행위는 OS 내부자원을 이용하는 것이기 때문에, 코드상에서 해당 기능을 이용할 때, 자원을 빌려씁니다.
때문에, 사용한 후에는 OS에게 핸들을 반환해줘야 내부자원이 고갈되지 않습니다.

defer 지연실행은 역순으로 실행됩니다.
즉, 가장 먼저 선언된 defer 함수가 가장 나중에 실행됩니다.

아래의 예제코드를 보면 맨 나중에 선언한 defer 지연실행이 가장 먼저 실행된 것을 확인할 수 있습니다.

package main

import (
	"fmt"
	"os"
)

func main() {
	file, err := os.Create("data.txt")
	if err != nil {
		fmt.Println("파일 생성 중 오류 발생")
		return
	}
	defer fmt.Println("defer 지연실행은 맨먼저 선언한것이 맨 나중에 실행됩니다.")
	defer file.Close()
	defer fmt.Println("맨 마지막에 사용한 defer 지연실행이지만 가장 먼저 실행됩니다.")

	fmt.Println("파일에 Hello jiniworld!를 썼습니다.")
	fmt.Fprintln(file, "Hello jiniworld!")
}
파일에 Hello jiniworld!를 썼습니다.
맨 마지막에 사용한 defer 지연실행이지만 가장 먼저 실행됩니다.
defer 지연실행은 맨먼저 선언한것이 맨 나중에 실행됩니다.

3. 함수 타입 변수, 함수 리터럴

Go에서의 함수는, javascript와 같이 함수를 리턴타입으로 사용할 수 있습니다.

아래 예제를 살펴봅시다.

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

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

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

getOperator 함수와 같이 함수를 리턴할 수 있습니다.
이미 선언된 함수명을 리턴할수도 있고,
else if op == "-" 에서 정의했던 것 처럼, 익명함수 형태를 리턴할 수도 있습니다.

Go에서는 익명함수를 함수 리터럴 이라고 부릅니다.

함수명을 적지 않고 함수타입 변수값에 대입되는 함수값을 함수리터럴이라고 부릅니다. Java에서는 람다라고 부릅니다.


3.1. 별칭을 이용하여 가독성 높이기

함수를 리턴타입으로 사용하는 것은 가독성이 떨어지기 때문에, 아래와 같이 별칭을 정의하여 사용할 수도 있습니다.

type opFunc func(int, int) int

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

4. 함수 리터럴

외부에 정의된 외부변수를 함수 리터럴 내에서도 사용할 수 있습니다.

func main() {
	num := 1
	f := func(a int) {
		num *= a
	}
	num += 100
	f(2)
	fmt.Println(num)
}

함수리터럴을 선언할 때가 아닌, 그 함수리터럴을 사용할 때, 함수 내부가 실행됩니다.
따라서, 위의 코드의 실행결과는 202가됩니다. (1 -> 101 -> 202)

4.1. 캡처

외부변수를 함수 리터럴 내에서 사용할 때, 주의해야할 점이 있습니다.
함수리터럴은 호출되는 시점에 실행된다는 점입니다.

[]func() 를 이용하여 만약, 0, 1, 2를 출력하고자 할때 아래와 같은 방식으로 함수를 정의하면 예상과 달리 3, 3, 3이 출력됩니다.

func CaptureLoop() {
	f := make([]func(), 3)
	fmt.Println("ValueLoop")
	for i := 0; i < 3; i++ {
		f[i] = func() {
			fmt.Println(i)
		}
	}
	for i := 0; i < len(f); i++ {
		f[i]()
	}
}

함수 리터럴 내에서 외부변수는 값을 복사하는 것이 아니라, 변수값의 참조(주소값) 를 이용하여 캡쳐됩니다.
그런데 함수 리터럴 내의 함수는 호출시점에 실행되기 때문에, 변수값은 for루프를 모두 마친 후, 3이된 시점의 참조값을 읽어들이기 때문에 모두 3으로 출력되는 것입니다.

우리가 원래 의도했었던 것 처럼 0, 1, 2를 출력하게 하고 싶다면,
함수리터럴 내에서 외부 변수 i를 그대로 사용하는 것이 아니라, 새로운 변수에 값을 복사한 후, 그 값을 활용하면 됩니다.

func CaptureLoop2() {
	f := make([]func(), 3)
	fmt.Println("ValueLoop")
	for i := 0; i < 3; i++ {
		v := i
		f[i] = func() {
			fmt.Println(v)
		}
	}
	for i := 0; i < len(f); i++ {
		f[i]()
	}
}

4.2. 파일 핸들을 함수 리터럴 내에서 사용하기

함수타입을 인자로 받는 함수를 이용하고자 할때, 함수리터럴을 이용하면, 외부변수값을 함수 리터럴 내에서 사용할 수 있다고 했습니다.

여기에서 더 나아가, 파일 핸들과 같은 값도 함수 리터럴 내의 내부상태로 가질 수 있습니다.

package main

import (
	"fmt"
	"os"
)

type Writer func(string)

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

func main() {
	file, err := os.Create("data.txt")
	if err != nil {
		fmt.Println("파일 생성 실패")
		return
	}
	defer file.Close()
	writeHello(func(msg string) {
		fmt.Fprintln(file, msg)
	})
}

이러한 성질을 잘 이용하면, writeHello 함수에서 다양한 로직을 실행할 수 있습니다. (의존성 주입)

728x90
반응형

'Go' 카테고리의 다른 글

[Go] 16. 에러 핸들링  (0) 2024.03.10
[Go] 15. 자료구조 - list, ring, map  (0) 2024.02.05
[Go] 13. 인터페이스  (0) 2024.01.21
[Go] 12. 메서드  (0) 2024.01.21
[Go] 10. 패키지  (0) 2024.01.20