함수 선언하기
함수 선언은 func, 이름, 매개변수, 결과 타입, 본문으로 이루어집니다. 타입은 매개변수 이름 앞이 아니라 뒤에 옵니다.
add는 int 값 두 개를 받아 int를 반환합니다. greet는 아무것도 반환하지 않으므로 시그니처에 결과 타입이 없고 본문에 return도 필요 없습니다.
결과를 선언한 함수는 종료 문장으로 끝나야 합니다. 반환하지 않고 닫는 중괄호에 도달하는 경로가 있으면 컴파일러가 missing return으로 멈춥니다.
타입을 공유하는 매개변수
같은 타입의 매개변수가 연달아 오면 타입 이름 하나를 공유할 수 있습니다. func add(a, b int) int는 func add(a int, b int) int와 정확히 같습니다. 섞어 쓰는 것도 가능합니다:
func scale(x, y float64, factor int) (float64, float64)
여기서 x와 y는 float64이고 factor는 int입니다.
반환 값
함수는 값을 0개, 1개, 또는 여러 개 반환할 수 있습니다. 결과가 여럿이면 괄호에 넣고, 호출한 쪽은 다중 대입으로 받습니다:
이 코드는 3 2를 출력합니다. (value, error) 쌍은 Go에서 이 기능이 가장 흔하게 쓰이는 형태이며, 다중 반환 값 페이지에서 따로 다룹니다.
인자는 복사된다
Go는 모든 인자를 값으로 넘깁니다. 함수는 복사본으로 작업하므로 매개변수에 대입해도 호출한 쪽 변수는 절대 바뀌지 않습니다.
출력은 다음과 같습니다:
{1 2}
{100 2}
[99 2 3]
세 번째 줄에서 많은 사람이 놀랍니다. 슬라이스 값은 작은 헤더(포인터, 길이, 용량)입니다. 복사본도 같은 기반 배열을 가리키므로 s[0]에 쓰면 호출한 쪽 요소에 쓰게 됩니다. 하지만 함수 안의 append는 새 배열을 할당할 수 있고, 호출한 쪽은 그 새 헤더를 보지 못합니다. 함수가 슬라이스를 늘린다면 새 슬라이스를 반환하세요. 맵에도 같은 논리가 적용됩니다. 맵 값은 공유 데이터를 참조하므로 함수가 추가한 키는 호출한 쪽에서도 보입니다.
함수가 호출한 쪽 변수를 수정해야 하거나, 호출할 때마다 복사하고 싶지 않은 큰 구조체라면 포인터 매개변수를 쓰세요.
함수는 값이다
함수에는 이름 없이 쓰는 타입이 있습니다: func(int, int) int. 함수를 변수에 저장하고, 맵이나 슬라이스에 넣고, 다른 함수에 넘길 수 있습니다.
square는 변수에 대입된 익명 함수입니다. 익명 함수는 주변 스코프의 변수를 읽고 바꿀 수도 있으며, 그렇게 되면 클로저가 됩니다.
같은 형태가 여러 곳에 나온다면 이름 있는 함수 타입을 쓰면 시그니처를 읽기 쉬워집니다:
type Transform func(int) int
func apply(xs []int, f Transform) []int
함수 타입의 제로 값은 nil입니다. nil 함수를 호출하면 invalid memory address or nil pointer dereference로 패닉이 나므로, 선택적인 콜백은 호출하기 전에 확인하세요. 함수 값은 nil과만 비교할 수 있고, 서로 비교할 수는 없습니다.
재귀
함수는 자기 자신을 호출할 수 있습니다. Go에는 꼬리 호출 최적화가 없지만 고루틴 스택이 필요에 따라 늘어나므로 일반적인 재귀 깊이는 문제없습니다.
for i := range 6은 0부터 5까지 세며, Go 1.22 이상이 필요합니다.
Go 함수에 없는 것
- 오버로딩이 없습니다. 한 패키지의 두 함수는 매개변수가 달라도 이름을 공유할 수 없습니다.
- 기본 인자나 이름 붙은 인자가 없습니다. 모든 매개변수를 호출할 때마다 순서대로 넘깁니다. 설정 구조체를 쓰면 이름이 있는 선택적 필드를 얻을 수 있습니다:
NewServer(Config{Port: 8080}). - 이름 있는 중첩 함수가 없습니다. 함수 본문 안에서는 익명 함수만 선언할 수 있고, 보통 변수에 대입합니다.
길이가 변하는 인자 목록은 가변 인자 함수로 지원됩니다.
공개 함수와 비공개 함수
이름이 대문자로 시작하는 함수(Parse)는 공개(exported)되어 다른 패키지에서 호출할 수 있습니다. 소문자 이름(parse)은 자기 패키지 안에서만 보입니다. public이나 private 키워드는 없고, 대소문자가 규칙의 전부입니다.
흔한 실수
- 매개변수를 바꾼 것이 호출한 쪽에 반영되길 기대함. 매개변수에 대입하면 복사본만 바뀝니다. 새 값을 반환하거나 포인터를 받으세요.
- 함수 안에서 append하고 결과를 무시함.
func add(s []int) { s = append(s, 1) }는 호출한 쪽 슬라이스의 길이에 아무 영향도 없습니다.func add(s []int) []int로 쓰고s = add(s)로 사용하세요. - 사용하지 않는 결과. Go는 반환 값을 무시하는 것을 허용합니다.
go vet은 버려진 오류를 지적하지 않지만,errcheck(golangci-lint에 포함) 같은 린터는 지적합니다. 무시하는 것이 의도된 결정일 때만_ =를 쓰세요.
자주 묻는 질문
Go에서 함수는 어떻게 정의하나요?
func 키워드, 이름, 이름 뒤에 타입을 적은 매개변수 목록, 결과 타입을 씁니다: func add(a, b int) int { return a + b }. 결과가 없는 함수는 타입을 생략하고, 결과가 여럿인 함수는 괄호 안에 나열합니다: func divmod(a, b int) (int, int).
Go는 인자를 값으로 넘기나요, 참조로 넘기나요?
항상 값으로 넘깁니다. 함수는 각 인자의 복사본을 받습니다. int나 구조체라면 그 복사본은 호출한 쪽 변수와 독립적입니다. 슬라이스, 맵, 채널, 포인터는 공유 데이터를 가리키는 작은 값이라서, 그것이 가리키는 요소를 바꾸면 호출한 쪽에서도 보이지만 매개변수 자체에 다시 대입한 것은 보이지 않습니다. 함수가 호출한 쪽 변수를 바꿔야 한다면 포인터(*T)를 넘기세요.
Go에서 함수를 매개변수로 넘길 수 있나요?
네. 함수는 func(int) int 같은 타입을 가진 값입니다. 그 타입의 매개변수를 선언하고, 시그니처가 맞는 함수라면 이름이 있든 익명이든 넘길 수 있습니다: func apply(xs []int, f func(int) int).
Go는 함수 오버로딩이나 기본 매개변수를 지원하나요?
지원하지 않습니다. 패키지 안의 함수 이름은 모두 달라야 하고, 모든 매개변수는 호출할 때마다 넘겨야 합니다. 보통은 이름을 구분하거나(NewServer, NewServerWithTLS), 설정 구조체, 가변 매개변수, 함수형 옵션으로 대신합니다.