Updated:

11 minute read

개요

가변 인자 함수(Variadic Function)는 임의 개수의 인자를 받을 수 있는 함수입니다.

주요 특징:

  • 타입 앞에 생략 부호(...)를 붙여서 선언
  • 함수 내부에서 슬라이스로 처리됨
  • 반드시 매개변수 목록의 마지막에 위치해야 함
  • 슬라이스 언팩(unpacking)으로 전달 가능

기본 사용법

1. 기본 가변 인자 함수

package main

import "fmt"

// 가변 인자는 내부적으로 []int 슬라이스로 처리됨
func sum(numbers ...int) int {
    total := 0
    for _, num := range numbers {
        total += num
    }
    return total
}

func main() {
    // 0개 인자
    fmt.Println(sum())              // 출력: 0
    
    // 1개 인자
    fmt.Println(sum(1))             // 출력: 1
    
    // 여러 인자
    fmt.Println(sum(1, 2, 3, 4, 5)) // 출력: 15
    
    // 슬라이스 언팩
    numbers := []int{10, 20, 30}
    fmt.Println(sum(numbers...))    // 출력: 60
}

2. 일반 매개변수와 혼합

// 가변 인자는 반드시 마지막에 위치
func greet(prefix string, names ...string) string {
    if len(names) == 0 {
        return prefix + " nobody"
    }
    
    result := prefix
    for i, name := range names {
        if i > 0 {
            result += ","
        }
        result += " " + name
    }
    return result
}

func main() {
    fmt.Println(greet("Hello"))                    // Hello nobody
    fmt.Println(greet("Hello", "Alice"))           // Hello Alice
    fmt.Println(greet("Hello", "Alice", "Bob"))    // Hello Alice, Bob
    fmt.Println(greet("Hi", "A", "B", "C", "D"))   // Hi A, B, C, D
}

3. 다양한 타입의 가변 인자

// 문자열 가변 인자
func concat(separator string, words ...string) string {
    result := ""
    for i, word := range words {
        if i > 0 {
            result += separator
        }
        result += word
    }
    return result
}

// interface{} 또는 any를 사용한 범용 가변 인자
func printAll(args ...interface{}) {
    for i, arg := range args {
        fmt.Printf("Arg %d: %v (type: %T)\n", i, arg, arg)
    }
}

// Go 1.18+ any 타입 사용
func printAllAny(args ...any) {
    for i, arg := range args {
        fmt.Printf("Arg %d: %v\n", i, arg)
    }
}

func main() {
    fmt.Println(concat("-", "Go", "is", "awesome"))
    // 출력: Go-is-awesome
    
    printAll(1, "hello", 3.14, true, []int{1, 2, 3})
    // 출력:
    // Arg 0: 1 (type: int)
    // Arg 1: hello (type: string)
    // Arg 2: 3.14 (type: float64)
    // Arg 3: true (type: bool)
    // Arg 4: [1 2 3] (type: []int)
}

슬라이스 언팩 (Unpacking)

1. 기본 언팩

func multiply(factors ...int) int {
    if len(factors) == 0 {
        return 0
    }
    result := 1
    for _, f := range factors {
        result *= f
    }
    return result
}

func main() {
    numbers := []int{2, 3, 4}
    
    // 슬라이스를 개별 인자로 언팩
    result := multiply(numbers...)
    fmt.Println(result) // 출력: 24
    
    // 추가 인자와 함께 사용 불가 (컴파일 에러)
    // result = multiply(1, numbers..., 5) // 에러!
    // result = multiply(numbers..., 5)     // 에러!
    
    // 여러 슬라이스 병합 후 언팩
    more := []int{5, 6}
    combined := append(numbers, more...)
    result = multiply(combined...)
    fmt.Println(result) // 출력: 1440
}

2. 슬라이스 복사 vs 언팩

func modifyArgs(args ...int) {
    if len(args) > 0 {
        args[0] = 999 // 슬라이스 내부 수정
    }
}

func main() {
    // 직접 전달: 새 슬라이스 생성
    a, b, c := 1, 2, 3
    modifyArgs(a, b, c)
    fmt.Println(a, b, c) // 출력: 1 2 3 (변경 안 됨)
    
    // 슬라이스 언팩: 원본 슬라이스 공유
    slice := []int{1, 2, 3}
    modifyArgs(slice...)
    fmt.Println(slice) // 출력: [999 2 3] (변경됨!)
}

실전 활용 패턴

1. fmt 패키지 스타일 포맷팅

import "strings"

