9장. 함수
지금까지는 모든 코드를 main 안에 적었다.
프로그램이 조금만 커져도 이 방식은 빠르게 무너진다.
같은 일을 여러 번 적어야 하고,
한 함수가 너무 많은 일을 떠안게 된다.
해결책은 단순하다. 같은 일을 묶어 이름을 붙이는 것. 이것이 함수 다.
이 장의 목표:
- 함수를 정의하고 호출할 수 있다
- 매개변수와 반환값을 다양한 형태로 다룰 수 있다
- 다중 반환값과 명명된 반환값을 안다
- 가변 인자를 사용할 수 있다
defer로 정리 작업을 안전하게 처리한다- 익명 함수와 클로저를 이해한다
9.1 함수 정의와 호출
가장 기본적인 모양은 이렇다.
func 이름(매개변수) 반환타입 {
// 본문
}
작은 예제부터 본다.
package main
import "fmt"
func greet() {
fmt.Println("Hello!")
}
func main() {
greet()
greet()
}
실행 결과:
Hello!
Hello!
func는 함수를 시작하는 키워드- 함수 이름은 변수 이름 규칙과 같다
()안에 매개변수를 적는다 (없으면 비워 둔다)- 매개변수 뒤에 반환 타입을 적는다 (없으면 생략)
- 중괄호 위치는 3장에서 본 규칙 그대로
매개변수가 없는 함수
위 greet 처럼 괄호 안을 비워 두면 된다.
반환값이 없는 함수
반환 타입을 적지 않으면 아무것도 반환하지 않는다.
이런 함수도 안에서 return 을 써서 일찍 빠져나올 수 있다.
func warn(msg string) {
if msg == "" {
return
}
fmt.Println("경고:", msg)
}
9.2 매개변수와 반환값
값을 받고 값을 돌려주는 함수가 가장 흔하다.
func square(n int) int {
return n * n
}
func main() {
fmt.Println(square(4)) // 16
}
n int는 “이름이 n 이고 타입은 int 인 매개변수”- 마지막
int는 반환 타입 - 함수 본문에서
return으로 값을 돌려준다
같은 타입 매개변수 묶기
매개변수가 여러 개고 타입이 같다면 마지막 한 번만 타입을 적어도 된다.
// 매번 적기
func add(a int, b int) int {
return a + b
}
// 한 번만 적기 (같은 결과)
func add(a, b int) int {
return a + b
}
세 개 이상도 마찬가지다.
func clamp(x, lo, hi int) int {
if x < lo {
return lo
}
if x > hi {
return hi
}
return x
}
타입이 섞여 있다면 그룹별로 묶는다.
func repeat(s string, n int) string {
// ...
}
func mix(a, b int, c, d string) {
// ...
}
9.3 다중 반환값
Go 함수는 값을 여러 개 동시에 돌려줄 수 있다. 다른 많은 언어와 구별되는 특징이다.
func swap(a, b int) (int, int) {
return b, a
}
func main() {
x, y := swap(1, 2)
fmt.Println(x, y) // 2 1
}
반환 타입을 괄호로 묶고, return 도 콤마로 여러 값을 돌려준다.
호출 쪽은 받는 변수 개수를 정확히 맞춰야 한다.
결과와 에러를 함께 돌려주기
다중 반환의 가장 흔한 쓰임은 “결과 + 성공 여부” 또는 “결과 + 에러” 다.
func divide(a, b int) (int, bool) {
if b == 0 {
return 0, false
}
return a / b, true
}
func main() {
q, ok := divide(10, 3)
if ok {
fmt.Println("몫:", q)
} else {
fmt.Println("0 으로 나눌 수 없음")
}
}
Go 표준 라이브러리는 이 패턴을 폭넓게 쓴다.
정식 에러 처리는 error 인터페이스로 한다.
이 부분은 21장에서 자세히 다룬다.
blank identifier _
여러 값 중 일부만 필요하다면
나머지를 _ (언더스코어) 로 받아 버린다.
_, ok := divide(10, 0)
if !ok {
fmt.Println("실패")
}
_ 는 “이 값을 안 쓰겠다” 는 명시다.
Go 는 선언만 하고 안 쓰는 변수를 컴파일 에러로 처리한다.
_ 는 그 규칙을 우회하는 공식 도구다.
9.4 명명된 반환값 (named return)
반환 타입에 이름을 미리 붙일 수도 있다.
func divmod(a, b int) (q, r int) {
q = a / b
r = a % b
return
}
- 반환 타입 자리에
q, r int처럼 이름과 타입을 같이 적었다 - 함수가 시작될 때
q,r은 제로값 (0) 으로 자동 선언된다 - 마지막의
return은 값을 적지 않아도q,r의 현재 값을 돌려준다. 이걸 naked return 이라 부른다
호출 쪽은 일반 다중 반환과 똑같이 받는다.
q, r := divmod(17, 5)
fmt.Println(q, r) // 3 2
언제 좋고 언제 나쁜가
명명된 반환값의 장점:
- 함수 시그니처만 봐도 반환의 의미를 알 수 있다
- 짧은 함수에서 의도를 분명히 드러낸다
단점:
- 함수가 길어지면 어디서 값이 바뀌는지 추적이 어렵다
- naked return 은 함수 끝에서 어떤 값이 나가는지 한눈에 안 보인다
짧고 의미가 분명한 함수에서만 쓴다. 본문이 길어지면 그냥 일반 다중 반환을 쓰는 편이 안전하다.
9.5 가변 인자 (variadic)
같은 타입의 인자를 임의 개수 받고 싶을 때 쓴다.
타입 앞에 점 세 개 ... 를 붙이면 된다.
func sum(nums ...int) int {
total := 0
for _, n := range nums {
total += n
}
return total
}
func main() {
fmt.Println(sum()) // 0
fmt.Println(sum(1, 2, 3)) // 6
fmt.Println(sum(10, 20, 30, 40)) // 100
}
함수 내부에서 nums 는 슬라이스([]int) 처럼 다룬다.
range 로 순회할 수 있다.
슬라이스는 11장에서 본격적으로 다룬다. 지금은 “여러 값을 한 변수로 받는다” 정도만 알면 충분하다.
슬라이스 펼쳐 넘기기
이미 슬라이스를 가지고 있는데
가변 인자 함수에 그걸 넘기고 싶다면
호출할 때 ... 를 변수 뒤에 붙인다.
xs := []int{1, 2, 3, 4}
fmt.Println(sum(xs...)) // 10
xs... 가 없으면 컴파일 에러가 난다.
Go 는 슬라이스를 자동으로 풀어 주지 않는다.
가변 인자는 마지막에 하나만
함수 시그니처에서 가변 인자는 가장 마지막 매개변수여야 하며, 하나만 둘 수 있다.
// OK
func f(prefix string, nums ...int) {}
// 컴파일 에러
func g(nums ...int, suffix string) {}
9.6 defer
defer 는 어떤 호출을
지금 등록하고 함수가 끝날 때 실행 시킨다.
func main() {
defer fmt.Println("끝")
fmt.Println("시작")
fmt.Println("작업 중")
}
실행 결과:
시작
작업 중
끝
defer 가 가장 위에 있지만 출력은 마지막에 나온다.
main 이 반환되기 직전에 실행되기 때문이다.
어디에 쓰나
가장 흔한 용도는 “정리(cleanup) 작업” 이다.
- 파일 열고 닫기
- 락 잡고 풀기
- 네트워크 연결 열고 닫기
- 자원 빌리고 반납하기
이런 작업은 “여는 부분” 바로 옆에
“닫는 부분” 을 defer 로 같이 적는다.
중간에 어떤 분기로 빠지든
함수만 끝나면 반드시 정리가 호출된다.
func work() {
f := openFile()
defer closeFile(f) // 함수 끝에서 무조건 닫힘
// 중간에 return, 에러로 빠져도 closeFile 은 호출된다
process(f)
}
실제 파일 닫기는 29장에서, 락 해제는 23장에서 다시 만난다. 여기서는
defer의 동작 원리만 익혀 두자.
LIFO 순서
여러 defer 가 쌓이면
가장 나중에 등록된 것부터 실행된다.
스택 구조(LIFO)다.
func main() {
defer fmt.Println("1")
defer fmt.Println("2")
defer fmt.Println("3")
}
실행 결과:
3
2
1
여러 자원을 순서대로 열었을 때 역순으로 닫는 것이 자연스럽다는 점과 맞아 떨어진다.
평가 시점은 등록할 때, 실행은 함수 끝
defer 가 받는 함수 호출의 인자 는
defer 를 만나는 그 순간 평가된다.
실제 호출만 함수 끝으로 미뤄진다.
func main() {
x := 10
defer fmt.Println("x =", x) // 여기서 x 가 10 으로 캡처됨
x = 99
}
실행 결과:
x = 10
x 가 99 로 바뀌었지만,
출력은 defer 가 등록되던 시점의 10 이다.
“인자는 그 자리에서 굳고, 실행만 미뤄진다” 고 기억하면 된다.
9.7 익명 함수와 클로저
함수에 꼭 이름을 붙여야 하는 것은 아니다. 이름 없이 그 자리에 함수를 만들어 바로 쓰는 것을 익명 함수 라 부른다.
정의 후 즉시 호출
func main() {
func() {
fmt.Println("이름 없이 호출")
}()
}
마지막의 () 가 호출 부분이다.
정의하자마자 부른 것이다.
변수에 함수를 담기
함수는 그 자체가 하나의 값이다. 변수에 넣어 둘 수 있고, 다른 함수에 넘길 수도 있다.
func main() {
add := func(a, b int) int {
return a + b
}
fmt.Println(add(3, 4)) // 7
}
add 의 타입은 func(int, int) int 다.
이런 함수 타입을 매개변수로 받는 함수도 만들 수 있다.
func apply(f func(int) int, x int) int {
return f(x)
}
func main() {
double := func(n int) int { return n * 2 }
fmt.Println(apply(double, 5)) // 10
}
클로저: 바깥 변수를 캡처한다
익명 함수가 자신을 둘러싼 함수의 변수를 계속 들고 있을 수 있다. 이렇게 바깥 변수를 붙잡고 있는 함수를 클로저(closure) 라 부른다.
간단한 카운터를 만들어 본다.
func makeCounter() func() int {
count := 0
return func() int {
count++
return count
}
}
func main() {
c := makeCounter()
fmt.Println(c()) // 1
fmt.Println(c()) // 2
fmt.Println(c()) // 3
}
makeCounter 의 지역 변수 count 는
보통이라면 함수가 끝나는 순간 사라진다.
하지만 안에서 만든 익명 함수가 count 를 잡고 있어서
함수가 반환된 뒤에도 살아남는다.
호출할 때마다 같은 count 가 1 씩 증가한다.
각 클로저는 자기 변수를 가진다
makeCounter 를 다시 부르면
완전히 새로운 count 가 만들어진다.
c1 := makeCounter()
c2 := makeCounter()
fmt.Println(c1()) // 1
fmt.Println(c1()) // 2
fmt.Println(c2()) // 1 (c2 의 count 는 따로)
fmt.Println(c1()) // 3
클로저는 “함수 + 캡처한 변수” 의 묶음이다. 같은 함수를 두 번 호출해 만든 두 클로저는 서로 다른 환경을 가진다.
클로저는 강력하지만, 어떤 변수를 캡처했는지를 머릿속에 그리고 있어야 한다. 특히 for 루프 안에서 클로저를 만들 때 함정이 있다. 동시성 코드에서도 자주 문제가 되므로 22장에서 다시 정리한다.
9.8 정리
이 장에서 살펴본 내용:
- 함수는
func 이름(매개변수) 반환타입 { ... } - 같은 타입 매개변수는 묶어서 한 번만 타입 표기 가능
- 다중 반환값으로 결과와 상태를 함께 돌려준다
_로 필요 없는 반환값을 무시한다- 명명된 반환값과 naked return 은 짧은 함수에 한정
- 가변 인자
...T와 슬라이스 펼치기xs... defer는 함수 종료 시 실행, LIFO, 인자는 등록 시점에 평가- 함수도 값이다. 익명 함수와 클로저로 동작을 변수처럼 다룬다
함수를 자유롭게 다루게 됐으니 이제 다음 질문이 자연스럽다. “여기 선언한 변수는 어디까지 살아 있는가?”
다음 장은 변수의 범위(scope)다.
패키지 / 함수 / 블록 세 단계의 범위,
변수 가리기(shadowing) 와 := 의 함정,
그리고 전역 변수를 자제해야 하는 이유까지 다룬다.