Tucker의 Go 언어 프로그래밍 책과 유튜브를 통해 학습 중입니다.
가변 인수 함수
- fmt패키지의 Println 함수는 인수 개수가 정해져있지 않음.
fmt.Println() // 인수가 없을 수도 있고
fmt.Println(1) // 인수가 1개일 수도 있고
fmt.Println(1, 2, 3, 4, 5, 6, 7, 8, 9) // 인수가 많을 수도 있음.
→ 위와 같이 함수 인수 개수가 고정적이지 않은 함수를 가변 인수 함수(Variadic function)이라고 함.
... 키워드 사용
- ... 키워드를 사용해서 가변 인수를 처리할 수 있음
- 인수 타입 앞에 ...를 붙여서 해당 타입 인수를 여러 개 받는 가변 인수임을 표시하면 됨.
package main
import "fmt"
func sum(nums ...int) int { // 가변 인수를 받는 함수
sum := 0
fmt.Printf("nums 타입: %T\n", nums) // nums 타입 출력
for _, v := range nums {
sum += v
}
return sum
}
func main() {
fmt.Println(sum(1, 2, 3, 4, 5)) // 인수 5개를 사용
fmt.Println(sum(10, 20)) // 인수 2개를 사용
fmt.Println(sum()) // 인수 0개를 사용
}
// 결과
nums 타입: []int
15
nums 타입: []int
30
nums 타입: []int
0
- 인수 타입인 int 앞에 ...를 붙여 가변 인수 타입을 선언
- nums 타입을 출력하면, sum() 함수 내부에서 nums는 int 슬라이스 타입 []int로 처리 됨.
= 가변 인수는 함수 내부에서 해당 타입의 슬라이스로 처리됨.
※ 인수 타입 앞에 점 .을 3개 찍어서 ...으로 가변 인수를 표시하고 함수 내부에서는 슬라이스 타입으로 동작함 ※
fmt.Println(2, "hello", 3.14) // 여러 타입의 인수를 섞어쓸 수 있음
- Println() 함수를 호출하며 여러 타입을 인수로 한 번에 섞어 쓰기 위해서는 빈 인터페이스 interface{}를 사용해야 함.
- 모든 타입이 빈 인터페이스를 포함하고 있기 때문에 빈 인터페이스 가변 인수 ...interface{} 타입으로 받으면,
모든 타입의 가변 인수를 받을 수 있음.
- 함수 내부에서 인터페이스 변환 기능을 이용해 타입별로 다르게 동작시킴.
defer 지연 실행
- 파일이나 소켓 핸들처럼 OS 내부 자원을 사용하는 경우, 파일을 생성하거나 읽을 때 OS에 파일 핸들을 요청함.
- 해당 자원은 OS 내부 자원이기 때문에 사용 후 반드시 OS에 돌려줘야 함.
- 프로그램에서 OS 내부 자원을 되돌려주지 않으면 내부 자원이 고갈되어 더는 파일을 만들지 못하거나, 네트워크 통신을 못할 수 있음.
- 파일 작업 이후 반.드.시 파일 핸들을 반환해야 함!
= 함수 종료 전에 처리해야 하는 코드가 있을 때 defer를 사용해 실행할 수 있음.
defer 명령문
→ 해당 명령어와 같이 사용하면 바로 실행되는 것이 아닌, 해당 함수가 종료되기 직전에 실행되도록 지연됨.
package main
import (
"fmt"
"os"
)
func main() {
f, err := os.Create("test.txt") // 파일 생성
if err != nil { // 에러 확인
fmt.Println("Failed to create a file", err)
return
}
defer fmt.Println("반드시 호출됩니다.") // 지연 수행될 코드
defer f.Close() // 지연 수행될 코드
defer fmt.Println("파일을 닫았습니다.") // 지연 수행될 코드
fmt.Println("파일에 Hello World를 씁니다.")
fmt.Fprintln(f, "Hello World") // 파일에 텍스트를 씀
}
- os 패키지에서 제공하는 Create() 함수를 사용해 파일을 생성
- Create() 함수가 에러 없이 제대로 실행 됐는지 확인
- defer로 함수 종료 전에 반드시 호출되어야 할 코드를 지정
※ defer는 역순으로 호출됨! (FILO or LIFO)
함수 타입 변수
- 함수를 값으로 갖는 변수
- 컴퓨터는 0과 1로 나타낼 수 있는 숫자값만 가질 수 있고, 포인터는 숫자로 나타낼 수 있는 메모리 주소를 값으로 가짐.
= 함수를 숫자로 표현
* 함수 호출 과정
✓ 1번 라인에서 main() 함수가 시작되고 100번 라인에서 f()함수가 시작된다는 가정
- 컴퓨터 내부(CPU 내부)에 프로그램 카운터(Program counter)가 있고, 이는 다음 실행할 라인을 나타내는 레지스터
1. 1번 라인 명령을 실행하면 프로그램 카운터는 1이 증가하여 2를 가리키고 다음에 2번 라인을 실행
2. 만약 f() 함수가 호출되면 프로그램 카운터는 f() 함수의 시작 포인트인 100번 라인으로 변경
3. 다음 번에 100번 라인부터 명령을 실행
즉, 함수의 시작 위치가 함수의 주소가 될 수 있음
= 주소는 숫자값, 메모리도 시작되는 주소값을 포인터 변수로 사용할 수 있듯 함수도 주소가 있고 숫자값이며, 그 값을 변수 값으로 사용할 수 있음.
★ 함수 시작 지점이 바로 함수를 가리키는 값이고, 마치 포인터처럼 함수를 가리킨다고 해서 함수 포인터(Function pointer)라고 부름.
→ 함수 타입은 함수 시그니쳐(Function signature)로 표현
- 함수 이름과 구현을 제외한 함수 시그니쳐
func (int, int) int
package main
import "fmt"
func add(a, b int) int {
return a + b
}
func mul(a, b int) int {
return a * b
}
func getOperator(op string) func(int, int) int {
if op == "+" {
return add
} else if op == "*" {
return mul
} else {
return nil
}
}
func main() {
var operator func(int, int) int
operator = getOperator("*")
var result = operator(3, 4)
fmt.Println(result)
}
// 결과 값
12
✓ 인수로 오는 op 값에 따라 다른 함수를 반환 함.
- op가 "+"이면 add() 함수(주소)를 반환하고, "*"이면 mul() 함수(주소)를 반환하며 이 두 개가 아니면 nil을 반환
func (int, int) int
→ 함수 타입이며, int 2개를 입력 받고 int로 출력하는 함수
- 함수 타입은 유효하지 않은 메모리 주소를 나타내는 nil을 값으로 가질 수 있음(어떤 함수도 가리키지 않음을 나타냄)
- 함수 타입 변수 operator에 소괄호로 묶어 operator가 가리키는 함수를 호출 할 수 있음.
= operator가 mul()함수를 가리키므로 operator(3, 4)는 mul(3, 4)와 같음.
* 별칭으로 함수 정의 줄여쓰기
- 함수 정의는 일반적으로 길기 때문에 가독성을 위해 별칭 타입을 사용하여 함수 정의를 짧게 줄일 수 있음.
type opFunc func (int, int) int
func getOperator(op string) opFunc
→ 위와 같이 func(int, int) int 함수 정의를 opFunc으로 짧게 재정의하면 getOperator()함수 정의도 짧게 적을 수 있음.
* 함수 정의에서 매개변수명 생략
- 함수 정의에서 매개변수명은 적어도 되고, 적지 않아도 됨
func (int, int) int
func (a int, b int) int
함수 리터럴(Function literal)
- 이름 없는 함수로 함수명을 적지 않고 함수 타입 변숫값으로 대입되는 함숫값.
- 함수명이 없기 때문에 함수명으로 직접 함수를 호출할 수 없고 함수 타입 변수로만 호출됨.
- 다른 프로그래밍 언어에서는 람다(Lambda)라고 부르기도 함.
package main
import "fmt"
func getOperator(op string) func(int, int) int {
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() {
fn := getOperator("*")
result := fn(3, 4)
fmt.Println(result)
}
// 결과
12
* 함수 리터럴 내부 상태(First class / First Object)
- 필요한 변수를 내부 상태로 가질 수 있음
- 함수 리터럴 내부에서 사용되는 외부 변수는 자동으로 함수 내부 상태로 저장됨.
package main
import "fmt"
func main() {
i := 0
f := func() {
i += 10
}
i++
f()
fmt.Println(i)
}
// 결과
11
- i 변수는 함수 내부가 아닌 외부에 있는 외부 변수이며, 함수 리터럴 내부에서 외부 변수인 i에 접근
= 함수 리터럴 내부에서 외부 변수에 접근할 때 필요한 변수를 내부 상태로 가져와서 접근할 수 있게 함.
f := func() {
i += 10
}
- func()에 입력 값이 없는 데 i를 사용할 수 있는 것은 캡쳐(Capture)라는 것 때문 (i는 외부의 i를 내부 상태로 사용하는 것)
= 함수 리터럴 외부 변수를 내부 상태로 가져오는 것을 챕쳐(Capture)라고 함.
캡쳐는 값 복사가 아닌 참조 형태(레퍼런스)로 가져오게 되니 주의 필요! = (포인터가 복사된다고 볼 수 있음)
package main
import "fmt"
func CaptureLoop() {
f := make([]func(), 3)
fmt.Println("ValueLoop")
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("valueLoop2")
for i := 0; i < 3; i++ {
v := i
f[i] = func() {
fmt.Println(v)
}
}
for i := 0; i < 3; i++ {
f[i]()
}
}
func main() {
CaptureLoop()
CaptureLoop2()
}
// 결과
ValueLoop
0
1
2
valueLoop2
0
1
2
※ 책에는 CaptureLoop의 값이 333으로 나오는데, 우선 책에서 사용하는 Go 버전(1.16)과 내가 사용하는 Go 버전(1.23)이 다름.
- 1.22 이후로 function literal 참조하는 방식이 바뀐 거 같음. (조금 더 찾아보고 업데이트는 추후에 하기로 ...)
(https://tip.golang.org/doc/go1.22 / https://go.dev/ref/spec#Function_literals)
* 의존성 주입(Dependency injection)
package main
import (
"fmt"
"os"
)
type Writer func(string)
func writeHello(writer Writer) {
writer("Hello World")
}
func main() {
f, err := os.Create("test.txt")
if err != nil {
fmt.Println("Failed to create a file")
return
}
defer f.Close()
writeHello(func(msg string) {
fmt.Fprintln(f, msg)
})
}
- writeHello() 함수 입장에서 보면, writeHello()는 인수로 Writer 함수 타입을 받음
writeHello() 함수 입장에서는 인수로 온 writer를 호출 했을 때 그게 파일에 씌여질지, 출력될지 알 수가 없음
→ 이렇게 외부에서 로직을 주입하는 것을 의존성 주입이라고 함. (로직을 분리하는 효과가 있음)
✓ 반드시 함수 리터럴로만 할 수 있는 것은 아니고, 인터페이스를 통해서도 구현할 수 있음
type WriterInterface interface {
Write(string)
}
..
func writeHello2(writer WriterInterface) {
writer.Write("Hello World")
}
'Language > Golang' 카테고리의 다른 글
Golang (Go언어) 자료 구조(Data Structure) 2/2 (0) | 2024.12.03 |
---|---|
Golang (Go언어) 자료 구조(Data Structure) 1/2 (1) | 2024.12.02 |
Golang (Go언어) 인터페이스(Interface) (0) | 2024.11.27 |
Golang (Go언어) 메서드(Method) (2) | 2024.11.27 |
Golang (Go언어) 슬라이스(Slice) 2/2 (2) | 2024.11.20 |