일급 함수
함수를 값으로 다루기
Go에서 함수는 일급 객체(first-class citizen) 입니다. 이는 함수를 변수에 저장하고, 다른 함수의 인자로 전달하고, 함수의 반환값으로 사용할 수 있음을 의미합니다.
이 특성은 Go를 함수형 프로그래밍 스타일로도 활용할 수 있게 해주며, 미들웨어, 콜백, 이벤트 핸들러 같은 패턴을 자연스럽게 구현할 수 있게 합니다.
package main
import "fmt"
func add(a, b int) int { return a + b }
func sub(a, b int) int { return a - b }
func mul(a, b int) int { return a * b }
func main() {
// 함수를 변수에 저장
var op func(int, int) int
op = add
fmt.Println("add(3, 4) =", op(3, 4))
op = sub
fmt.Println("sub(3, 4) =", op(3, 4))
op = mul
fmt.Println("mul(3, 4) =", op(3, 4))
// 함수를 맵에 저장 — 디스패치 테이블
ops := map[string]func(int, int) int{
"+": add,
"-": sub,
"*": mul,
}
for _, symbol := range []string{"+", "-", "*"} {
fmt.Printf("10 %s 3 = %d\n", symbol, ops[symbol](10, 3))
}
}
실행 결과:
add(3, 4) = 7
sub(3, 4) = -1
mul(3, 4) = 12
10 + 3 = 13
10 - 3 = 7
10 * 3 = 30
함수 타입은 func(파라미터 타입들) 반환 타입 형태입니다. 파라미터 이름은 타입에 포함되지 않습니다.
함수 타입 선언
type 키워드로 함수 타입에 이름을 붙이면 코드가 훨씬 읽기 쉬워집니다. 이는 특히 함수 시그니처가 복잡할 때 유용합니다.
package main
import (
"fmt"
"net/http"
)
// 함수 타입 선언
type MathFunc func(float64, float64) float64
type Predicate func(int) bool
type Transformer func(string) string
// 표준 라이브러리 스타일: http.HandlerFunc
// type HandlerFunc func(ResponseWriter, *Request)
func applyMath(a, b float64, fn MathFunc) float64 {
return fn(a, b)
}
func filter(nums []int, pred Predicate) []int {
result := []int{}
for _, n := range nums {
if pred(n) {
result = append(result, n)
}
}
return result
}
func transform(strs []string, fn Transformer) []string {
result := make([]string, len(strs))
for i, s := range strs {
result[i] = fn(s)
}
return result
}
func main() {
// MathFunc 타입 활용
divide := MathFunc(func(a, b float64) float64 {
if b == 0 {
return 0
}
return a / b
})
fmt.Println(applyMath(10, 3, divide))
// Predicate 타입 활용
isEven := Predicate(func(n int) bool { return n%2 == 0 })
isPositive := Predicate(func(n int) bool { return n > 0 })
nums := []int{-3, -2, -1, 0, 1, 2, 3, 4, 5}
fmt.Println("짝수:", filter(nums, isEven))
fmt.Println("양수:", filter(nums, isPositive))
// Transformer 타입 활용
words := []string{"go", "is", "awesome"}
upper := Transformer(func(s string) string {
result := ""
for _, c := range s {
if c >= 'a' && c <= 'z' {
result += string(c - 32)
} else {
result += string(c)
}
}
return result
})
fmt.Println(transform(words, upper))
// http.HandlerFunc는 함수 타입 변환의 대표적 예
var handler http.Handler = http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
fmt.Fprintln(w, "Hello")
})
_ = handler
fmt.Println("http.HandlerFunc 타입 변환 성공")
}
실행 결과:
3.3333333333333335
짝수: [-2 0 2 4]
양수: [1 2 3 4 5]
[GO IS AWESOME]
http.HandlerFunc 타입 변환 성공
함수를 파라미터로 전달 — 고차 함수
다른 함수를 인자로 받는 함수를 고차 함수(higher-order function) 라고 합니다. Go 표준 라이브러리의 sort.Slice가 대표적인 예입니다.
package main
import (
"fmt"
"sort"
)
type Person struct {
Name string
Age int
City string
}
// 범용 정렬 — 비교 함수를 파라미터로 받음
func sortPeople(people []Person, less func(a, b Person) bool) {
sort.Slice(people, func(i, j int) bool {
return less(people[i], people[j])
})
}
// 함수를 받아서 실행 시간을 측정
func measure(name string, fn func()) {
fmt.Printf("[%s] 시작\n", name)
fn()
fmt.Printf("[%s] 완료\n", name)
}
// 함수를 받아 에러를 처리하는 래퍼
func withErrorHandling(fn func() error) {
if err := fn(); err != nil {
fmt.Println("에러 발생:", err)
}
}
// 재시도 로직 — 함수를 파라미터로 받아 반복 실행
func retry(maxAttempts int, fn func(attempt int) error) error {
for i := 1; i <= maxAttempts; i++ {
if err := fn(i); err == nil {
return nil
} else if i < maxAttempts {
fmt.Printf("시도 %d 실패: %v, 재시도...\n", i, err)
} else {
return fmt.Errorf("최대 시도 횟수 초과 (%d회): %w", maxAttempts, err)
}
}
return nil
}
func main() {
people := []Person{
{"Alice", 30, "서울"},
{"Bob", 25, "부산"},
{"Charlie", 35, "서울"},
{"Diana", 28, "대구"},
}
// 나이 오름차순 정렬
sortPeople(people, func(a, b Person) bool {
return a.Age < b.Age
})
fmt.Println("나이 순:")
for _, p := range people {
fmt.Printf(" %s (%d)\n", p.Name, p.Age)
}
// 이름 사전순 정렬
sortPeople(people, func(a, b Person) bool {
return a.Name < b.Name
})
fmt.Println("이름 순:")
for _, p := range people {
fmt.Printf(" %s\n", p.Name)
}
// measure 활용
measure("데이터 처리", func() {
sum := 0
for i := 0; i < 1000000; i++ {
sum += i
}
fmt.Println(" 합계:", sum)
})
// sort.Slice 직접 활용
scores := []int{85, 42, 93, 67, 78, 51}
sort.Slice(scores, func(i, j int) bool {
return scores[i] > scores[j] // 내림차순
})
fmt.Println("점수 내림차순:", scores)
}
실행 결과:
나이 순:
Bob (25)
Diana (28)
Alice (30)
Charlie (35)
이름 순:
Alice
Bob
Charlie
Diana
[데이터 처리] 시작
합계: 499999500000
[데이터 처리] 완료
점수 내림차순: [93 85 78 67 51 42]
함수를 반환하는 함수 — 함수 팩토리
함수가 함수를 반환하면 함수 팩토리(function factory) 패턴을 구현할 수 있습니다. 이를 통해 특정 설정이나 상태를 가진 함수를 동적으로 생성할 수 있습니다.
package main
import (
"fmt"
"strings"
)
// 곱셈기 팩토리 — factor가 설정된 함수를 반환
func multiplierFactory(factor int) func(int) int {
return func(n int) int {
return n * factor
}
}
// 접두사 추가기 팩토리
func prefixerFactory(prefix string) func(string) string {
return func(s string) string {
return prefix + s
}
}
// 범위 검사기 팩토리
func rangeCheckerFactory(min, max int) func(int) bool {
return func(n int) bool {
return n >= min && n <= max
}
}
// 파이프라인: 여러 변환 함수를 순서대로 적용
func pipeline(fns ...func(string) string) func(string) string {
return func(s string) string {
for _, fn := range fns {
s = fn(s)
}
return s
}
}
// 로거 팩토리 — 레벨이 설정된 로그 함수 반환
func loggerFactory(level string) func(string, ...any) {
prefix := fmt.Sprintf("[%s] ", strings.ToUpper(level))
return func(format string, args ...any) {
msg := fmt.Sprintf(format, args...)
fmt.Println(prefix + msg)
}
}
func main() {
// 곱셈기 생성
double := multiplierFactory(2)
triple := multiplierFactory(3)
times10 := multiplierFactory(10)
fmt.Println(double(5)) // 10
fmt.Println(triple(5)) // 15
fmt.Println(times10(5)) // 50
// 접두사 추가기 생성
addHTTPS := prefixerFactory("https://")
addWWW := prefixerFactory("www.")
fmt.Println(addHTTPS("example.com"))
fmt.Println(addWWW("example.com"))
// 범위 검사기
isValidAge := rangeCheckerFactory(0, 150)
isValidPort := rangeCheckerFactory(1, 65535)
fmt.Println("나이 25 유효?", isValidAge(25))
fmt.Println("나이 200 유효?", isValidAge(200))
fmt.Println("포트 8080 유효?", isValidPort(8080))
fmt.Println("포트 0 유효?", isValidPort(0))
// 파이프라인 구성
trim := func(s string) string { return strings.TrimSpace(s) }
lower := func(s string) string { return strings.ToLower(s) }
addDot := func(s string) string {
if !strings.HasSuffix(s, ".") {
return s + "."
}
return s
}
normalize := pipeline(trim, lower, addDot)
inputs := []string{" Hello World ", "GO PROGRAMMING", " 기본값 "}
for _, input := range inputs {
fmt.Printf("%q → %q\n", input, normalize(input))
}
// 로거 팩토리
info := loggerFactory("info")
warn := loggerFactory("warn")
errLog := loggerFactory("error")
info("서버가 포트 %d에서 시작됩니다", 8080)
warn("메모리 사용량이 %d%%에 도달했습니다", 80)
errLog("데이터베이스 연결 실패: %s", "timeout")
}
실행 결과:
10
15
50
https://example.com
www.example.com
나이 25 유효? true
나이 200 유효? false
포트 8080 유효? true
포트 0 유효? false
" Hello World " → "hello world."
"GO PROGRAMMING" → "go programming."
" 기본값 " → "기본값."
[INFO] 서버가 포트 8080에서 시작됩니다
[WARN] 메모리 사용량이 80%에 도달했습니다
[ERROR] 데이터베이스 연결 실패: timeout
익명 함수와 즉시 실행 함수
이름 없는 익명 함수(anonymous function) 는 선언과 동시에 실행하거나, 변수에 저장하거나, 다른 함수의 인자로 바로 전달할 수 있습니다.
package main
import "fmt"
func main() {
// 즉시 실행 함수 (IIFE — Immediately Invoked Function Expression)
result := func(a, b int) int {
return a + b
}(10, 20)
fmt.Println("즉시 실행:", result)
// 익명 함수를 변수에 저장
square := func(n int) int {
return n * n
}
fmt.Println("제곱:", square(7))
// 고루틴에서 익명 함수 활용
done := make(chan bool)
go func() {
fmt.Println("고루틴에서 실행되는 익명 함수")
done <- true
}()
<-done
// 슬라이스 내 함수 목록
transforms := []func(int) int{
func(n int) int { return n + 1 },
func(n int) int { return n * 2 },
func(n int) int { return n * n },
}
n := 3
for _, fn := range transforms {
fmt.Printf("변환 결과: %d\n", fn(n))
}
// 재귀 익명 함수 — 변수에 먼저 선언한 후 본문에서 참조
var factorial func(int) int
factorial = func(n int) int {
if n <= 1 {
return 1
}
return n * factorial(n-1)
}
fmt.Println("5! =", factorial(5))
}
실행 결과:
즉시 실행: 30
제곱: 49
고루틴에서 실행되는 익명 함수
변환 결과: 4
변환 결과: 6
변환 결과: 9
5! = 120
재귀 익명 함수를 작성할 때는 반드시 변수를 먼저 선언(var factorial func(int) int)한 뒤 대입해야 합니다. 그렇지 않으면 함수 본문 안에서 자기 자신을 참조할 수 없습니다.
실전 예제: 미들웨어 패턴
함수를 값으로 다루는 가장 강력한 실전 활용 중 하나는 HTTP 미들웨어 패턴입니다. 각 미들웨어는 핸들러를 받아 새로운 핸들러를 반환합니다.
package main
import (
"fmt"
"net/http"
"time"
)
// 미들웨어 타입 정의
type Middleware func(http.HandlerFunc) http.HandlerFunc
// 로깅 미들웨어
func loggingMiddleware(next http.HandlerFunc) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
start := time.Now()
fmt.Printf("[%s] %s %s\n", time.Now().Format("15:04:05"), r.Method, r.URL.Path)
next(w, r)
fmt.Printf("[%s] 완료 (%v)\n", time.Now().Format("15:04:05"), time.Since(start))
}
}
// 인증 미들웨어
func authMiddleware(next http.HandlerFunc) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
token := r.Header.Get("Authorization")
if token != "Bearer secret-token" {
http.Error(w, "인증 실패", http.StatusUnauthorized)
return
}
next(w, r)
}
}
// 복구 미들웨어 — 패닉 처리
func recoveryMiddleware(next http.HandlerFunc) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
defer func() {
if err := recover(); err != nil {
fmt.Println("패닉 복구:", err)
http.Error(w, "서버 에러", http.StatusInternalServerError)
}
}()
next(w, r)
}
}
// 미들웨어 체이닝 — 오른쪽에서 왼쪽으로 적용
func chain(h http.HandlerFunc, middlewares ...Middleware) http.HandlerFunc {
for i := len(middlewares) - 1; i >= 0; i-- {
h = middlewares[i](h)
}
return h
}
// 실제 핸들러
func apiHandler(w http.ResponseWriter, r *http.Request) {
fmt.Fprintln(w, `{"status":"ok","message":"API 응답"}`)
}
func main() {
// 미들웨어 체인 적용
handler := chain(
apiHandler,
recoveryMiddleware,
loggingMiddleware,
authMiddleware,
)
mux := http.NewServeMux()
mux.HandleFunc("/api", handler)
fmt.Println("미들웨어 패턴 데모")
fmt.Println("서버 시작: http://localhost:8080")
fmt.Println("테스트 명령:")
fmt.Println(` curl -H "Authorization: Bearer secret-token" http://localhost:8080/api`)
fmt.Println(` curl http://localhost:8080/api # 인증 실패`)
if err := http.ListenAndServe(":8080", mux); err != nil {
fmt.Println("서버 에러:", err)
}
}
이 패턴은 실제 프로덕션 Go 웹 서버에서 광범위하게 사용됩니다. 각 미들웨어가 독립적으로 관심사를 분리하고, 체인으로 조합할 수 있다는 점이 핵심입니다.
실전 예제: 이벤트 핸들러 시스템
package main
import "fmt"
// 이벤트 핸들러 타입
type EventHandler func(data any)
// 이벤트 버스
type EventBus struct {
handlers map[string][]EventHandler
}
func NewEventBus() *EventBus {
return &EventBus{handlers: make(map[string][]EventHandler)}
}
// 핸들러 등록
func (eb *EventBus) On(event string, handler EventHandler) {
eb.handlers[event] = append(eb.handlers[event], handler)
}
// 이벤트 발생
func (eb *EventBus) Emit(event string, data any) {
for _, handler := range eb.handlers[event] {
handler(data)
}
}
// 한 번만 실행되는 핸들러
func (eb *EventBus) Once(event string, handler EventHandler) {
var wrapper EventHandler
wrapper = func(data any) {
handler(data)
// 자기 자신을 제거 (간단한 구현)
handlers := eb.handlers[event]
for i, h := range handlers {
// 함수 포인터 비교는 불가능하므로 실행 후 첫 번째 제거
_ = h
eb.handlers[event] = append(handlers[:i], handlers[i+1:]...)
break
}
_ = wrapper
}
eb.On(event, wrapper)
}
func main() {
bus := NewEventBus()
// 여러 핸들러 등록
bus.On("user.login", func(data any) {
fmt.Printf("로그인 감지: %v\n", data)
})
bus.On("user.login", func(data any) {
fmt.Printf("로그인 감사 로그 기록: %v\n", data)
})
bus.On("user.logout", func(data any) {
fmt.Printf("로그아웃: %v\n", data)
})
// 이벤트 발생
bus.Emit("user.login", map[string]string{"user": "alice", "ip": "127.0.0.1"})
bus.Emit("user.login", map[string]string{"user": "bob", "ip": "192.168.1.1"})
bus.Emit("user.logout", "alice")
bus.Emit("user.purchase", "결제 완료") // 핸들러 없음 — 무시됨
}
실행 결과:
로그인 감지: map[ip:127.0.0.1 user:alice]
로그인 감사 로그 기록: map[ip:127.0.0.1 user:alice]
로그인 감지: map[ip:192.168.1.1 user:bob]
로그인 감사 로그 기록: map[ip:192.168.1.1 user:bob]
로그아웃: alice
고수 팁
함수 타입으로 인터페이스 대체: 단일 메서드 인터페이스는 함수 타입으로 대체하는 것이 더 관용적입니다. http.Handler 인터페이스 대신 http.HandlerFunc 타입을 사용하는 것이 그 예입니다. 인터페이스는 여러 메서드가 필요할 때 사용하세요.
함수 합성의 순서: 미들웨어 체이닝에서 적용 순서가 중요합니다. 바깥쪽에서 안쪽 순서로 실행됩니다. 요청 처리 순서와 응답 처리 순서가 반대임을 항상 염두에 두세요.
nil 함수 체크: 함수 타입 변수의 기본값은 nil입니다. 함수를 호출하기 전에 if fn != nil 체크를 습관화하세요. nil 함수를 호출하면 패닉이 발생합니다.