본문으로 건너뛰기

함수 기본

함수란?​

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

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.Println과 fmt.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에서는 절대 패닉을 사용하지 마세요.