본문으로 건너뛰기

함수 기본

함수란?

함수는 프로그램을 구성하는 기본 단위입니다. 반복되는 코드를 하나의 이름으로 묶어 재사용할 수 있게 해주며, 복잡한 문제를 작은 단위로 나눠 해결하는 핵심 도구입니다.

Go의 함수는 다른 언어와 비교했을 때 몇 가지 강력한 특징을 갖습니다.

  • 다중 반환값: 함수가 여러 값을 동시에 반환할 수 있습니다.
  • 네임드 반환값: 반환값에 이름을 붙여 문서화와 naked return을 활용할 수 있습니다.
  • 가변 인자: ...T 문법으로 임의 개수의 인자를 받을 수 있습니다.
  • 일급 객체: 함수를 변수에 저장하거나 다른 함수의 인자로 전달할 수 있습니다.

기본 함수 선언

Go 함수의 기본 형태는 다음과 같습니다.

func 함수이름(파라미터) 반환타입 {
// 함수 본문
}
package main

import "fmt"

// 두 정수를 더하는 기본 함수
func add(a int, b int) int {
return a + b
}

// 같은 타입의 파라미터는 마지막에 타입을 한 번만 쓸 수 있습니다
func multiply(a, b int) int {
return a * b
}

// 반환값이 없는 함수
func greet(name string) {
fmt.Printf("안녕하세요, %s님!\n", name)
}

func main() {
result := add(3, 5)
fmt.Println("3 + 5 =", result)

product := multiply(4, 7)
fmt.Println("4 × 7 =", product)

greet("Go 개발자")
}

실행 결과:

3 + 5 = 8
4 × 7 = 28
안녕하세요, Go 개발자님!

파라미터 타입 생략 문법(a, b int)은 a int, b int와 완전히 동일합니다. 같은 타입이 연속될 때 코드를 간결하게 만들어 줍니다.


다중 반환값 — Go의 핵심 특징

Go의 가장 두드러진 특징 중 하나는 함수가 여러 값을 동시에 반환 할 수 있다는 점입니다. 다른 언어에서는 에러 처리를 위해 예외(Exception)를 사용하거나, 구조체를 반환해야 했습니다. Go는 값과 에러를 함께 반환하는 관용적인 패턴을 언어 차원에서 지원합니다.

package main

import (
"errors"
"fmt"
"strconv"
)

// 두 값을 동시에 반환
func divide(a, b float64) (float64, error) {
if b == 0 {
return 0, errors.New("0으로 나눌 수 없습니다")
}
return a / b, nil
}

// 문자열을 파싱해서 정수와 에러를 반환
func parseAge(s string) (int, error) {
age, err := strconv.Atoi(s)
if err != nil {
return 0, fmt.Errorf("나이 파싱 실패: %w", err)
}
if age < 0 || age > 150 {
return 0, fmt.Errorf("유효하지 않은 나이: %d", age)
}
return age, nil
}

// 세 값을 반환하는 함수
func minMax(nums []int) (min, max, sum int) {
if len(nums) == 0 {
return 0, 0, 0
}
min, max, sum = nums[0], nums[0], 0
for _, n := range nums {
if n < min {
min = n
}
if n > max {
max = n
}
sum += n
}
return min, max, sum
}

func main() {
// 다중 반환값 처리 — 관용적인 패턴
result, err := divide(10, 3)
if err != nil {
fmt.Println("에러:", err)
} else {
fmt.Printf("10 / 3 = %.4f\n", result)
}

// 0으로 나누기 시도
_, err = divide(5, 0)
if err != nil {
fmt.Println("에러:", err)
}

// 문자열 파싱
age, err := parseAge("25")
if err != nil {
fmt.Println("파싱 에러:", err)
} else {
fmt.Printf("나이: %d세\n", age)
}

// 세 값 동시 반환
nums := []int{3, 1, 4, 1, 5, 9, 2, 6}
min, max, sum := minMax(nums)
fmt.Printf("최솟값: %d, 최댓값: %d, 합계: %d\n", min, max, sum)
}

