프로그래밍 언어/Go

Go: 함수 (익명함수, 스코프, 고루틴, 지연호출)

함수

Go 가 다른 언어가 다른 점은 클래스가 없다는 점이다. 메서드라고 표현하는 것마저도 표현 방식이 다를 뿐 일반 함수와 아주 유사하게 표시된다. 메서드에 대해서는 추후 알아보기로 하고, 지금부터는 Go 의 일반적인 함수에 대해 알아본다.

 

함수로직이나 알고리즘을 묶은 코드의 집합이다. 함수형 프로그래밍에서는 기본 단위로 사용되기도 한다. 함수를 사용하는 가장 큰 의미는 중복을 제거하는 일이다. 이것이 가장 먼저이며 그 다음으로는 함수 내부는 블랙박스로 하여 구현에 상관없이 추상화를 하여 함수 단위로 프로그램을 구성할 수 있도록 만드는 것이다. 함수의 입력과 출력만 알고 있으면 구현과 관계 없이 프로그램을 구성할 수 있다. 여기서 함수는 프로그램을 구성하는 기본 단위로써 동작한다.

 

아래의 코드는 Go 함수의 많은 부분을 이야기한다. 하나 하나씩 파헤쳐보자. Go 함수의 구성요소는 func 키워드, 함수의 이름, 입력, 출력정도가 있다.

package main

import (
	"fmt"
	"log"
)

func sayHello(message string) (string, error) {
	return message, nil
}

func main() {
	helloGo, err := sayHello("Hello, Go!")
	if err != nil {
		log.Fatal(err)
	}
	// -> Hello, Go!
	fmt.Println(helloGo)
}

func 키워드를 통해 sayHello 라는 함수를 정의했다. 그리고 맨 처음에 봤던 코드와는 다른 것을 볼 수 있는데, 바로 괄호 사이에 파라매터가 있다는 것과 함수를 호출하면 값을 돌려주는 것이다. 함수는 기본적으로 어떠한 값을 입력하면 출력을 해주게 되어있다. 출력은 있는 경우도 있고 없는 경우도 있으나, 있는 경우가 더 많다.

 

sayHello 를 보자면, string 타입의 message 라는 파라매터를 입력으로 받으며, 출력으로는 string, error 타입에 해당하는 값을 돌려주게 되는 것이다. 여기서 Go 함수가 가지는 특이한 점이 드러나게 되는데, Go 는 리턴 값으로 여러 개의 값을 돌려줄 수 있다. 다른 언어처럼 배열로 해야한다거나 객체를 써서 해야한다거나 하는 것이 아니다.

 

또한 이것은 자료구조로 알아서 변하는 것이 아니라, 그냥 여러 개의 값을 반환하는 것 뿐이다. 다중 값 반환을 지원하는 타 언어에서는 암묵적으로 다중 값 반환에 대해 튜플로 변환하는 경우가 있는데, Go 에서는 그런식으로 처리하지 않는다.

nil 은 주로 에러가 없을 때 반환한다. nil 은 맵, 슬라이스에 대해 제로 값으로써 동작하기도 한다.

return

return 키워드는 함수에서 값을 반환할 때 사용하는데, return 을 사용하면 그 이후의 코드는 실행되지 않는다. 즉, return 을 사용하면 함수를 그 자리에서 멈추고 밖으로 내보낸다. 일반적으로 함수의 맨 마지막에서 사용한다. 조건문과 함께 사용하는 경우는 상당히 일반적이다.

package main

import (
	"errors"
	"fmt"
	"log"
)

func sayHello() (string, error) {
	if err := errors.New("Err"); err != nil {
		return "", err
	}
	return "Hello, Go!", nil
}

func main() {
	helloGo, err := sayHello()
	if err != nil {
		// -> Err
		log.Fatal(err)
	}
	fmt.Println(helloGo)
}

이름이 있는 리턴 값

리턴을 명시할 때 이름도 같이 명시해주는 것으로 함수 내부에서 변수를 별도로 선언하지 않고도 값을 반환해준다.

package main

import "fmt"

func sayHello() (helloGo string) {
	helloGo = "Hello, Go!"
	return
}

func main() {
	helloGo := sayHello()

	// -> Hello, Go!
	fmt.Println(helloGo)
}

리턴 값에 이름을 준다는 것은, 해당 리턴 값이 의미하는 것을 다른 개발자에게 알려줄 수 있는 도구가 되기도 한다.

package main

import "fmt"

func sayHello() (helloGo string) {
	return "Hello, Go!"
}

func main() { /* ... */ }

함수 스코프

함수는 또한 하나의 스코프로 동작할 수 있으며 함수 자신보다 상위의 스코프에서 선언한 변수에 대해서는 접근할 수 있지만, 함수내부에서 선언한 변수에 대해서 상위 스코프에서 접근할 수 없다. 따라서 아래의 코드에서 패키지 스코프에서 선언한 message 변수에는 접근이 가능하지만, 함수 스코프에서 선언한 helloGo 에 대해서는 접근할 수 없다.

package main

var (
	message string = "Hello, Go!"
)

func sayHello() {
	helloGo := message
}

func main() {
	sayHello()
	// fmt.Println(helloGo) // -> Error
}

익명 함수

Go 는 함수가 중요하게 여겨지고 있으므로, 익명 함수또한 존재한다. 함수를 파라매터로 넘길 수 있으며 값으로 취급할 수 있다. 그 말은 함수형 프로그래밍에서 일급 함수고차 함수로써의 역할도 할 수 있다는 이야기다. 함수를 값으로써 여길 수 있다면 일급 함수이며 함수를 리턴하거나 함수를 파라매터로 받거나 리턴한다면 그 함수는 고차 함수라고 이야기할 수 있다.

