Updated:

11 minute read

개요

Go의 함수는 일급 객체(first-class citizen)로 다음과 같은 특징을 가집니다:

  • Pass by value: 모든 인자는 값으로 전달
  • 다중 반환 값: 여러 값을 동시에 반환 가능
  • Named return values: 반환 값에 이름 지정 가능
  • 가변 인자: 임의 개수의 인자 전달 가능
  • 클로저 지원: 외부 변수 캡처 가능
  • defer 문: 함수 종료 시 실행할 코드 예약
  • 메서드: 타입에 연결된 함수

기본 함수 정의

1. 기본 형태

package main

import "fmt"

// 매개변수와 반환 타입
func add(a int, b int) int {
    return a + b
}

// 같은 타입의 연속된 매개변수는 타입을 한 번만 선언
func multiply(a, b, c int) int {
    return a * b * c
}

// 반환 값이 없는 함수
func printMessage(msg string) {
    fmt.Println(msg)
}

func main() {
    fmt.Println(add(3, 5))           // 출력: 8
    fmt.Println(multiply(2, 3, 4))   // 출력: 24
    printMessage("Hello, Go!")       // 출력: Hello, Go!
}

2. 다중 반환 값

import "errors"

// 두 개의 값 반환
func divide(a, b float64) (float64, error) {
    if b == 0 {
        return 0, errors.New("division by zero")
    }
    return a / b, nil
}

func main() {
    // 모든 반환 값 받기
    result, err := divide(10, 2)
    if err != nil {
        fmt.Println("Error:", err)
        return
    }
    fmt.Println("Result:", result) // 출력: Result: 5

    // 빈 식별자(_)로 에러 무시 (권장하지 않음)
    result, _ = divide(10, 0)
    fmt.Println(result) // 출력: 0 (에러 무시됨)
}

3. Named Return Values

// 반환 값에 이름 지정
func rectangleProps(width, height float64) (area, perimeter float64) {
    area = width * height
    perimeter = 2 * (width + height)
    return // naked return: 명시된 변수들 자동 반환
}

// named return은 문서화와 가독성 향상에 유용
func parseConfig(path string) (config map[string]string, err error) {
    config = make(map[string]string)
    
    // err가 이미 선언되어 있어 := 대신 = 사용
    if path == "" {
        err = errors.New("empty path")
        return
    }
    
    // 정상 처리...
    return
}

func main() {
    area, perimeter := rectangleProps(5, 3)
    fmt.Printf("Area: %.2f, Perimeter: %.2f\n", area, perimeter)
    // 출력: Area: 15.00, Perimeter: 16.00
}

가변 인자 함수 (Variadic Functions)

// 가변 인자는 슬라이스로 전달됨
func sum(numbers ...int) int {
    total := 0
    for _, num := range numbers {
        total += num
    }
    return total
}

// 가변 인자는 마지막 매개변수만 가능
func printf(format string, args ...interface{}) {
    fmt.Printf(format, args...)
}

func main() {
    fmt.Println(sum(1, 2, 3))           // 출력: 6
    fmt.Println(sum(1, 2, 3, 4, 5))     // 출력: 15
    
    // 슬라이스를 가변 인자로 전달 (언팩)
    numbers := []int{10, 20, 30}
    fmt.Println(sum(numbers...))        // 출력: 60
    
    printf("Name: %s, Age: %d\n", "Alice", 25)
    // 출력: Name: Alice, Age: 25
}

함수 타입과 고차 함수

1. 함수를 변수에 할당

func main() {
    // 함수 타입: func(int, int) int
    var operation func(int, int) int
    
    operation = add
    fmt.Println(operation(5, 3)) // 출력: 8
    
    operation = func(a, b int) int {
        return a - b
    }
    fmt.Println(operation(5, 3)) // 출력: 2
}

2. 함수를 인자로 전달

func applyOperation(a, b int, op func(int, int) int) int {
    return op(a, b)
}

func main() {
    result := applyOperation(10, 5, add)
    fmt.Println(result) // 출력: 15
    
    // 익명 함수 전달
    result = applyOperation(10, 5, func(a, b int) int {
        return a * b
    })
    fmt.Println(result) // 출력: 50
}

3. 함수를 반환

func makeMultiplier(factor int) func(int) int {
    return func(n int) int {
        return n * factor
    }
}

func main() {
    double := makeMultiplier(2)
    triple := makeMultiplier(3)
    
    fmt.Println(double(5))  // 출력: 10
    fmt.Println(triple(5))  // 출력: 15
}

클로저 (Closures)

// 클로저는 외부 변수를 캡처하고 유지
func counter() func() int {
    count := 0
    return func() int {
        count++
        return count
    }
}

