Go 함수 정의와 가시성
Go 언어에서 함수는 func 키워드를 사용하여 정의합니다. 기본적인 구조는 매개변수와 반환 타입을 명시하는 형태입니다. Go는 강타입 언어이므로 인자와 반환값 모두 타입을 정확히 선언해야 합니다.
func calculateSum(x int, y int) int {
return x + y
}
// 인자의 타입이 같다면 마지막에만 선언할 수 있습니다.
func calculateProduct(x, y int) int {
return x * y
}
Go에서 함수의 접근 제어는 이름의 첫 글자 대소문자로 결정됩니다. 첫 글자가 대문자(예: Calculate)인 함수는 다른 패키지에서 접근 가능한 Public 상태가 되며, 소문자(예: calculate)인 경우 해당 패키지 내부에서만 사용 가능한 Private 상태가 됩니다.
매개변수 전달 방식
Go는 기본적으로 값에 의한 전달(Pass by Value)을 사용합니다. 함수에 인자를 전달하면 값이 복사되어 전달되므로 함수 내부에서 값을 변경해도 원본 데이터에는 영향을 주지 않습니다.
func updateValue(val int) {
val = val * 10
}
func main() {
num := 5
updateValue(num)
// num은 여전히 5입니다.
}
만약 함수 내부에서 원본 값을 수정해야 한다면 포인터를 사용하여 참조를 전달해야 합니다.
func updateReference(val *int) {
*val = *val * 10
}
func main() {
num := 5
updateReference(&num)
// num은 이제 50이 됩니다.
}
가변 인자와 인터페이스
개수가 정해지지 않은 인자를 받으려면 ... 문법을 사용합니다. 이는 함수 내부에서 슬라이스처럼 동작합니다. interface{} 타입을 사용하면 어떤 타입의 값이라도 인자로 받을 수 있습니다.
func printLogs(messages ...interface{}) {
for _, msg := range messages {
fmt.Printf("Log: %v\n", msg)
}
}
다중 반환값과 명명된 반환값
Go 함수는 여러 개의 값을 동시에 반환할 수 있습니다. 주로 결과값과 에러 객체를 함께 반환할 때 사용합니다. 또한 반환값에 이름을 부여하여 코드 가독성을 높일 수 있습니다.
func divide(a, b float64) (result float64, err error) {
if b == 0 {
err = errors.New("0으로 나눌 수 없습니다")
return // 명명된 반환값 덕분에 변수명을 생략하고 return만 쓸 수 있습니다.
}
result = a / b
return
}
익명 함수와 클로저
이름이 없는 익명 함수는 변수에 할당하거나 즉시 실행할 수 있습니다. 익명 함수가 외부 범위의 변수를 참조하면 클로저(Closure)가 형성됩니다.
func incrementer() func() int {
count := 0
return func() int {
count++
return count
}
}
func main() {
inc := incrementer()
fmt.Println(inc()) // 1
fmt.Println(inc()) // 2
}
고차 함수를 활용한 데코레이터 패턴
함수를 인자로 받거나 반환하는 고차 함수를 활용하면 기존 로직을 수정하지 않고 기능을 확장하는 데코레이터 패턴을 구현할 수 있습니다. 대표적인 사례가 실행 시간 측정입니다.
type WorkFunc func(int) int
func timeTracker(f WorkFunc) WorkFunc {
return func(n int) int {
start := time.Now()
result := f(n)
fmt.Printf("소요 시간: %v\n", time.Since(start))
return result
}
}
func heavyTask(n int) int {
time.Sleep(time.Second)
return n * n
}
func main() {
decoratedTask := timeTracker(heavyTask)
fmt.Println(decoratedTask(10))
}
재귀 함수 최적화: 메모이제이션과 꼬리 재귀
재귀 함수는 복잡한 문제를 단순화하지만 성능 이슈가 발생할 수 있습니다. 피보나치 수열을 예로 들어 최적화 기법을 살펴보겠습니다.
메모이제이션(Memoization)
이미 계산된 값을 배열이나 맵에 저장하여 중복 계산을 방지합니다.
var cache = make(map[int]int)
func fibMemo(n int) int {
if n <= 1 { return n }
if val, ok := cache[n]; ok { return val }
cache[n] = fibMemo(n-1) + fibMemo(n-2)
return cache[n]
}
꼬리 재귀(Tail Recursion)
재귀 호출이 함수의 마지막 동작이 되도록 설계하여 스택 공간을 절약합니다. (Go 컴파일러가 모든 꼬리 재귀를 최적화하지는 않지만, 논리적 구조를 선형적으로 바꿀 수 있습니다.)
func fibTail(n, a, b int) int {
if n == 0 { return a }
return fibTail(n-1, b, a+b)
}
func Fibonacci(n int) int {
return fibTail(n, 0, 1)
}
Map-Filter-Reduce 패턴
함수형 프로그래밍 스타일을 적용하여 데이터를 효율적으로 처리할 수 있습니다.
type Employee struct {
Name string
Salary int
}
// Filter: 조건에 맞는 데이터만 추출
func filterHighSalary(emps []Employee, limit int) []Employee {
var result []Employee
for _, e := range emps {
if e.Salary > limit {
result = append(result, e)
}
}
return result
}
// Map: 데이터 형태 변환
func mapToNames(emps []Employee) []string {
var names []string
for _, e := range emps {
names = append(names, e.Name)
}
return names
}
// Reduce: 데이터를 하나의 값으로 집약
func sumSalaries(emps []Employee) int {
sum := 0
for _, e := range emps {
sum += e.Salary
}
return sum
}
함수 파이프라인 구축
여러 단계의 데이터 처리 로직을 파이프라인 형태로 연결하여 유연한 구조를 만들 수 있습니다.
type PipelineStage func([]Employee) []Employee
func executePipeline(data []Employee, stages ...PipelineStage) []Employee {
for _, stage := range stages {
data = stage(data)
}
return data
}
func main() {
workers := []Employee{{"Kim", 3000}, {"Lee", 5000}, {"Park", 2000}}
// 파이프라인 단계 정의
highPayStage := func(emps []Employee) []Employee {
return filterHighSalary(emps, 2500)
}
result := executePipeline(workers, highPayStage)
fmt.Println("고소득자 목록:", result)
}