Go 함수
Go 에서 함수는 일등 시민이며, 함수는 Go 의 가장 기초적인 구성 요소이자 핵심입니다.
선언
함수 선언 형식은 다음과 같습니다.
func 함수명 ([매개변수 목록]) [반환값] {
함수체
}함수를 선언하는 방법은 두 가지가 있습니다. 하나는 func 키워드로 직접 선언하는 것이고, 다른 하나는 var 키워드로 선언하는 것입니다. 다음과 같습니다.
func sum(a int, b int) int {
return a + b
}
var sum = func(a int, b int) int {
return a + b
}함수 시그니처는 함수 이름, 매개변수 목록, 반환값으로 구성됩니다. 아래는 전체 예시로, 함수 이름은 Sum 이며, 두 개의 int 타입 매개변수 a, b 가 있고, 반환값 타입은 int 입니다.
func Sum(a int, b int) int {
return a + b
}매우 중요한 점은 Go 에서 함수는 오버로드를 지원하지 않는다는 것입니다. 아래 코드는 컴파일을 통과할 수 없습니다.
type Person struct {
Name string
Age int
Address string
Salary float64
}
func NewPerson(name string, age int, address string, salary float64) *Person {
return &Person{Name: name, Age: age, Address: address, Salary: salary}
}
func NewPerson(name string) *Person {
return &Person{Name: name}
}Go 의 철학은 시그니처가 다르면 완전히 다른 함수이므로 같은 이름을 사용하면 안 된다는 것입니다. 함수 오버로드는 코드를 혼란스럽고 이해하기 어렵게 만듭니다. 이러한 철학이 옳은지는 사람마다 다르지만, 적어도 Go 에서는 함수 이름만으로 그 기능을 알 수 있으며 어떤 오버로드인지 찾을 필요가 없습니다.
매개변수
Go 에서 매개변수 이름은 필수가 아니며, 일반적으로 인터페이스나 함수 타입 선언 시에만 사용됩니다. 하지만 가독성을 위해 매개변수 이름을 지정하는 것이 좋습니다.
type ExWriter func(io.Writer) error
type Writer interface {
ExWrite([]byte) (int, error)
}동일한 타입의 매개변수는 한 번만 타입을 선언할 수 있으며, 조건은 인접해 있어야 합니다.
func Log(format string, a1, a2 any) {
...
}가변 길이는 0 개 이상의 값을 받을 수 있으며, 매개변수 목록의 끝에 선언해야 합니다. 가장 대표적인 예가 fmt.Printf 함수입니다.
func Printf(format string, a ...any) (n int, err error) {
return Fprintf(os.Stdout, format, a...)
}值得一提的是,Go 에서 함수 매개변수는 값 전달입니다. 즉, 매개변수를 전달할 때 실参의 값을 복사합니다. 슬라이스나 맵을 전달할 때 많은 메모리가 복사될 것이라고 걱정한다면, 이 두 데이터 구조는 본질적으로 포인터이므로 걱정할 필요가 없다고 말씀드릴 수 있습니다.
반환값
아래는 간단한 함수 반환값 예시입니다. Sum 함수는 int 타입 값을 반환합니다.
func Sum(a, b int) int {
return a + b
}함수가 반환값이 없을 때는 void 가 필요 없으며, 반환값이 없으면 됩니다.
func ErrPrintf(format string, a ...any) {
_, _ = fmt.Fprintf(os.Stderr, format, a...)
}Go 는 함수가 여러 반환값을 가질 수 있으며, 이때는 괄호로 반환값을 묶어야 합니다.
func Div(a, b float64) (float64, error) {
if a == 0 {
return math.NaN(), errors.New("0 은 나눗셈의 제수가 될 수 없습니다")
}
return a / b, nil
}Go 는 이름 있는 반환값도 지원하며, 매개변수 이름과 중복될 수 없습니다. 이름 있는 반환값을 사용할 때는 return 키워드에 반환값을 지정하지 않아도 됩니다.
func Sum(a, b int) (ans int) {
ans = a + b
return
}매개변수와 마찬가지로, 동일한 타입의 이름 있는 반환값이 여러 개일 때는 반복되는 타입 선언을 생략할 수 있습니다.
func SumAndMul(a, b int) (c, d int) {
c = a + b
d = a * b
return
}이름 있는 반환값이 어떻게 선언되든, 항상 return 키워드 뒤의 값이 최우선입니다.
func SumAndMul(a, b int) (c, d int) {
c = a + b
d = a * b
// c, d 는 반환되지 않습니다.
return a + b, a * b
}익명 함수
익명 함수는 시그니처가 없는 함수입니다. 아래 함수 func(a, b int) int 는 이름이 없으므로 함수체 바로 뒤에 괄호를 붙여 호출해야 합니다.
func main() {
func(a, b int) int {
return a + b
}(1, 2)
}함수를 호출할 때 매개변수가 함수 타입인 경우 이름이 중요하지 않으므로 익명 함수를 직접 전달할 수 있습니다. 다음과 같습니다.
type Person struct {
Name string
Age int
Salary float64
}
func main() {
people := []Person{
{Name: "Alice", Age: 25, Salary: 5000.0},
{Name: "Bob", Age: 30, Salary: 6000.0},
{Name: "Charlie", Age: 28, Salary: 5500.0},
}
slices.SortFunc(people, func(p1 Person, p2 Person) int {
if p1.Name > p2.Name {
return 1
} else if p1.Name < p2.Name {
return -1
}
return 0
})
}이는 사용자 정의 정렬 규칙의 예시입니다. slices.SortFunc 는 두 개의 매개변수를 받습니다. 하나는 슬라이스이고 다른 하나는 비교 함수입니다. 재사용을 고려하지 않는다면 익명 함수를 직접 전달할 수 있습니다.
클로저
클로저 (Closure) 는 일부 언어에서 Lambda 표현식이라고도 하며, 익명 함수와 함께 사용됩니다. 클로저 = 함수 + 환경 참조입니다. 아래 예시를 보겠습니다.
func main() {
grow := Exp(2)
for i := range 10 {
fmt.Printf("2^%d=%d\n", i, grow())
}
}
func Exp(n int) func() int {
e := 1
return func() int {
temp := e
e *= n
return temp
}
}출력
2^0=1
2^1=2
2^2=4
2^3=8
2^4=16
2^5=32
2^6=64
2^7=128
2^8=256
2^9=512Exp 함수의 반환값은 함수이며, 이를 grow 함수라고 합니다. 이를 한 번 호출할 때마다 변수 e 는 지수적으로 증가합니다. grow 함수는 Exp 함수의 두 변수 e 와 n 을 참조합니다. 이들은 Exp 함수의 스코프 내에서 생성되며, 정상적인 경우 Exp 함수 호출이 끝나면 스택에서 빠져나가며 메모리가 회수됩니다. 하지만 grow 함수가 이들을 참조하므로 회수되지 않고 힙으로 도피합니다. Exp 함수의 수명이 끝났더라도 변수 e 와 n 의 수명은 끝나지 않았으며, grow 함수 내에서 이 두 변수를 직접 수정할 수 있습니다. grow 함수가 바로 클로저 함수입니다.
클로저를 사용하면 피보나치 수열을 구하는 함수를 매우 간단하게 구현할 수 있습니다. 코드는 다음과 같습니다.
func main() {
// 10 개의 피보나치 수
fib := Fib(10)
for n, next := fib(); next; n, next = fib() {
fmt.Println(n)
}
}
func Fib(n int) func() (int, bool) {
a, b, c := 1, 1, 2
i := 0
return func() (int, bool) {
if i >= n {
return 0, false
} else if i < 2 {
f := i
i++
return f, true
}
a, b = b, c
c = a + b
i++
return a, true
}
}출력은 다음과 같습니다.
0
1
1
2
3
5
8
13
21
34지연 호출
defer 키워드는 함수를 일정 시간 지연하여 호출하게 하며, 함수 반환 전에 이러한 defer 가 설명하는 함수들이 하나씩 실행됩니다. 아래 예시를 보겠습니다.
func main() {
Do()
}
func Do() {
defer func() {
fmt.Println("1")
}()
fmt.Println("2")
}출력
2
1defer 는 함수 반환 전에 실행되므로 defer 에서 함수의 반환값을 수정할 수도 있습니다.
func main() {
fmt.Println(sum(3, 5))
}
func sum(a, b int) (s int) {
defer func() {
s -= 10
}()
s = a + b
return
}여러 defer 가 설명하는 함수가 있을 때는 스택과 같이 선입후출 순서로 실행됩니다.
func main() {
fmt.Println(0)
Do()
}
func Do() {
defer fmt.Println(1)
fmt.Println(2)
defer fmt.Println(3)
defer fmt.Println(4)
fmt.Println(5)
}0
2
5
4
3
1지연 호출은 일반적으로 파일 리소스 해제, 네트워크 연결 종료 등에 사용되며, 또 다른 용도는 panic 을 캡처하는 것이지만 이는 오류 처리 섹션에서 다룰 내용입니다.
루프
명시적으로 금지되지는 않았지만, 일반적으로 for 루프에서 defer 를 사용하는 것은 권장되지 않습니다. 다음과 같습니다.
func main() {
n := 5
for i := range n {
defer fmt.Println(i)
}
}출력은 다음과 같습니다.
4
3
2
1
0이 코드 결과는 올바르지만 과정이 올바르지 않을 수 있습니다. Go 에서 defer 를 하나 생성할 때마다 현재 고루틴에 메모리 공간을 할당해야 합니다. 위의 예시가 간단한 for n 루프가 아니라 복잡한 데이터 처리 플로우이고, 외부 요청 수가 갑자기 급증하면 짧은 시간에 대량의 defer 가 생성됩니다. 루프 횟수가 많거나 불확실할 경우 메모리 사용량이 갑자기 급증할 수 있으며, 이를 일반적으로 메모리 누수라고 합니다.
매개변수 사전 계산
지연 호출에는 다소 반직관적인 세부사항이 있습니다. 아래 예시를 보겠습니다.
func main() {
defer fmt.Println(Fn1())
fmt.Println("3")
}
func Fn1() int {
fmt.Println("2")
return 1
}이 함정은 매우 은밀합니다. 笔者도 이전에 이 함정 때문에半天 동안 원인을 찾지 못한 적이 있습니다. 출력이 무엇일지 추측해보세요. 정답은 다음과 같습니다.
2
3
1많은 사람들이 다음과 같은 출력일 것이라고 생각할 수 있습니다.
3
2
1사용자의 의도대로라면 fmt.Println(Fn1()) 부분은 함수체 실행이 끝난 후 실행되기를 원했을 것입니다. fmt.Println 은 실제로 마지막에 실행되지만, Fn1() 은 예상 밖이었습니다. 아래 예시가 더 명확합니다.
func main() {
var a, b int
a = 1
b = 2
defer fmt.Println(sum(a, b))
a = 3
b = 4
}
func sum(a, b int) int {
return a + b
}출력은 반드시 3 이지 7 이 아닙니다. 지연 호출 대신 클로저를 사용하면 결과가 또 다릅니다.
func main() {
var a, b int
a = 1
b = 2
f := func() {
fmt.Println(sum(a, b))
}
a = 3
b = 4
f()
}클로저의 출력은 7 입니다. 그렇다면 지연 호출과 클로저를 결합하면 어떻게 될까요?
func main() {
var a, b int
a = 1
b = 2
defer func() {
fmt.Println(sum(a, b))
}()
a = 3
b = 4
}이번에는 정상적으로 7 이 출력됩니다. 아래를 수정하여 클로저를 없애면
func main() {
var a, b int
a = 1
b = 2
defer func(num int) {
fmt.Println(num)
}(sum(a, b))
a = 3
b = 4
}출력이 다시 3 으로 돌아갑니다. 위의 몇 가지 예시를 비교하면 이 코드
defer fmt.Println(sum(a,b))는 실제로
defer fmt.Println(3)와 동일함을 알 수 있습니다.
Go 는 sum 함수를 마지막에 호출하지 않으며, sum 함수는 지연 호출이 실행되기 전에 이미 호출되어 fmt.Println 의 매개변수로 전달됩니다. 요약하면, defer 가 직접 작용하는 함수의 매개변수는 사전 계산된다는 것입니다. 이로 인해 첫 번째 예시에서 이상한 현상이 발생했습니다. 특히 지연 호출에서 함수 반환값을 매개변수로 사용하는 경우 주의해야 합니다.