func main() {
    c1 := counter()
    c2 := counter()
    
    fmt.Println(c1()) // 출력: 1
    fmt.Println(c1()) // 출력: 2
    fmt.Println(c1()) // 출력: 3
    
    fmt.Println(c2()) // 출력: 1 (독립적인 카운터)
    fmt.Println(c2()) // 출력: 2
}

클로저 활용 예제

// 필터 함수
func filter(numbers []int, predicate func(int) bool) []int {
    result := []int{}
    for _, num := range numbers {
        if predicate(num) {
            result = append(result, num)
        }
    }
    return result
}

func main() {
    numbers := []int{1, 2, 3, 4, 5, 6, 7, 8, 9, 10}
    
    // 짝수 필터
    evens := filter(numbers, func(n int) bool {
        return n%2 == 0
    })
    fmt.Println(evens) // 출력: [2 4 6 8 10]
    
    // threshold를 캡처하는 클로저
    threshold := 5
    greaterThan := filter(numbers, func(n int) bool {
        return n > threshold
    })
    fmt.Println(greaterThan) // 출력: [6 7 8 9 10]
}

defer 문

1. 기본 사용법

import "os"

func readFile(path string) error {
    file, err := os.Open(path)
    if err != nil {
        return err
    }
    defer file.Close() // 함수 종료 시 자동 실행
    
    // 파일 읽기 작업...
    return nil
}

func main() {
    fmt.Println("Start")
    defer fmt.Println("End")
    fmt.Println("Middle")
    // 출력:
    // Start
    // Middle
    // End
}

2. defer 스택 (LIFO)

func deferStack() {
    defer fmt.Println("First defer")
    defer fmt.Println("Second defer")
    defer fmt.Println("Third defer")
    fmt.Println("Function body")
    // 출력:
    // Function body
    // Third defer
    // Second defer
    // First defer
}

3. defer에서 인자 평가 시점

func deferEvaluation() {
    x := 10
    defer fmt.Println("Deferred:", x) // x는 이 시점에 평가됨
    
    x = 20
    fmt.Println("Current:", x)
    // 출력:
    // Current: 20
    // Deferred: 10 (defer 등록 시점의 값)
}

4. defer로 panic 복구

func safeDivide(a, b int) (result int, err error) {
    defer func() {
        if r := recover(); r != nil {
            err = fmt.Errorf("panic recovered: %v", r)
        }
    }()
    
    if b == 0 {
        panic("division by zero")
    }
    
    result = a / b
    return
}

func main() {
    result, err := safeDivide(10, 0)
    if err != nil {
        fmt.Println("Error:", err) 
        // 출력: Error: panic recovered: division by zero
    } else {
        fmt.Println("Result:", result)
    }
}

메서드 (Methods)

1. 값 리시버 vs 포인터 리시버

type Rectangle struct {
    Width  float64
    Height float64
}

// 값 리시버 (복사본에 대해 동작)
func (r Rectangle) Area() float64 {
    return r.Width * r.Height
}

// 포인터 리시버 (원본을 수정 가능)
func (r *Rectangle) Scale(factor float64) {
    r.Width *= factor
    r.Height *= factor
}

// 포인터 리시버 (큰 구조체 복사 방지)
func (r *Rectangle) Perimeter() float64 {
    return 2 * (r.Width + r.Height)
}

func main() {
    rect := Rectangle{Width: 10, Height: 5}
    
    fmt.Println("Area:", rect.Area())           // 출력: Area: 50
    fmt.Println("Perimeter:", rect.Perimeter()) // 출력: Perimeter: 30
    
    rect.Scale(2)
    fmt.Println("After scale:", rect) 
    // 출력: After scale: {20 10}
}

2. 인터페이스 구현

type Shape interface {
    Area() float64
    Perimeter() float64
}

type Circle struct {
    Radius float64
}

func (c Circle) Area() float64 {
    return 3.14159 * c.Radius * c.Radius
}

func (c Circle) Perimeter() float64 {
    return 2 * 3.14159 * c.Radius
}

func printShapeInfo(s Shape) {
    fmt.Printf("Area: %.2f, Perimeter: %.2f\n", s.Area(), s.Perimeter())
}

func main() {
    rect := Rectangle{Width: 10, Height: 5}
    circle := Circle{Radius: 7}
    
    printShapeInfo(rect)   // Rectangle도 Shape 인터페이스 구현
    printShapeInfo(circle) // Circle도 Shape 인터페이스 구현
}

Pass by Value vs Pass by Reference

// 값 전달: 복사본이 전달됨
func modifyValue(x int) {
    x = 100
}

// 포인터 전달: 원본을 수정 가능
func modifyPointer(x *int) {
    *x = 100
}

// 슬라이스는 참조 타입처럼 동작 (내부적으로 포인터 포함)
func modifySlice(s []int) {
    s[0] = 100 // 원본 슬라이스 수정됨
}

