고 기초 #4 함수, 다중 반환, error 타입
#3 제어 흐름에서 분기와 반복을 봤습니다. 이번엔 — 그 코드를 묶어 재사용하는 도구인 Go의 함수를 정리합니다.
함수 정의 #
func add(a int, b int) int {
return a + b
}
func main() {
result := add(2, 3)
fmt.Println(result) // 5
}문법: func 이름(매개변수) 반환타입 { 본문 }. 매개변수마다 타입을 적습니다.
같은 타입 매개변수 묶기 #
func add(a, b int) int {
return a + b
}같은 타입이 연속이면 마지막에 한 번만 적습니다.
반환값 없음 #
func greet(name string) {
fmt.Println("안녕,", name)
}반환 타입을 비웁니다. C의 void와 같은 역할입니다.
다중 반환 — Go의 시그니처 #
Go의 가장 두드러진 특징 — 함수가 여러 값을 한 번에 반환할 수 있습니다.
func divide(a, b int) (int, int) {
q := a / b
r := a % b
return q, r
}
func main() {
quot, rem := divide(10, 3)
fmt.Println(quot, rem) // 3 1
}JS의 디스트럭처링이나 Python의 튜플 분해와 비슷하지만 — Go는 진짜 다중 값입니다. 호출하는 쪽에서 두 변수에 동시에 받습니다.
일부 무시 — _
#
quot, _ := divide(10, 3) // 나머지는 무시_는 “이 값은 안 쓸 거야” 의미. 가독성 좋고, 사용하지 않는 변수 컴파일 에러도 피할 수 있습니다.
명명된 반환값 #
반환값에 미리 이름을 줄 수 있습니다.
func divide(a, b int) (q, r int) {
q = a / b
r = a % b
return // q, r 자동 사용 (naked return)
}q, r이 함수 시작에서 자동으로 선언되고, return에 인자가 없으면 그 값들이 자동 반환됩니다.
가독성 측면에서 호불호가 갈립니다. 짧은 함수에서 명확함이 늘어나면 OK, 길어지면 명시적인 return q, r이 더 좋습니다.
error 타입 — Go의 에러 처리
#
다른 언어가 try/catch로 예외를 던지는 상황에서, Go는 error를 반환값으로 다룹니다.
import (
"fmt"
"strconv"
)
func main() {
n, err := strconv.Atoi("42")
if err != nil {
fmt.Println("실패:", err)
return
}
fmt.Println(n)
}Atoi는 두 값을 반환합니다 — 변환된 정수와 error. error가 nil이면 성공이고, nil 아니면 그 안에 에러 정보.
if err != nil { ... } 패턴이 Go 코드의 절반쯤이라고 해도 과언이 아닙니다.
error는 인터페이스 #
type error interface {
Error() string
}error는 빌트인 인터페이스로, Error() string 메서드 하나만 가집니다. 어떤 타입이든 이 메서드가 있으면 error가 됩니다.
자기 함수에서 에러 반환 #
import "errors"
func divide(a, b int) (int, error) {
if b == 0 {
return 0, errors.New("0으로 나눌 수 없음")
}
return a / b, nil
}
func main() {
r, err := divide(10, 0)
if err != nil {
fmt.Println("에러:", err)
return
}
fmt.Println(r)
}errors.New(...)가 가장 단순한 에러 생성. 메시지에 값을 끼워넣으려면 fmt.Errorf가 더 흔합니다.
return 0, fmt.Errorf("0으로 나누려는 a=%d", a)Sentinel 에러 — 미리 정의된 에러 #
라이브러리들이 자주 쓰는 패턴 — 미리 변수로 정의해 둔 에러.
var ErrNotFound = errors.New("not found")
func lookup(key string) (string, error) {
if key == "" {
return "", ErrNotFound
}
return "value", nil
}
// 호출자가 비교 가능
if errors.Is(err, ErrNotFound) {
// ...
}errors.Is는 중급 #2 에러 처리에서 자세히 다룹니다.
defer — 함수 끝나기 직전에 실행
#
리소스 정리(파일 닫기, 락 해제, DB 연결 닫기)에 거의 표준으로 쓰는 도구.
func readFile() {
file, err := os.Open("data.txt")
if err != nil {
return
}
defer file.Close() // 함수 끝날 때 자동 호출
// ... 파일 사용
}defer는 그 줄을 즉시 실행하는 게 아니라, 함수가 return 직전 실행되도록 예약합니다. 어떤 경로(정상 return, panic 등)로 함수를 빠져나가도 호출됩니다.
여러 defer는 LIFO #
func main() {
defer fmt.Println("1")
defer fmt.Println("2")
defer fmt.Println("3")
fmt.Println("main")
}
// main
// 3
// 2
// 1스택처럼 — 나중에 적은 게 먼저 실행됩니다.
defer의 인자는 즉시 평가됨 #
자주 헷갈리는 함정.
i := 10
defer fmt.Println(i) // 인자 i가 이때 평가 → 10
i = 20
// 함수 끝 — 출력: 10fmt.Println(i)의 i가 defer 시점에 평가되어 10으로 박힙니다. 그 뒤에 i가 바뀌어도 영향 없습니다.
이걸 피하려면 클로저로 감싸세요.
i := 10
defer func() { fmt.Println(i) }() // 실행 시점에 i 평가
i = 20
// 출력: 20가변 인자 — ...
#
func sum(nums ...int) int {
total := 0
for _, n := range nums {
total += n
}
return total
}
sum(1, 2) // 3
sum(1, 2, 3, 4, 5) // 15
sum() // 0nums ...int — 임의 개수의 int를 받아 슬라이스로 모음. JS의 rest 매개변수 (기초 #4)와 같은 역할입니다.
슬라이스 펼쳐 넘기기 #
이미 슬라이스가 있으면 — ...으로 펼쳐 인자로 보낼 수 있습니다.
nums := []int{1, 2, 3}
sum(nums...) // 6 — 슬라이스를 인자들로 풀어서함수도 값 (1급 객체) #
함수를 변수에 담거나, 다른 함수에 인자로 넘기거나, 반환할 수 있습니다.
add := func(a, b int) int {
return a + b
}
result := add(2, 3)함수 타입 #
type BinaryOp func(int, int) int
func apply(op BinaryOp, a, b int) int {
return op(a, b)
}
apply(add, 2, 3) // 5함수 시그니처를 타입 별칭으로 만듭니다. 콜백을 받는 API에서 자주 등장하는 모양입니다.
클로저 — 함수가 환경을 들고 다님 #
JS의 클로저와 같은 개념. Go에서도 그대로.
func makeCounter() func() int {
count := 0
return func() int {
count++
return count
}
}
counter := makeCounter()
fmt.Println(counter()) // 1
fmt.Println(counter()) // 2
fmt.Println(counter()) // 3반환된 익명 함수가 count를 캡처해 살아남습니다. JS와 동일한 동작입니다.
메서드 — 다음 글 미리보기 #
함수가 특정 타입에 묶여 있으면 메서드라 부릅니다.
type Counter struct {
count int
}
func (c *Counter) Increment() {
c.count++
}
c := &Counter{}
c.Increment()
c.Increment()
fmt.Println(c.count) // 2func (c *Counter) Increment()의 (c *Counter) 부분이 리시버. 이 함수는 *Counter의 메서드입니다. #6 구조체와 메서드에서 자세히.
자주 쓰는 패턴들 #
1) 에러를 위로 흘리기 #
func processFile(path string) error {
file, err := os.Open(path)
if err != nil {
return fmt.Errorf("파일 열기 실패: %w", err)
}
defer file.Close()
if err := parse(file); err != nil {
return fmt.Errorf("파싱 실패: %w", err)
}
return nil
}%w verb가 핵심 — 원래 에러를 wrapping합니다. 호출자가 errors.Is/errors.As로 안의 에러까지 검사할 수 있게 됩니다.
2) 에러와 결과를 한 번에 반환 #
func parseUser(input string) (*User, error) {
parts := strings.Split(input, ":")
if len(parts) != 2 {
return nil, fmt.Errorf("잘못된 형식: %q", input)
}
return &User{Name: parts[0], ID: parts[1]}, nil
}성공이면 (결과, nil), 실패면 (zero값, 에러)가 표준입니다.
3) 함수 분해 — 작게 자르기 #
Go 코드의 일반적인 모양은 함수를 작게 자르고, 각 함수가 한 가지 일을 하게 하는 것입니다. early return + 에러 wrapping으로 흐름이 깔끔해집니다.
마무리 #
이번 글에서 정리한 내용:
func 이름(매개변수) 반환타입 { }- 같은 타입은 묶어 적기, 명명된 반환값 가능
- 다중 반환 — Go의 시그니처
error는 인터페이스 —Error() string메서드만 있으면 됨if err != nil { return err }가 표준 패턴errors.New/fmt.Errorf로 에러 생성,%w로 wrappingdefer로 정리 코드 — LIFO 순서, 인자는 즉시 평가- 가변 인자
..., 슬라이스 펼치기nums... - 함수도 1급 값, 클로저 동작
- 메서드는 #6에서
다음 글(#5 컬렉션)에서는 Go의 세 가지 컬렉션 — 배열, 슬라이스, 맵 — 의 사용법과 자주 만나는 함정을 다룹니다.