업무에 사용하는 Go 언어 9 – 함수 func

Go 언어의 함수에 대해서 다루어보도록 하겠습니다.
프로그래밍 언어에서 함수는 필수적인 요소일 것 입니다.
함수형 프로그래밍이든 객체 지향 프로그래밍을 하든 어떤 패턴을 써도 함수는 필수적으로 사용됩니다.
함수를 잘 활용하면 코드를 재사용 할 수 있고 프로그램의 구조를 잡을 수 있습니다.

업무에 사용하는 Go 언어 썸네일

func

Go 언어에서도 함수는 0개 혹은 여러 인자를 받을 수 있습니다.
func 키워드를 사용하여 선언할 수 있고 함수 이름, 매개변수, 반환 타입, 내용을 정의하면 됩니다.
Go func의 기본형은 다음과 같습니다.

Go
func 함수이름(매개변수 목록) 반환타입 {
    // 함수 내용
}

통상 프로그래밍 언어의 함수를 학습할 때 덧셈을 수행하는 함수를 만들어보는 것 같습니다.
다음과 같이 만들어보고 실행해봅시다.

Go
package main

import "fmt"

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

func main() {
    result := add(3, 4)
    fmt.Println("3 + 4 =", result)
}

임의로 정수의 덧셈을 수행하는 함수를 만들었습니다.
다른 타입에 대한 덧셈을 수행하는 함수도 만들어보세요.

매개변수와 반환값

다른 언어와 상이한 점은 Go 언어에서는 반환타입이 가장 마지막에 온다는 것입니다.
이는 변수와 상수를 알아봤을 때와 같이 Go 언어만의 철학에 해당합니다.

함수의 매개변수와 반환값에 대한 타입은 필수적으로 명시되어야 합니다.
여러 매개변수가 있을 때 타입이 동일하다면 한 번만 타입을 명시해도 됩니다.
다음 예시처럼 코드를 작성할 수 있습니다.

Go
package main

import "fmt"

func multiply(x, y int) int {
    return x * y
}

func main() {
    fmt.Println("5 * 6 =", multiply(5, 6))
}

코드에서 x, y 매개변수 모두 int 타입이므로 마지막에 한 번만 타입을 명시하여 간결하게 작성할 수 있습니다.

약간의 여담

필자의 경우 처음으로 접하게 된 프로그래밍 언어가 C언어이기 때문인지,
매개변수의 타입이 동일하더라도 타입을 적는 편이다.
가독성도 타입을 적어주는 것이 더 좋다고 생각한다.

다중 반환값

Go 언어 함수는 여러 값을 반환할 수 있습니다.
함수가 다중 반환값을 가지는 경우 타입을 괄호로 묶어 여러 번 명시하면 되겠습니다.

Go
package main

import "fmt"

func divide(dividend, divisor int) (int, int) {
    quotient := dividend / divisor
    remainder := dividend % divisor
    return quotient, remainder
}

func main() {
    q, r := divide(9, 4)
    fmt.Println("몫:", q, "나머지:", r)
}

위 코드는 두 개의 반환값(몫, 나머지)를 반환합니다.

이름이 있는 반환값

매개변수에 이름을 지정하는 것처럼 반환값에도 이름을 지정할 수 있습니다.
반환값에 이름이 지정되어 있고 return 키워드에 값을 지정하지 않으면 이름이 지정된 반환값을 반환하게 됩니다.
이런 방식의 return을 “naked” return이라고 합니다.

Go
package main

import "fmt"

func rectangleArea(length, width int) (area int) {
    area = length * width
    return
}

func main() {
    fmt.Println("사각형 면적:", rectangleArea(5, 7))
}

위 코드는 미리 int 타입의 area라는 변수를 반환하겠다고 명시한 경우입니다.

필자의 개인적인 의견

Go
package main

import "fmt"

func rectangleArea(length, width int) (area int) {
	area = length * width
	another := 1
	return another
}

func main() {
	fmt.Println("사각형 면적:", rectangleArea(5, 7))
}

위 코드의 경우 어떠한 오류도 발생하지 않고 잘 실행된다.
분명 int 타입의 area 변수를 return한다고 정의된 함수인데 another라는 다른 변수를 return한다.
글을 적으면서도 가독성이 안 좋다.

