[Go] variadic arguments function
Updated:
개요
가변 인자 함수(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 등 표준 라이브러리에서 광범위하게 사용