package main

import "fmt"

func each(arr []string, iterator func(string)) {
	for _, v := range arr {
		iterator(v)
	}
}

func main() {
	p := func(v string) {
		fmt.Println(v)
	}

	// each([]string{"Hello, Go!", "Who are you?"}, func(v string) {
	// 	fmt.Println(v)
	// })
	each([]string{"Hello, Go!", "Who are you?"}, p)
}

Go 의 함수는일급(First-Class) 함수다. 일급 함수는 함수형 프로그래밍에서 쓰이는 개념으로써, 함수를 다른 함수의 파라매터로 넘기거나 변수에 담고, 리턴으로 사용하는 등, 함수 자체를 값 처리할 수 있는 것을 의미한다.

defer

defer 키워드는 Go 에서 지원하는 특이한 키워드 중 하나다. 내가 PHP 언어를 할때 불편하게 여겼던 것중 하나는, 파일을 열고, 닫는 함수를 호출해주어야 하는데 로직상 그렇게 하기 까다로운 경우가 있다는 것이었다. 데이터베이스 커넥션을 열고 닫을 때도 마찬가지였다. 그런데 Go 에서는 이러한 문제점을 확실하게 해결해줄 수 있는데, 그것이 바로 defer 키워드다. 이 녀석은 동시성 키워드인 go 와 함께 같이 쓰이는 경우가 많기 때문에 반드시 알아둘 필요가 있다.

package main

import (
	"fmt"
	"log"
	"os"
)

func main() {
	file, err := os.Open("./Go.txt")
	if err != nil {
		log.Fatal(err)
	}
	defer file.Close()

	// -> ./Go.txt
	fmt.Println(file.Name())
}

defer 를 사용하면 함수가 종료될 때 호출된다. 런타임 중에 에러(패닉)가 발생한다고 해도 defer 키워드를 사용한 함수는 호출이 보장된다. 위의 코드를 보면 defer 를 통해 file.Close() 를 먼저 예약을 해두고, 파일에 대한 작업을 하는 것을 볼 수 있다. 파일을 닫거나, 커넥션을 종료하는 등 열고 닫는 것이 세트로 존재해야 하며, 리소스를 풀어주지 않으면 메모리를 점유하여 시스템에 문제를 일으키는 경우에 대해 defer 를 사용하면 손쉽게 제어가 가능하다. 참고로 defer 를 여러개 사용하는 것도 가능한데, 이는 스택으로 쌓이기 때문에 가장 나중에 설정된 defer 부터 실행된다.

go & go-routine

go 키워드는 동시성을 쓸 때 사용한다. Go 언어의 동시성은 기본적으로 OS 스레드를 한 층 더 추상화한 코루틴(co-routine)이며 비동기적 프로그래밍을 할 수 있다. Go 에서는 이를 고루틴(go-routine)이라고 한다. 멀티 스레딩을 활용할 때 유용한데, 스레드에 자원을 배분하거나 스레드 자체에 대해 간섭하는 것은 개발자가 할 필요없고, 고 런타임이 알아서 해주게 된다.

package main

import (
	"fmt"
	"sync"
	"time"
)

var wg sync.WaitGroup

func main() {
	wg.Add(1)
	go func() {
		defer wg.Done()
		for i := 0; i < 10; i++ {
			fmt.Println(i)
			time.Sleep(time.Second)
		}
	}()

	wg.Add(1)
	go func() {
		defer wg.Done()
		for _, byte := range "Hello, Go!" {
			fmt.Printf("%c\n", byte)
			time.Sleep(time.Second)
		}
	}()

	wg.Wait()
	fmt.Println("Goodbye")
}

아래의 2개의 고루틴은 숫자와 문자를 번갈아서 , 혹은 임의의 순서를 가지고 출력될 것이다. 여기서 sync.WaitGroup 을 사용한 것을 볼 수 있는데, main() 함수 또한 고루틴이며 만약 다른 고루틴보다 main() 고루틴이 먼저 끝날경우 프로그램자체가 종료되기 때문에 이를 사용한 것이라고 보면 된다. 고루틴과 동시성에 대한 내용은 별도로 주제를 나눠서 이야기해야 하므로 함수에서는 일단 동시성을 위해 go 키워드를 사용할 수 있다는 것만 알아두자.

Go 함수가 지원하지 않는 것들

Go 언어의 함수가 다른 언어에 비해 지원하지 않는 것들을 알아둘 필요가 있다. Go 언어만 하는 것이 아니라면 Go 를 학습하면서 의아한 점이 몇몇 생기기 때문이다. Go 에서 함수 파트에 대해 지원하지 않는 것은 다음과 같다.

 

  • 선택적 파라매터
  • 파라매터에 대한 기본 값(default value)
  • 메서드 오버로딩

선택적 파라매터는 파라매터가 있을 수도 있고 없을 수도 있는 것이며, 기본 값은 함수 호출시 해당 파라매터에 대한 값을 넘기지 않았을 경우 적용할 값이다. 구글에서는 이러한 것을 언어 차원에서 배제시켜버렸다. 타언어에서 함수에 대해 저러한 것들은 함수의 가능성과 다양성을 발휘할 수 있도록 해주었지만, Go 언어는 단순함과 일관성을 추구하므로 함수의 복잡성이 증가하는 것을 일찌감치 배제하기 위해 저런 선택을 한 것으로 생각된다.