// 슬라이스 자체를 재할당하면 원본에 영향 없음
func reassignSlice(s []int) {
    s = append(s, 99) // 새 슬라이스 생성, 원본 변경 안 됨
}

// 슬라이스를 수정하려면 포인터 사용
func appendSlice(s *[]int, value int) {
    *s = append(*s, value)
}

func main() {
    x := 10
    modifyValue(x)
    fmt.Println(x) // 출력: 10 (변경 안 됨)
    
    modifyPointer(&x)
    fmt.Println(x) // 출력: 100 (변경됨)
    
    slice := []int{1, 2, 3}
    modifySlice(slice)
    fmt.Println(slice) // 출력: [100 2 3]
    
    reassignSlice(slice)
    fmt.Println(slice) // 출력: [100 2 3] (변경 안 됨)
    
    appendSlice(&slice, 99)
    fmt.Println(slice) // 출력: [100 2 3 99]
}

고급 패턴

1. 함수형 옵션 패턴

type Server struct {
    Host    string
    Port    int
    Timeout int
}

type Option func(*Server)

func WithHost(host string) Option {
    return func(s *Server) {
        s.Host = host
    }
}

func WithPort(port int) Option {
    return func(s *Server) {
        s.Port = port
    }
}

func WithTimeout(timeout int) Option {
    return func(s *Server) {
        s.Timeout = timeout
    }
}

func NewServer(opts ...Option) *Server {
    // 기본값 설정
    server := &Server{
        Host:    "localhost",
        Port:    8080,
        Timeout: 30,
    }
    
    // 옵션 적용
    for _, opt := range opts {
        opt(server)
    }
    
    return server
}

func main() {
    // 유연한 생성자
    server1 := NewServer()
    server2 := NewServer(WithPort(9000))
    server3 := NewServer(WithHost("0.0.0.0"), WithPort(3000), WithTimeout(60))
    
    fmt.Printf("%+v\n", server1) // {Host:localhost Port:8080 Timeout:30}
    fmt.Printf("%+v\n", server2) // {Host:localhost Port:9000 Timeout:30}
    fmt.Printf("%+v\n", server3) // {Host:0.0.0.0 Port:3000 Timeout:60}
}

2. 에러 처리 패턴

// 에러 래핑
func processData(data string) error {
    if err := validateData(data); err != nil {
        return fmt.Errorf("validation failed: %w", err)
    }
    
    if err := saveData(data); err != nil {
        return fmt.Errorf("save failed: %w", err)
    }
    
    return nil
}

// 에러 체이닝 헬퍼
type ErrorHandler struct {
    err error
}

func (e *ErrorHandler) Do(fn func() error) *ErrorHandler {
    if e.err != nil {
        return e
    }
    e.err = fn()
    return e
}

func (e *ErrorHandler) Error() error {
    return e.err
}

func processWithChaining() error {
    eh := &ErrorHandler{}
    return eh.
        Do(func() error { return step1() }).
        Do(func() error { return step2() }).
        Do(func() error { return step3() }).
        Error()
}

3. 메모이제이션 (Memoization)

func fibonacci() func(int) int {
    cache := make(map[int]int)
    
    var fib func(int) int
    fib = func(n int) int {
        if n <= 1 {
            return n
        }
        
        if val, ok := cache[n]; ok {
            return val
        }
        
        result := fib(n-1) + fib(n-2)
        cache[n] = result
        return result
    }
    
    return fib
}

func main() {
    fib := fibonacci()
    
    fmt.Println(fib(10)) // 출력: 55
    fmt.Println(fib(20)) // 출력: 6765 (캐시 활용으로 빠름)
}

4. 파이프라인 패턴

func square(in <-chan int) <-chan int {
    out := make(chan int)
    go func() {
        defer close(out)
        for n := range in {
            out <- n * n
        }
    }()
    return out
}

func double(in <-chan int) <-chan int {
    out := make(chan int)
    go func() {
        defer close(out)
        for n := range in {
            out <- n * 2
        }
    }()
    return out
}

func main() {
    // 채널 생성
    in := make(chan int)
    
    // 파이프라인 구성
    c1 := square(in)
    c2 := double(c1)
    
    // 데이터 전송
    go func() {
        for i := 1; i <= 5; i++ {
            in <- i
        }
        close(in)
    }()
    
    // 결과 수신
    for n := range c2 {
        fmt.Println(n) // 2, 8, 18, 32, 50
    }
}

성능 최적화

1. 인라인 힌트

// 컴파일러에게 인라인 힌트 제공 (작은 함수)
//go:inline
func add(a, b int) int {
    return a + b
}

// 인라인 방지 (디버깅/프로파일링 시)
//go:noinline
func complexOperation(data []int) int {
    // 복잡한 연산...
    return 0
}