실행 결과:

10 / 3 = 3.3333
에러: 0으로 나눌 수 없습니다
나이: 25세
최솟값: 1, 최댓값: 9, 합계: 31

반환값이 여러 개일 때는 괄호로 묶어 선언합니다. 특정 반환값이 필요 없을 때는 _(블랭크 식별자)로 무시할 수 있습니다.


네임드 반환값 (Named Return Values)

반환값에 이름을 붙이면 함수 선언부에서 반환값의 의미를 명확히 전달할 수 있고, 함수 본문에서 해당 변수를 바로 사용할 수 있습니다. 또한 return 키워드만 쓰는 naked return 이 가능합니다.

package main

import (
"fmt"
"math"
)

// 네임드 반환값: 선언부에서 의미가 명확합니다
func circleMetrics(radius float64) (area, perimeter float64) {
area = math.Pi * radius * radius
perimeter = 2 * math.Pi * radius
return // naked return — area와 perimeter를 그대로 반환
}

// 에러 처리에서 네임드 반환값 활용
func safeDivide(a, b int) (result int, err error) {
defer func() {
if r := recover(); r != nil {
err = fmt.Errorf("패닉 복구: %v", r)
}
}()

if b == 0 {
err = fmt.Errorf("0으로 나눌 수 없습니다")
return // err가 설정된 채로 반환
}
result = a / b
return // result와 err(nil)을 반환
}

// 복잡한 계산에서 중간 상태를 추적하기 좋은 패턴
func statistics(data []float64) (mean, variance, stddev float64) {
n := float64(len(data))
if n == 0 {
return // 모두 0
}

// 평균 계산
for _, v := range data {
mean += v
}
mean /= n

// 분산 계산
for _, v := range data {
diff := v - mean
variance += diff * diff
}
variance /= n

// 표준편차
stddev = math.Sqrt(variance)
return
}

func main() {
area, perimeter := circleMetrics(5)
fmt.Printf("반지름 5 원: 넓이=%.2f, 둘레=%.2f\n", area, perimeter)

result, err := safeDivide(10, 3)
if err != nil {
fmt.Println("에러:", err)
} else {
fmt.Println("10 / 3 =", result)
}

_, err = safeDivide(10, 0)
fmt.Println("0 나누기 에러:", err)

data := []float64{2, 4, 4, 4, 5, 5, 7, 9}
mean, variance, stddev := statistics(data)
fmt.Printf("평균=%.2f, 분산=%.2f, 표준편차=%.2f\n", mean, variance, stddev)
}

실행 결과:

반지름 5 원: 넓이=78.54, 둘레=31.42
10 / 3 = 3
0 나누기 에러: 0으로 나눌 수 없습니다
평균=5.00, 분산=4.00, 표준편차=2.00

주의: naked return은 짧은 함수에서는 가독성을 높이지만, 함수가 길어질수록 어떤 값이 반환되는지 파악하기 어려워집니다. 10줄을 넘는 함수에서는 명시적으로 return result, err 형태를 쓰는 것이 좋습니다.


가변 인자 함수 (Variadic Functions)

...T 문법을 사용하면 임의 개수의 인자를 받을 수 있습니다. 가변 인자는 함수 내부에서 슬라이스로 처리됩니다. 가변 인자는 파라미터 목록의 마지막에만 올 수 있습니다.

package main

import "fmt"

// 임의 개수의 정수를 받아 합산
func sum(nums ...int) int {
total := 0
for _, n := range nums {
total += n
}
return total
}

// 가변 인자와 일반 인자 혼합
func logMessage(level string, args ...any) {
fmt.Printf("[%s] ", level)
fmt.Println(args...)
}

// 가변 인자로 최댓값 찾기
func max(first int, rest ...int) int {
m := first
for _, v := range rest {
if v > m {
m = v
}
}
return m
}