개인적인 경험에 의하면 업무를 하면서 “naked” return, name return value 모두 사용해본 적이 없다.

개인적인 의견일 뿐 사용하면 안 된다는 의미는 아니니 참고만 하시기 바랍니다.

가변 인자

Go 언어 함수는 가변 인자를 받을 수 있도록 허용합니다.
가변 인자는 ...키워드(점 3개)로 매개변수가 가변 인자임을 명시합니다.
JS를 다루어보신 분들은 많이 익숙하실 것 같습니다.

Go
package main

import "fmt"

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

func main() {
    fmt.Println("합계:", sum(1, 2, 3, 4, 5))
}

위의 코드는 개수에 관계없이 여러 개의 정수를 받아 그의 합을 계산하는 함수를 작성한 것입니다.
가변 인자는 특정 타입의 slice로 처리됩니다. 위의 경우 nums는 []int slice가 되는 것입니다.
따라서, 함수 내부에서 slice와 관련된 연산을 수행할 수 있습니다.

함수 리터럴

Go 언어 함수는 익명 함수의 의미에서 리터럴로 선언이 가능합니다.
다른 함수 내에서 선언하거나 변수에 할당할 수 있습니다.
이 또한, JS를 다루어보신 본들은 익숙하실 겁니다.

Go
package main

import "fmt"

func main() {
	add := func(a int, b int) int {
		return a + b
	}
	fmt.Println("10 + 20 =", add(10, 20))
}

위 코드는 앞에서 작성한 덧셈을 수행하는 함수를 리터럴로 선언한 예시입니다.

함수 클로저 (Closure)

클로저는 함수 내에서 외부 변수에 접근하고, 그 변수를 참조하는 익명 함수를 생성하는 기능입니다.
이는 함수를 둘러싼 환경(context)을 캡처하여 외부 함수가 종료된 후에도 그 환경에 접근할 수 있게 합니다.

클로저의 원리와 특징

클로저는 함수가 선언될 당시의 환경을 기억하며, 함수가 호출될 때마다 그 환경에 접근하여 필요한 연산을 수행합니다.
특히, Go 언어에서 클로저는 함수 내에서 선언된 외부 변수를 기억합니다.
예를 들어, 어떤 함수 내부에서 변수 counter를 생성하고, 이를 참조하는 클로저 increment를 선언했다면, incrementcounter의 상태를 계속 유지하고 변경할 수 있습니다.

Go
package main

import "fmt"

// 카운터 클로저 생성 함수
func makeCounter() func() int {
    counter := 0
    return func() int {  // 익명 함수 리터럴을 반환
        counter++  // 외부 변수 counter를 기억하여 사용
        return counter
    }
}

func main() {
    increment := makeCounter()  // 클로저 생성
    fmt.Println(increment())  // 출력: 1
    fmt.Println(increment())  // 출력: 2
    fmt.Println(increment())  // 출력: 3

    newCounter := makeCounter()  // 새로운 클로저 생성
    fmt.Println(newCounter())  // 출력: 1
    fmt.Println(newCounter())  // 출력: 2
}

어떠한 상태를 유지한다고 이해하시면 편할 것 같습니다.

defer

Go 언어는 함수 실행이 끝난 후에 실행되었으면 하는 작업을 예약하는 기능을 제공한답니다.
예약된 호출(혹은 작업)은 컴파일 시에 적용은 되지만 둘러싼 가장 가까운 함수가 종료될 때까지 수행되지 않습니다.
다음 코드를 실행해보시면 이해가 빠르게 되실 것 같습니다.

Go
package main

import "fmt"

func main() {
	defer fmt.Println("world")

	fmt.Println("hello")
}

hello가 출력된 후 main 함수가 종료되면 world가 출력됩니다.
따라서 hello world를 출력하게 됩니다.

업무에서는 주로 파일을 처리한 후 닫거나 리소스 해제를 위해 사용되는 것 같습니다.

Go
package main

import (
    "fmt"
    "os"
)

func main() {
    file, err := os.Create("example.txt")
    if err != nil {
        fmt.Println("파일 생성 에러:", err)
        return
    }
    defer file.Close()

    fmt.Fprintln(file, "Hello, World!")
    fmt.Println("파일에 데이터를 작성했습니다.")
}

다음 글에서 알아볼 내용

Go 언어 포인터