2. 슬라이스 사전 할당

// 비효율적
func inefficient(n int) []int {
    var result []int
    for i := 0; i < n; i++ {
        result = append(result, i) // 재할당 발생
    }
    return result
}

// 효율적
func efficient(n int) []int {
    result := make([]int, 0, n) // 용량 사전 할당
    for i := 0; i < n; i++ {
        result = append(result, i)
    }
    return result
}

3. 큰 구조체는 포인터로 전달

type LargeStruct struct {
    data [1000]int
}

// 비효율적: 4KB 복사
func processByValue(ls LargeStruct) {
    // ...
}

// 효율적: 8바이트 포인터 전달
func processByPointer(ls *LargeStruct) {
    // ...
}

일반적인 실수

1. 반복문에서 goroutine 클로저 (Go 1.21 이전)

// 잘못된 예 (Go 1.21 이전)
func wrongGoroutines() {
    for i := 0; i < 5; i++ {
        go func() {
            fmt.Print(i, " ") // 모든 goroutine이 같은 i 참조
        }()
    }
    time.Sleep(time.Second)
    // 예측 불가능한 출력 (아마도: 5 5 5 5 5)
}

// 올바른 예 1: 인자로 전달
func correctGoroutines1() {
    for i := 0; i < 5; i++ {
        go func(n int) {
            fmt.Print(n, " ")
        }(i)
    }
    time.Sleep(time.Second)
}

// 올바른 예 2: 새 변수 생성 (Go 1.21 이전)
func correctGoroutines2() {
    for i := 0; i < 5; i++ {
        i := i // 새 변수
        go func() {
            fmt.Print(i, " ")
        }()
    }
    time.Sleep(time.Second)
}

2. defer에서 에러 무시

// 잘못된 예
func badDefer() {
    file, _ := os.Create("test.txt")
    defer file.Close() // Close의 에러 무시됨
}

// 올바른 예
func goodDefer() (err error) {
    file, err := os.Create("test.txt")
    if err != nil {
        return err
    }
    defer func() {
        closeErr := file.Close()
        if err == nil {
            err = closeErr // 다른 에러가 없을 때만 close 에러 반환
        }
    }()
    
    // 파일 작업...
    return nil
}

3. Named return과 shadowing

// 주의: 변수 섀도잉
func shadowingIssue() (result int, err error) {
    result = 10
    
    // 잘못된 예: := 사용으로 새 err 변수 생성
    if data, err := getData(); err != nil {
        return 0, err // 반환되는 err는 외부 err가 아님
    } else {
        result = data
    }
    
    return result, nil // err는 여전히 nil
}

// 올바른 예
func noShadowing() (result int, err error) {
    result = 10
    
    var data int
    data, err = getData() // = 사용
    if err != nil {
        return 0, err
    }
    
    result = data
    return result, nil
}

실전 예제

package main

import (
    "fmt"
    "strings"
)

// 1. 고차 함수로 문자열 변환
func transformStrings(strings []string, fn func(string) string) []string {
    result := make([]string, len(strings))
    for i, s := range strings {
        result[i] = fn(s)
    }
    return result
}

// 2. 제네릭 맵 함수 (Go 1.18+)
func Map[T, U any](slice []T, fn func(T) U) []U {
    result := make([]U, len(slice))
    for i, v := range slice {
        result[i] = fn(v)
    }
    return result
}

// 3. Reduce 함수
func Reduce[T, U any](slice []T, initial U, fn func(U, T) U) U {
    result := initial
    for _, v := range slice {
        result = fn(result, v)
    }
    return result
}

func main() {
    words := []string{"hello", "world", "go"}
    
    // 대문자 변환
    upper := transformStrings(words, strings.ToUpper)
    fmt.Println(upper) // [HELLO WORLD GO]
    
    // 제네릭 맵 사용
    lengths := Map(words, func(s string) int {
        return len(s)
    })
    fmt.Println(lengths) // [5 5 2]
    
    // Reduce로 합계
    numbers := []int{1, 2, 3, 4, 5}
    sum := Reduce(numbers, 0, func(acc, n int) int {
        return acc + n
    })
    fmt.Println(sum) // 15
}

정리

  • Go 함수는 일급 객체로 변수 할당, 인자 전달, 반환 가능
  • 다중 반환과 Named return으로 명확한 에러 처리
  • 가변 인자로 유연한 API 설계
  • 클로저로 상태 캡처 및 고차 함수 구현
  • defer로 리소스 정리 보장 (LIFO 순서)
  • 메서드로 타입에 동작 부여 (값/포인터 리시버 구분)
  • 함수형 옵션 패턴으로 확장 가능한 생성자
  • 포인터 전달로 성능 최적화
  • Go 1.22부터 반복문 변수 스코핑 개선