// 슬라이스를 가변 인자로 전달하기 — ... 언팩 문법
func appendStrings(sep string, parts ...string) string {
result := ""
for i, p := range parts {
if i > 0 {
result += sep
}
result += p
}
return result
}

func main() {
fmt.Println(sum()) // 인자 없음
fmt.Println(sum(1, 2, 3))
fmt.Println(sum(1, 2, 3, 4, 5))

// 슬라이스를 언팩해서 전달
numbers := []int{10, 20, 30, 40}
fmt.Println(sum(numbers...)) // ... 언팩

logMessage("INFO", "서버 시작", "포트:", 8080)
logMessage("ERROR", "연결 실패")

fmt.Println(max(3))
fmt.Println(max(3, 1, 4, 1, 5, 9, 2, 6))

words := []string{"Go", "는", "재밌다"}
fmt.Println(appendStrings(" ", words...))
fmt.Println(appendStrings("-", "2024", "01", "01"))
}

실행 결과:

0
6
15
100
[INFO] 서버 시작 포트: 8080
[ERROR] 연결 실패
3
9
Go 는 재밌다
2024-01-01

fmt.Printlnfmt.Printf 자체도 가변 인자 함수입니다. fmt.Println(args...) 형태로 슬라이스를 그대로 전달하는 패턴을 주목하세요.


실전 예제: 에러 + 값 동시 반환 패턴

실무에서 가장 자주 쓰이는 패턴입니다. Go의 에러 처리 관용구를 완전한 예제로 살펴봅니다.

package main

import (
"errors"
"fmt"
"os"
"strconv"
)

// 사용자 정의 에러 타입
type ValidationError struct {
Field string
Message string
}

func (e *ValidationError) Error() string {
return fmt.Sprintf("유효성 검사 실패 [%s]: %s", e.Field, e.Message)
}

// 설정값 파싱 — 여러 에러 상황을 값 + 에러로 반환
func parseConfig(portStr, timeoutStr string) (port, timeout int, err error) {
port, err = strconv.Atoi(portStr)
if err != nil {
err = &ValidationError{Field: "port", Message: "정수가 아닙니다"}
return
}
if port < 1 || port > 65535 {
err = &ValidationError{Field: "port", Message: "1~65535 범위여야 합니다"}
return
}

timeout, err = strconv.Atoi(timeoutStr)
if err != nil {
err = &ValidationError{Field: "timeout", Message: "정수가 아닙니다"}
return
}
if timeout <= 0 {
err = &ValidationError{Field: "timeout", Message: "양수여야 합니다"}
return
}

return // port, timeout, nil
}

// 파일 내용을 읽어 길이와 함께 반환
func readFileContent(path string) (content string, size int, err error) {
data, err := os.ReadFile(path)
if err != nil {
// 에러 래핑 — 컨텍스트 추가
err = fmt.Errorf("파일 읽기 실패 (%s): %w", path, err)
return
}
content = string(data)
size = len(data)
return
}

// errors.Is / errors.As 활용 예
func handleError(err error) {
var ve *ValidationError
if errors.As(err, &ve) {
fmt.Printf("입력 오류 — 필드: %s, 메시지: %s\n", ve.Field, ve.Message)
return
}
if errors.Is(err, os.ErrNotExist) {
fmt.Println("파일을 찾을 수 없습니다")
return
}
fmt.Println("알 수 없는 에러:", err)
}

func main() {
// 정상 케이스
port, timeout, err := parseConfig("8080", "30")
if err != nil {
handleError(err)
} else {
fmt.Printf("포트: %d, 타임아웃: %d초\n", port, timeout)
}

// 유효성 검사 에러
_, _, err = parseConfig("99999", "30")
if err != nil {
handleError(err)
}

// 존재하지 않는 파일
_, _, err = readFileContent("존재하지않는파일.txt")
if err != nil {
handleError(err)
}
}

