프로그래밍 언어

[Things of Go] Go언어에서의 함수 객체 활용 방안 - 기본편

Go언어에서는 함수는 1급 객체이다. 즉, 함수 객체는 숫자나 문자열 같은 변수처럼 쓸 수 있다. 다만 이러한 성질이 있다는 것은 알아도 활용하는 것은 다른 문제이다. 커뮤니티에서도 Go언어로 함수형 프로그래밍을 하는 것을 적극적으로 추천하지는 않는다. 하지만 함수 객체는 특정한 상황에서는 뛰어난 해결책이 될 수 있다. 해당 글에서는 함수 객체를 실제로 이용하기에 앞서 Go언어에서의 함수 객체의 정의와 기본적인 문법에 대해 설명하도록 하겠다.

함수의 기본

Go언어의 함수는 다음과 같은 형태이다.

func 함수명(매개변수) 반환값 {
    구현부
}

함수를 변수처럼 쓰기 위해서는 그에 맞는 자료형으로 선언해야 한다. 그렇다면 함수의 자료형은 어떤 모습일까?

함수의 형태

모든 함수가 같은 자료형은 아니다. 함수는 고유한 형태를 가지고 있다. 그리고 함수의 형태를 결정짓는 것은 매개변수와 반환값이다.

함수의 자료형은 다음과 같이 표현된다.

func(매개변수)자료형

위에서 보듯이 Go언어의 함수의 형태는 매개변수와 반환값으로 결정된다. 즉, 만약 서로 다른 두 함수의 매개변수와 반환값의 개수와 자료형이 같으면 두 함수는 같은 형태라고 할 수 있다. 아래의 예제를 보면 이해하기 쉽다.

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

func sub(a int, b int) int {
    return a-b
}

func main() {

    var f func(int, int) int

    a, b := 5, 4

    f = add
    fmt.Println(f(a,b))
    f = sub
    fmt.Println(f(a,b))

}

 

9
1

Process finished with exit code 0

add와 sub는 비록 함수명과 구현부가 완전히 달라도 매개변수와 반환값의 자료형이 같기 때문에 func(int, int) int의 같은 자료형이라 볼 수 있다. main에서는 func(int, int) int 형 변수 f를 만들고 f에 add와 sub를 차례대로 할당하고 실행시켰다.

함수 객체

함수는 함수 내에서 정의될 수도 있다. 다만 이 때의 함수는 일반적으로 함수를 선언할 때의 모습과 조금 다르다.

func SomeFunc() {

    printNum := func(i int) {
        fmt.Printf("It is %d\n", i)
    }
    i := 3

    printNum(i)

}

 

It is 3

Process finished with exit code 0

평소에 함수를 정의할 때와 비교했을 때 함수 선언문의 우변에 있는 함수의 함수명이 생략된 것을 알 수 있다. 해당 함수는 함수명 대신 선언문의 좌변인 printNum으로 호출이 된다. printNum이 함수명이라고 볼 수도 있다.

익명 함수

익명 함수는 뜻 그대로 이름이 없는 함수이다. 즉, 함수명이 생략된 함수이다. 위 예제의 함수 선언문의 우변이 바로 이 익명 함수이다. 익명 함수는 이외에도 여러 함수들을 묶어주는 역할을 한다. 아마 동시성 프로그래밍을 하다보면 많이 이용하게 될 것이다.

func main() {

    var wg sync.WaitGroup
    ch := make(chan int, 10)

    for i := 0; i < 10; i ++ {
        ch <- i
        wg.Add(1)
        go func() {
            fmt.Printf("Job %d finished.\n", <-ch)
            wg.Done()
        }()
    }
    wg.Wait()

}

 

Job 0 finished.
Job 3 finished.
Job 1 finished.
Job 4 finished.
Job 8 finished.
Job 2 finished.
Job 6 finished.
Job 9 finished.
Job 7 finished.
Job 5 finished.

Process finished with exit code 0

해당 프로그램은 동시성 프로그래밍을 이용했기 때문에 실행 결과의 각 줄은 시도할 때마다 순서가 바뀔 수 있다.

위처럼 익명함수를 선언한 후에 바로 호출 할 때 함수 몸체 뒤에 (매개 변수)를 붙이는 것을 잊으면 안된다.(위의 익명함수는 따로 매개변수를 받지 않기 때문에 ()이다.) 익명 함수 뒤에 매개변수를 붙이지 않으면 함수 객체를 뜻하고 매개변수를 붙이면 해당 함수 객체의 호출을 뜻한다. 우리가 함수를 호출 할 때를 떠올려 보면 이해하기 쉽다.

클로저

클로저는 자신을 포함한 함수의 변수들을 참조하는 함수 객체이다. 익명함수와 혼동되기도 하지만 익명함수와 클로저는 포함 관계에 있다. 쉽게 말해서 클로저는 외부 변수를 참조하는 익명 함수이다. 예시를 보면 이해하기 쉽다.

type Product struct {
	Identifier int
	Name       string
	User       string
}

func (p Product) Info() string {
	return fmt.Sprintf("%s (ID:%d), owned by %s", p.Name, p.Identifier, p.User)
}

func Factory(name string) func(string) Product {

	p := Product{}
	p.Name = name
	i := 0
    
	return func(user string) Product {
		p.Identifier = i
		p.User = user
		i ++
		return p
	}
    
}

func main() {

	airpods := Factory("air-pods")
	buzz := Factory("galaxy-buzz")

	p := airpods("Park")
	fmt.Println(p.Info())

	l := airpods("Lee")
	fmt.Println(l.Info())

	k := buzz("Kim")
	fmt.Println(k.Info())

	j := airpods("Jeong")
	fmt.Println(j.Info())

}

 

air-pods (ID:0), owned by Park
air-pods (ID:1), owned by Lee
galaxy-buzz (ID:0), owned by Kim
air-pods (ID:2), owned by Jeong

Process finished with exit code 0

Factory 함수는 반환값으로 함수 객체를 갖는다. 이 함수 객체를 살펴보면 Factory 함수의 변수들을 이용한다는 것을 알 수 있다. 즉, 이 반환된 함수 객체를 클로저라고 부른다. Factory 함수를 객체화시켜 airpods에 넣으면 그 순간 Factory함수가 따로 저장이 된다. 클로저는 Factory의 i를 증가시키는데, 해당 클로저의 외부 함수인 Factory 객체는 i를 저장한다. 이후 해당 객체가 다시 호출되면 저장된 i를 호출한다.

하나 더 주목할 것이 있다면 airpods와 같이 선언된 buzz일 것이다. airpods와 buzz는 따로 선언되었기 때문에  i를 공유하지 않는다. airpods와 buzz가 참조하는 Factory는 서로 다른 함수 객체라는 뜻이다.

마치며...

지금까지 golang에서의 함수 객체의 기본적인 문법에 대해 알아보았다. 사실 이 글의 내용은 이미 알고 있지만 어떻게 사용해야 할지 모르는 독자들도 많을 것이다. 다음 글에는 함수 객체를 이용하는 방법에 대해 설명하도록 하겠다.