// fmt.Printf, fmt.Sprintf와 유사한 패턴
func formatLog(level, format string, args ...interface{}) string {
    message := fmt.Sprintf(format, args...)
    return fmt.Sprintf("[%s] %s", strings.ToUpper(level), message)
}

func main() {
    log := formatLog("info", "User %s logged in at %d", "Alice", 14)
    fmt.Println(log)
    // 출력: [INFO] User Alice logged in at 14
    
    log = formatLog("error", "Failed to connect to %s:%d", "localhost", 8080)
    fmt.Println(log)
    // 출력: [ERROR] Failed to connect to localhost:8080
}

2. 빌더 패턴

type QueryBuilder struct {
    query string
}

func (qb *QueryBuilder) Select(columns ...string) *QueryBuilder {
    qb.query = "SELECT "
    if len(columns) == 0 {
        qb.query += "*"
    } else {
        qb.query += strings.Join(columns, ", ")
    }
    return qb
}

func (qb *QueryBuilder) From(table string) *QueryBuilder {
    qb.query += " FROM " + table
    return qb
}

func (qb *QueryBuilder) Where(conditions ...string) *QueryBuilder {
    if len(conditions) > 0 {
        qb.query += " WHERE " + strings.Join(conditions, " AND ")
    }
    return qb
}

func (qb *QueryBuilder) Build() string {
    return qb.query
}

func main() {
    qb := &QueryBuilder{}
    
    query := qb.Select("name", "age").
        From("users").
        Where("age > 18", "active = true").
        Build()
    
    fmt.Println(query)
    // 출력: SELECT name, age FROM users WHERE age > 18 AND active = true
}

3. 함수형 옵션 패턴

type Server struct {
    host    string
    port    int
    timeout int
    logger  Logger
}

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() {
    // 유연한 설정
    s1 := NewServer()
    s2 := NewServer(WithPort(9000))
    s3 := NewServer(WithHost("0.0.0.0"), WithPort(3000), WithTimeout(60))
    
    fmt.Printf("%+v\n", s1) // {host:localhost port:8080 timeout:30}
    fmt.Printf("%+v\n", s2) // {host:localhost port:9000 timeout:30}
    fmt.Printf("%+v\n", s3) // {host:0.0.0.0 port:3000 timeout:60}
}

4. 로깅 유틸리티

type LogLevel int

const (
    DEBUG LogLevel = iota
    INFO
    WARN
    ERROR
)

type Logger struct {
    level LogLevel
}

func (l *Logger) log(level LogLevel, format string, args ...interface{}) {
    if level >= l.level {
        levelStr := []string{"DEBUG", "INFO", "WARN", "ERROR"}[level]
        message := fmt.Sprintf(format, args...)
        fmt.Printf("[%s] %s\n", levelStr, message)
    }
}

func (l *Logger) Debug(format string, args ...interface{}) {
    l.log(DEBUG, format, args...)
}

func (l *Logger) Info(format string, args ...interface{}) {
    l.log(INFO, format, args...)
}

func (l *Logger) Warn(format string, args ...interface{}) {
    l.log(WARN, format, args...)
}

func (l *Logger) Error(format string, args ...interface{}) {
    l.log(ERROR, format, args...)
}

func main() {
    logger := &Logger{level: INFO}
    
    logger.Debug("This won't be printed")           // 출력 안 됨
    logger.Info("Server started on port %d", 8080)  // [INFO] Server started on port 8080
    logger.Warn("High memory usage: %d%%", 85)      // [WARN] High memory usage: 85%
    logger.Error("Failed to connect: %v", errors.New("timeout"))
    // [ERROR] Failed to connect: timeout
}

5. 집합 연산

// 합집합
func union(sets ...[]int) []int {
    seen := make(map[int]bool)
    result := []int{}
    
    for _, set := range sets {
        for _, num := range set {
            if !seen[num] {
                seen[num] = true
                result = append(result, num)
            }
        }
    }
    
    return result
}

// 최소값 찾기
func min(first int, rest ...int) int {
    minimum := first
    for _, num := range rest {
        if num < minimum {
            minimum = num
        }
    }
    return minimum
}

// 최대값 찾기
func max(first int, rest ...int) int {
    maximum := first
    for _, num := range rest {
        if num > maximum {
            maximum = num
        }
    }
    return maximum
}

func main() {
    set1 := []int{1, 2, 3}
    set2 := []int{3, 4, 5}
    set3 := []int{5, 6, 7}
    
    result := union(set1, set2, set3)
    fmt.Println(result) // [1 2 3 4 5 6 7]
    
    fmt.Println(min(5, 2, 8, 1, 9))  // 1
    fmt.Println(max(5, 2, 8, 1, 9))  // 9
}

제네릭과 가변 인자 (Go 1.18+)