실행 결과:

포트: 8080, 타임아웃: 30초
입력 오류 — 필드: port, 메시지: 1~65535 범위여야 합니다
파일을 찾을 수 없습니다

실전 예제: HTTP 핸들러 함수 시그니처

Go 표준 라이브러리의 net/http 패키지에서 핸들러 함수는 고정된 시그니처를 가집니다. 이 패턴을 이해하면 웹 서버 개발의 기초를 다질 수 있습니다.

package main

import (
"encoding/json"
"fmt"
"net/http"
"strconv"
)

// 표준 HTTP 핸들러 시그니처
// func(http.ResponseWriter, *http.Request)

type Response struct {
Status string `json:"status"`
Message string `json:"message"`
Data any `json:"data,omitempty"`
}

// JSON 응답을 편리하게 보내는 헬퍼 함수
func writeJSON(w http.ResponseWriter, statusCode int, data any) {
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(statusCode)
if err := json.NewEncoder(w).Encode(data); err != nil {
fmt.Println("JSON 인코딩 에러:", err)
}
}

// 기본 핸들러
func helloHandler(w http.ResponseWriter, r *http.Request) {
writeJSON(w, http.StatusOK, Response{
Status: "ok",
Message: "Hello, Go!",
})
}

// 쿼리 파라미터를 처리하는 핸들러
func squareHandler(w http.ResponseWriter, r *http.Request) {
numStr := r.URL.Query().Get("n")
if numStr == "" {
writeJSON(w, http.StatusBadRequest, Response{
Status: "error",
Message: "파라미터 n이 필요합니다",
})
return
}

n, err := strconv.Atoi(numStr)
if err != nil {
writeJSON(w, http.StatusBadRequest, Response{
Status: "error",
Message: "n은 정수여야 합니다",
})
return
}

writeJSON(w, http.StatusOK, Response{
Status: "ok",
Data: map[string]int{"input": n, "square": n * n},
})
}

func main() {
mux := http.NewServeMux()
mux.HandleFunc("/hello", helloHandler)
mux.HandleFunc("/square", squareHandler)

fmt.Println("서버 시작: http://localhost:8080")
fmt.Println("테스트: curl http://localhost:8080/hello")
fmt.Println("테스트: curl 'http://localhost:8080/square?n=7'")

if err := http.ListenAndServe(":8080", mux); err != nil {
fmt.Println("서버 에러:", err)
}
}

이 예제는 실제 HTTP 서버를 시작합니다. go run 후 위에 표시된 curl 명령으로 테스트할 수 있습니다.


함수 선언 관련 주요 규칙 정리

규칙설명
파라미터 타입 생략a, b int처럼 같은 타입은 마지막에 한 번만
다중 반환값(int, error) 형태로 괄호 안에 나열
네임드 반환값(result int, err error) — naked return 가능
가변 인자...T는 반드시 마지막 파라미터
슬라이스 언팩fn(slice...) 형태로 슬라이스를 가변 인자로 전달
블랭크 식별자_로 불필요한 반환값 무시

고수 팁

관용적인 에러 반환 순서: Go에서 다중 반환값을 사용할 때 에러는 항상 마지막에 옵니다. (value, error) 순서는 Go 커뮤니티 전체의 관용구입니다. 이 순서를 지키면 if err != nil 패턴이 자연스럽게 따라옵니다.

함수 크기: Go의 관용적인 코드는 함수를 작게 유지합니다. 하나의 함수는 하나의 일만 합니다. 20~30줄이 넘어간다면 더 작은 함수로 분리를 고려하세요.

패닉 vs 에러 반환: 예측 가능한 실패(파일 없음, 잘못된 입력)는 에러를 반환하고, 프로그래밍 버그(nil 포인터 역참조, 범위 초과)는 패닉이 자연스럽습니다. 공개 API에서는 절대 패닉을 사용하지 마세요.