개요
이번 블로그 포스트에서는 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로 개발되었습니다.관심있으신 분들은 앱을 다운로드하여 사용해 주시면 정말 감사하겠습니다.