// 제네릭 타입 가변 인자
func Contains[T comparable](target T, items ...T) bool {
    for _, item := range items {
        if item == target {
            return true
        }
    }
    return false
}

// 제네릭 최소/최대값
func Min[T constraints.Ordered](first T, rest ...T) T {
    min := first
    for _, v := range rest {
        if v < min {
            min = v
        }
    }
    return min
}

func Max[T constraints.Ordered](first T, rest ...T) T {
    max := first
    for _, v := range rest {
        if v > max {
            max = v
        }
    }
    return max
}

// 제네릭 슬라이스 연결
func Concat[T any](slices ...[]T) []T {
    var totalLen int
    for _, s := range slices {
        totalLen += len(s)
    }
    
    result := make([]T, 0, totalLen)
    for _, s := range slices {
        result = append(result, s...)
    }
    return result
}

func main() {
    // 타입 추론
    fmt.Println(Contains(3, 1, 2, 3, 4))        // true
    fmt.Println(Contains("go", "java", "python")) // false
    
    fmt.Println(Min(5, 2, 8, 1, 9))             // 1
    fmt.Println(Max(5.5, 2.3, 8.7, 1.2))        // 8.7
    
    s1 := []string{"a", "b"}
    s2 := []string{"c", "d"}
    s3 := []string{"e"}
    fmt.Println(Concat(s1, s2, s3))             // [a b c d e]
}

타입 단언과 가변 인자

// interface{} 가변 인자에서 타입 검사
func processValues(values ...interface{}) {
    for i, v := range values {
        switch val := v.(type) {
        case int:
            fmt.Printf("[%d] int: %d (doubled: %d)\n", i, val, val*2)
        case string:
            fmt.Printf("[%d] string: %s (length: %d)\n", i, val, len(val))
        case bool:
            fmt.Printf("[%d] bool: %v\n", i, val)
        case []int:
            fmt.Printf("[%d] []int: %v (sum: %d)\n", i, val, sum(val...))
        default:
            fmt.Printf("[%d] unknown type: %T\n", i, val)
        }
    }
}

// 특정 타입만 허용
func sumIntegers(values ...interface{}) (int, error) {
    total := 0
    for i, v := range values {
        num, ok := v.(int)
        if !ok {
            return 0, fmt.Errorf("argument %d is not an integer: %T", i, v)
        }
        total += num
    }
    return total, nil
}

func main() {
    processValues(42, "hello", true, []int{1, 2, 3}, 3.14)
    // [0] int: 42 (doubled: 84)
    // [1] string: hello (length: 5)
    // [2] bool: true
    // [3] []int: [1 2 3] (sum: 6)
    // [4] unknown type: float64
    
    result, err := sumIntegers(1, 2, 3, 4, 5)
    fmt.Println(result, err) // 15 <nil>
    
    result, err = sumIntegers(1, "2", 3)
    fmt.Println(result, err) // 0 argument 1 is not an integer: string
}

성능 고려사항

1. 슬라이스 생성 오버헤드

import "testing"

// 가변 인자: 매번 새 슬라이스 생성
func variadicSum(nums ...int) int {
    total := 0
    for _, n := range nums {
        total += n
    }
    return total
}

// 슬라이스 직접 전달: 슬라이스 재사용 가능
func sliceSum(nums []int) int {
    total := 0
    for _, n := range nums {
        total += n
    }
    return total
}

func BenchmarkVariadic(b *testing.B) {
    for i := 0; i < b.N; i++ {
        variadicSum(1, 2, 3, 4, 5) // 매번 새 슬라이스 할당
    }
}

func BenchmarkSlice(b *testing.B) {
    nums := []int{1, 2, 3, 4, 5}
    for i := 0; i < b.N; i++ {
        sliceSum(nums) // 슬라이스 재사용
    }
}

// 결과: 슬라이스 직접 전달이 약 20-30% 빠름

2. 슬라이스 언팩 사용 시점

func process(items ...string) {
    // 처리...
}

func main() {
    items := []string{"a", "b", "c"}
    
    // 이미 슬라이스라면 언팩 사용 (추가 할당 없음)
    process(items...)
    
    // 개별 값이라면 직접 전달
    process("x", "y", "z")
}

일반적인 실수와 주의사항

1. 가변 인자 위치 제한

// ❌ 컴파일 에러: 가변 인자는 마지막에만 위치 가능
// func invalid(args ...int, suffix string) {}

// ✅ 올바른 사용
func valid(prefix string, args ...int) {}

// ✅ 여러 일반 매개변수 후 가변 인자
func alsoValid(a string, b int, c bool, args ...int) {}

