[Go] function
개요
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부터 반복문 변수 스코핑 개선