개요
이번 블로그 포스트에서는 Golang에서 Function(함수)에 대해 좀 더 자세히 알아보고 함수를 활용하는 다양한 방법에 대해서 알아봅시다. 이 블로그 포스트에서 소개하는 코드는 다음 링크를 통해 확인하실 수 있습니다.
함수 기초
Golang에서 함수를 다루는 기초적인 방법은 이전 블로그 포스트를 확인해 주시기 바랍니다.
가변 인수 함수
Golang에서는 함수에서 다음과 같이 가변 인수(Variable argument list)를 사용할 수 있습니다.
fmt.Println()
fmt.Println(1)
fmt.Println(1, 2, 3, 4, 5)
Golang에서는 다음과 같이 ... 키워드를 사용하여 가변 인수 함수를 만들 수 있습니다.
func FUNCTION_NAME(param ...TYPE) TYPE {
// Code block
}
이를 확인하기 위해 main.go 파일을 생성하고 다음과 같이 수정합니다.
package main
import "fmt"
func sum(nums ...int) int {
fmt.Printf("nums type: %T\n", nums)
sum := 0
for _, num := range nums {
sum += num
}
return sum
}
func main() {
fmt.Println(sum(1, 2, 3, 4, 5))
fmt.Println(sum(10, 20))
fmt.Println(sum())
}
이를 실행하면 다음과 같은 결과를 얻을 수 있습니다.
# go run main.go
nums type: []int
15
nums type: []int
30
nums type: []int
0
defer
Golang은 defer라는 지연 실행 구문을 제공합니다. defer는 함수 종료전에 실행됨을 보장합니다.
defer STATEMENT
이를 확인하기 위해서 main.go 파일을 다음과 같이 수정합니다.
package main
import "fmt"
func print() {
defer fmt.Println("World!")
fmt.Println("Hello")
}
func main() {
print()
}
이를 실행하면 다음과 같은 결과가 출력되는 것을 확인할 수 있습니다.
# go run main.go
Hello
World!
defer는 파일을 읽기 위해 파일을 열고 닫는 등, 주로 OS 자원을 반환할 때 사용됩니다. 이를 확인하기 위해 main.go 파일을 다음과 같이 수정합니다.
package main
import "os"
func readFile() {
f, err := os.Open("file.txt")
if err != nil {
return
}
f.Close()
}
func readFileDefer() {
f, err := os.Open("file.txt")
defer f.Close()
if err != nil {
return
}
}
func main() {
readFile()
readFileDefer()
}
defer를 사용하지 않는 경우, Close 함수 전에 오류가 발생하면, Close 함수는 호출되지 않습니다.
func readFile() {
f, err := os.Open("file.txt")
if err != nil {
return
}
f.Close()
}
하지만 defer를 사용하면 오류가 발생하더라도, Close 함수의 호출이 보장됩니다.
func readFileDefer() {
f, err := os.Open("file.txt")
defer f.Close()
if err != nil {
return
}
}
함수 타입 변수
Golang에서는 함수도 타입으로 사용할 수 있습니다. 여기서 함수 타입 변수란 함수를 값으로 갖는 변수를 말합니다. 함수도 메모리에 시작 주소값을 가지고 있기 때문에 변수에 할당을 할 수 있습니다.
함수 타입은 다음과 같이 함수 시그니쳐(Function signature)로 표현합니다.
// Function
func add(a, b int) int {
return a + b
}
// Signature
func (int int) int
이를 확인하기 위해 main.go 파일을 다음과 같이 수정합니다.
package main
import "fmt"
func add(a, b int) int {
return a + b
}
func multiple(a, b int) int {
return a * b
}
func getOperator(op string) func(int, int) int {
if op == "+" {
return add
} else if op == "*" {
return multiple
} else {
return nil
}
}
func main() {
var op func(int, int) int
op = getOperator("+")
result := op(10, 20)
fmt.Println(result)
op = getOperator("*")
result = op(10, 20)
fmt.Println(result)
}
이를 실행하면 다음과 같은 결과를 확인할 수 있습니다.
# go run main.go
30
200
함수 시그니쳐의 별칭 타입
함수 시그니쳐에 별칭 타입을 사용하여 좀 더 간단하게 함수 타입을 사용할 수 있습니다.
// Function signature alias type
type OperateFunc func(int, int) int
func getOperator(op string) OperateFunc {
}
이를 확인하기 위해 main.go 파일을 다음과 같이 수정합니다.
package main
import "fmt"
type OperateFunc func(int, int) int
func add(a, b int) int {
return a + b
}
func multiple(a, b int) int {
return a * b
}
func getOperator(op string) OperateFunc {
if op == "+" {
return add
} else if op == "*" {
return multiple
} else {
return nil
}
}
func main() {
var op OperateFunc
op = getOperator("+")
result := op(10, 20)
fmt.Println(result)
op = getOperator("*")
result = op(10, 20)
fmt.Println(result)
}
이를 실행하면 다음과 같은 결과를 얻을 수 있습니다.
# go run main.go
30
200
별칭 타입을 이용하면 함수 시그니쳐를 좀 더 간단하게 사용할 수 있음을 알 수 있습니다.
함수 리터럴
Golang에서도 함수 리터럴(익명 함수)을 사용할 수 있습니다.
f := func(a, b int) int {
}
이를 확인하기 위해 main.go 파일을 다음과 같이 수정합니다.
package main
import "fmt"
type OperateFunc func(int, int) int
func getOperator(op string) OperateFunc {
if op == "+" {
return func(a, b int) int {
return a + b
}
} else if op == "*" {
return func(a, b int) int {
return a * b
}
} else {
return nil
}
}
func main() {
var op OperateFunc
op = getOperator("+")
result := op(10, 20)
fmt.Println(result)
op = getOperator("*")
result = op(10, 20)
fmt.Println(result)
}
이를 실행하면 다음과 같은 결과를 얻을 수 있습니다.
# go run main.go
30
200
Golang에서 일반적으로 함수는 상태를 가질 수 없지만, 함수 리터럴은 내부 상태를 가질 수 있습니다. 이를 확인하기 위해 main.go 파일을 다음과 같이 수정합니다.
package main
import "fmt"
func main() {
i := 0
f := func() {
i += 10
}
i++
f()
fmt.Println(i)
}
이를 실행하면 다음과 같은 결과를 얻을 수 있습니다.
# go run main.go
11
일반 함수는 외부에 선언된 변수에 접근할 수 없지만, 함수 리터럴은 외부에 선언된 변수에 접근(캡쳐)하여 사용할 수 있습니다. 여기서 캡쳐(Captured)된 값은 값 복사가 아닌 레퍼런스 복사가 이루어집니다. 즉, 포인터가 복사된다고 할 수 있습니다.
이를 확인하기 위해 main.go 파일을 다음과 같이 수정합니다.
package main
import "fmt"
func CaptureLoop1() {
f := make([]func(), 3)
fmt.Println("CaptureLoop1")
for i := 0; i < 3; i++ {
f[i] = func() {
fmt.Println(i)
}
}
for i := 0; i < 3; i++ {
f[i]()
}
}
func CaptureLoop2() {
f := make([]func(), 3)
fmt.Println("CaptureLoop2")
for i := 0; i < 3; i++ {
v := i
f[i] = func() {
fmt.Println(v)
}
}
for i := 0; i < 3; i++ {
f[i]()
}
}
func main() {
CaptureLoop1()
CaptureLoop2()
}
이를 실행하면 다음과 같은 결과를 확인할 수 있습니다.
# go run main.go
CaptureLoop1
3
3
3
CaptureLoop2
0
1
2
CaptureLoop1은 for의 변수 i의 인스턴스를 직접 참조하고 있으므로, i의 마지막 값인 3이 모두 출력됩니다. 하지만, CaptureLoop2는 i의 값을 v로 복사한 후, v의 인스턴스를 참조하고 있고 v는 for 루프에서 매번 새롭게 생성되므로, 0~2까지 모든 값들이 출력되는 것을 확인할 수 있습니다.
의존성 주입
Golang에서는 함수 타입을 이용하여 의존성 주입(DI - Dependency Injection)을 사용할 수 있습니다. 의존성 주입이란 비즈니스 로직이 내부에 존재하는 것이 아니라 외부로부터 로직을 주입받는 것을 의미합니다.
package main
import "fmt"
type PrintConsole func(string)
func PrintHello(p PrintConsole) {
p("Hello, World!")
}
func main() {
PrintHello(func(msg string) {
fmt.Println(msg)
})
}
이를 실행하면 다음과 같은 결과를 얻을 수 있습니다.
# go run main.go
Hello, World!
PrintHello는 PrintConsole이 어떤 역할(비즈니스 로직)을 하는지 알지 못합니다. PrintHello는 PrintConsole을 통해 의존성이 주입되었다고 할 수 있습니다.
완료
이것으로 함수에 대해서 좀 더 깊게 알아보고, 함수를 활용하는 방법에 대해서 알아보았습니다. 여기서 소개한 방법들을 활용하여 좀 더 멋진 코드를 작성해 보시기 바랍니다.
제 블로그가 도움이 되셨나요? 하단의 댓글을 달아주시면 저에게 큰 힘이 됩니다!
앱 홍보
Deku가 개발한 앱을 한번 사용해보세요.Deku가 개발한 앱은 Flutter로 개발되었습니다.관심있으신 분들은 앱을 다운로드하여 사용해 주시면 정말 감사하겠습니다.














![[심통]현장에서 바로 써먹는 리액트 with 타입스크립트 : 리액트와 스토리북으로 배우는 컴포넌트 주도 개발, 심통](https://img1c.coupangcdn.com/image/affiliate/banner/7cba8cb0601eebaf88a17a0c3cf65a63@2x.jpg)