2. 빈 가변 인자 처리

func process(items ...string) {
    // ❌ nil 체크 불필요 (항상 빈 슬라이스라도 생성됨)
    // if items == nil { ... }
    
    // ✅ 길이 체크
    if len(items) == 0 {
        fmt.Println("No items provided")
        return
    }
    
    for _, item := range items {
        fmt.Println(item)
    }
}

func main() {
    process()           // items는 빈 슬라이스 []string{}
    process("a", "b")   // items는 []string{"a", "b"}
}

3. 슬라이스 수정 주의

func doubleValues(nums ...int) {
    for i := range nums {
        nums[i] *= 2
    }
}

func main() {
    // 직접 전달: 새 슬라이스이므로 원본 변경 안 됨
    a, b, c := 1, 2, 3
    doubleValues(a, b, c)
    fmt.Println(a, b, c) // 1 2 3
    
    // 슬라이스 언팩: 같은 underlying array 공유
    slice := []int{1, 2, 3}
    doubleValues(slice...)
    fmt.Println(slice) // [2 4 6] (변경됨!)
    
    // 원본 보호하려면 복사 후 전달
    original := []int{1, 2, 3}
    copy := append([]int{}, original...)
    doubleValues(copy...)
    fmt.Println(original) // [1 2 3] (변경 안 됨)
}

4. interface{} 남용 주의

// ❌ 타입 안전성 없음
func badAdd(args ...interface{}) interface{} {
    // 런타임 타입 체크 필요
}

// ✅ 제네릭 사용 (Go 1.18+)
func Add[T constraints.Ordered](a, b T) T {
    return a + b
}

// ✅ 또는 명확한 타입 지정
func addInts(nums ...int) int {
    total := 0
    for _, n := range nums {
        total += n
    }
    return total
}

5. append와 가변 인자

func main() {
    slice := []int{1, 2, 3}
    
    // ❌ 잘못된 사용
    // slice = append(slice, []int{4, 5, 6}) // 컴파일 에러
    
    // ✅ 올바른 사용
    slice = append(slice, 4, 5, 6)
    
    // ✅ 또는 슬라이스 언팩
    more := []int{7, 8, 9}
    slice = append(slice, more...)
    
    fmt.Println(slice) // [1 2 3 4 5 6 7 8 9]
}

실전 예제 모음

package main

import (
    "fmt"
    "strings"
)

// 1. CSV 생성기
func makeCSV(headers []string, rows ...[]string) string {
    lines := []string{strings.Join(headers, ",")}
    for _, row := range rows {
        lines = append(lines, strings.Join(row, ","))
    }
    return strings.Join(lines, "\n")
}

// 2. 평균 계산
func average(numbers ...float64) float64 {
    if len(numbers) == 0 {
        return 0
    }
    sum := 0.0
    for _, n := range numbers {
        sum += n
    }
    return sum / float64(len(numbers))
}

// 3. 문자열 결합 빌더
func join(separator string, parts ...string) string {
    return strings.Join(parts, separator)
}

// 4. 조건부 필터
func filter(predicate func(int) bool, numbers ...int) []int {
    result := []int{}
    for _, n := range numbers {
        if predicate(n) {
            result = append(result, n)
        }
    }
    return result
}

func main() {
    // CSV 생성
    csv := makeCSV(
        []string{"Name", "Age", "City"},
        []string{"Alice", "25", "Seoul"},
        []string{"Bob", "30", "Busan"},
    )
    fmt.Println(csv)
    // Name,Age,City
    // Alice,25,Seoul
    // Bob,30,Busan
    
    // 평균 계산
    fmt.Printf("Average: %.2f\n", average(10, 20, 30, 40, 50))
    // Average: 30.00
    
    // 문자열 결합
    path := join("/", "home", "user", "documents", "file.txt")
    fmt.Println(path) // home/user/documents/file.txt
    
    // 짝수 필터
    evens := filter(func(n int) bool { return n%2 == 0 }, 1, 2, 3, 4, 5, 6)
    fmt.Println(evens) // [2 4 6]
}

정리

  • 가변 인자는 함수 내부에서 슬라이스로 처리됨
  • 반드시 매개변수 목록의 마지막에 위치
  • ... 연산자로 슬라이스 언팩 가능
  • interface{} 또는 any로 범용 가변 인자 구현
  • Go 1.18+ 제네릭으로 타입 안전한 가변 인자 함수 작성
  • 성능이 중요하면 슬라이스 직접 전달 고려
  • 함수형 옵션 패턴의 핵심 메커니즘
  • fmt.Printf, append 등 표준 라이브러리에서 광범위하게 사용