※ 디스커버리 Go 언어 라는 책 내용을 전반적으로 다루지만 책 리뷰보다는 좀 더 간단하게 정리해보겠습니다.
4. 함수
코드의 덩어리를 만든 후 그 덩어리를 호출하고 귀환할 수 있는 구조를 서브루틴이라고 하는데, Go 에서는 이 서브루틴을 함수라고 부릅니다. Go 에서는 값에 의한 호출 (Call by value) 만을 지원합니다. 함수 내에서 넘겨받은 변수값을 변경하더라도 함수 밖의 변수에는 영향을 주지 않습니다. 따라서 함수 밖의 값을 변경하려면 주소값을 넘겨받아 참조에 의한 호출 (Call by reference) 과 비슷한 효과를 낼 수 있습니다. 이번 장에서는 함수에 대한 여러가지들을 보도록 하겠습니다.
4.1 값 넘겨주고 넘겨받기
4.1.1 값 넘겨 주기
func ReadFrom(r io.Reader, lines *[]string) error {
위 함수에서 lines 의 자료형을 []string 이 아닌 *[]string 으로 받은 이유는 ReadFrom 함수가 lines 변수의 값을 변경하고자 하기 때문입니다. 하지만 []string 으로 받았더라도, 슬라이스가 갖고 있는 포인터, 길이, 용량 세 값 중 포인터를 타고 가 그 값을 변경한다면 이에 대한 영향을 받게 됩니다.
4.1.2 둘 이상의 반환값
Go 언어의 함수에서는 둘 이상의 반환값을 둘 수 있습니다.
func WriteTo(w io.Writer, lines []string) (int64, error) {
...
return n, err
}
위처럼 작성시 두개의 반환값을 둘 수 있으며 함수 안에서는 쉼표로 구분해 반환하면 됩니다.
4.1.3 에러값 주고받기
둘 이상의 값을 돌려줄 수 있게 되면서 반환값을 이용한 에러처리가 조금 더 수월해졌습니다.
하나의 값만 반환할 수 있는 경우였다면, 정상적이지 않은 결과값을 돌려주는 방식으로 약속을 하는 방법이 있을 수 있습니다. Go 에서는 strings.Index 함수가 문자열이 발견되지 않으면 -1 을 돌려주는 방식으로 약속되어 있습니다. 또 다른 방법은 호출하는 쪽에서 에러값을 받고 싶은 변수의 포인터나 레퍼런스를 함수로 넘겨주어 받는 방법입니다. 하지만 이 방법도 에러를 받을 자료형을 알아야 한다는 단점이 있습니다.
Go 의 관례상 에러는 가장 마지막 값으로 반환합니다.
패닉(panic) 이라는 개념이 있어 다른 언어들의 예외와 같은 것을 제공하지만, Go 언어의 패닉은 일반적인 에러상황에서 쓰이는 것이라기 보다는 심각한 에러 상황에서 사용됩니다. 패닉 없이 일반적인 흐름에서 에러를 처리할 수 있는 경우가 대부분이므로 에러값을 돌려주는 방식에 익숙해져야 합니다. 이처럼 에러를 돌려받아 이용하는 코드를 작성하다보면 반복적인 코드양이 매우 많아지는데, Go 에선 if 문을 아래와 같이 사용할 수 있습니다.
if err := TestFunc(); err != nil {
...
}
이렇게 사용할 경우 err 변수는 조건문 밖으로 벗어나면 소멸됩니다. Go 에서 예외를 현재 문맥에서 처리할 수 없을 때엔 해당 에러를 그대로 반환 가능합니다.
if err := TestFunc(); err != nil {
return nil, err
}
새로운 에러를 생성해야 하는 경우엔 간단한 방법으로 errors.New 와 fmt.ErrorOf 를 이용할 수 있습니다.
return errors.New("stringlist.ReadFrom: line is too long") // 로그 메시지는 문맥을 알기 쉽게 써준다
return fmt.ErrorOf("stringlist: too long line at %d", count) // 다른 부가 정보를 추가한 메시지 반환 가능
4.1.4 명명된 결과 인자
Go 에서는 돌려주는 값들 역시 넘겨받는 인자와 같은 형태로 사용할 수 있습니다. 돌려주는 인자들은 기본값으로 초기화되는데, 반환할 때 기존 방식대로 사용하거나 생략하고 return 만 사용할 수 있습니다. 생략하면 돌려주는 인자들의 값들이 반환됩니다.
func WriteTo(w io.Writer, lines []string) (n int64, err error) { // 돌려주는 값에 자료형만이 아닌 이름 추가
for _, line := range lines {
var nw int
nw, err = fmt.Fprintln(w, line)
n += int64(nw)
if err != nil {
return
}
}
return
}
하지만 이처럼 사용할 경우 명명된 결과 인자는 코드를 읽기 어렵게 하기 때문에 추천하는 방법은 아닙니다.
4.1.5 가변인자
append 함수와 같이 넘겨받을 수 있는 인자의 개수가 정해져 있지 않은 함수를 만드려면 슬라이스에 담아 넘기면 됩니다.
위의 WriteTo 함수를 변경해 보도록 하겠습니다.
// lines 를 가변인자로 변경해도 lines 는 슬라이스가 됨, 기존 방식으론 호출할 때 슬라이스를 넘겨야 했지만 이제 아래처럼 호출 가능
func WriteTo(w io.Writer, lines... string) (n int 64, err error) {
WriteTo(w, "hello", "world", "Go languae")
// 이미 슬라이스인 자료를 가변인자를 두는 함수로 넘기는 방법
lines := []string{"hello", "world", "Go language"}
WriteTo(w, lines...)
4.2 값으로 취급되는 함수
Go 언어에서 함수는 일급 시민으로 분류됩니다. 함수는 값으로 변수에 담길 수 있고 다른 함수로 넘기거나 돌려받을 수 있다는 것입니다.
4.2.1 함수 리터럴
func(a, b int) int {
return a + b
}
위처럼 함수의 이름 없이 값만 표현된 것을 함수 리터럴 (Functional literal) 이라 부르고, 익명함수 라고도 합니다.
4.2.2 고계 함수 (higher-order function)
함수를 넘기고 받기만 하면 고계 함수입니다. 각각의 사용처마다 다른 함수를 적용하고 싶을때 사용하면 빛을 발합니다.
func ReadFrom(r io.Reader, f func(line string)) error {
scanner := bufio.NewScanner(r)
for scanner.Scan() {
f(scanner.Text())
}
if err := scanner.Error(); err != nil {
return err
}
return nil
} // 함수를 인자로 받아 한 줄씩 읽어 해당 함수를 호출하도록 구현
func ExampleReadFrom_Print() {
r := strings.NewReader("bill\ntom\njane\n")
err := ReadFrom(r, func(line string) { // 함수 리터럴을 넣어서 호출
fmt.Println("(", line, ")")
})
if err != nil {
fmt.Println(err)
}
}
4.2.3 클로저
닫힘이라는 의미의 클로저(closure) 는, 외부에서 선언한 변수를 함수 리터럴 내에서 마음대로 접근할 수 있는 코드를 의미합니다.
func ExampleReadFrom_append() {
...
var lines []string
err := ReadFrom(r, func(line string) {
lines = append(lines, line) // 외부에서 선언된 변수 lines 에 접근
})
...
}
4.2.4 생성기 (Generator)
func NewIntGenerator() func() int {
var next int
return func() int {
next++
return next
}
}
func ExampleNewIntGenerator() {
gen1 := NewIntGenerator()
gen2 := NewIntGenerator()
fmt.Println(gen1(), gen1(), gen1())
fmt.Println(gen2(), gen2(), gen2(), gen2(), gen2())
fmt.Println(gen1(), gen1(), gen1(), gen1())
// Output:
// 1 2 3
// 1 2 3 4 5
// 4 5 6 7
}
정수를 반환하는 함수를 반환하는 NewIntGenerator 는 고계 함수인데, 반환하는 함수는 클로저 입니다. 반환하는 함수 리터럴이 next 변수에 접근하고 있어 세트로 묶이는데 NewIntGenerator 를 여러번 호출하면 각각의 함수가 갖고있는 next 는 위처럼 따로 분리되어 있습니다.
4.2.5 명명된 자료형
rune 은 사실 int32 의 별칭인데, 이것은 아래처럼 새로 이름 붙이기가 가능하며 이런 자료형을 명명된 자료형 이라고 합니다.
- type rune int32
명명되지 않은 자료형은 아래와 같은 것들이 있습니다.
- type runes []rune
- type TestFunc func() int
자료형에 이름을 붙이면 자료형을 검사함으로써 직접 수행하기 전 컴파일 시점에 버그 예방이 어느정도 가능합니다.
type VertexID int
type EdgeID int
func NewVertexIDGenerator() func VertexID {
var next int
return func() VertexID {
next++
return VertexID(next) // next 를 반환할 수 없고 VertexID 로 형변환 해줘야 함
}
}
// 서로 다른 명명된 자료형끼리 호환되지 않음
func NewEdge(eid EdgeID) {
...
}
func main() {
gen := NewVertexIDGenerator()
e := NewEdge(gen()) // EdgeID 자료형을 인자로 받는 함수에 VertexID 자료형의 변수를 넣어서 컴파일 시 오류
}
그러나 위에서 본 []rune 과 runes 와 같이 명명되지 않은 자료형과 명명된 자료형 사이에는 표현이 같으면 호환됩니다.
type runes []rune
func main() {
var a []rune = runes{65, 66}
fmt.Println(string(a))
}
명명된 자료형을 이용하면 자료형을 하드코딩하는 것에 비해 일괄적으로 자료형의 표현을 변경할 수 있는 장점도 있습니다.
4.2.6 명명된 함수형
함수의 자료형 역시 사용자가 정의할 수 있습니다.
- type BinOp func(int, int) int
명명된 함수형 역시 자료형 검사를 하며, 표현이 같은 함수형이라도 양쪽 모두 이름이 있는 경우엔 호환되지 않습니다.
- type BinSub func(int, int) int
Binsub 은 BinOp 와 동일한 표현형이지만, 호환되지 않습니다. 함수 리터럴과 명명된 함수형 사이에는 자동으로 형변환이 일어나지만 명명된 함수형 사이에선 자동으로 형변환이 일어나지 않음을 의미합니다.
4.2.7 인자 고정
크게 중요한 것 같지 않으니 생략합니다.
4.2.8 패턴의 추상화
고계 함수를 이용하면 좀 더 높은 수준의 추상화를 이룰 수 있습니다.
func NewIntGenerator() func() int {
var next int
return func() int {
next++
return next
}
}
/* 위에서 봤던 생성기의 경우 반복되는 패턴을 재사용 가능하게끔 추상화 가능 */
func NewVertexIDGenerator() func() VertexID {
gen := NewIntGenerator()
return func() VertexID {
return VertexID(gen())
}
}
Go 언어가 함수형 언어들이 지원하는 많은 기능을 지원하고 있어서 추상화가 가능하지만 굳이 그럴 필요가 없는 경우도 많습니다. 과도한 사용으로 오히려 코드의 가독성을 어렵게 만드는 일은 피해야 합니다.
4.2.9 자료구조에 담은 함수
일급 시민이라 하면 변수에 담을 수 있고 함수에 넘겨주고 반환받을 수 있는데, 자료구조에 담을 수 있다는 점도 있습니다.
type BinOp func(int, int) int
func Eval(opMap map[string]BinOp, expr string) int {
...
}
opMap := map[string]BinOp {
"+": func(a, b int) int {return a + b},
"-": func(a, b int) int {return a - b},
"*": func(a, b int) int {return a * b},
"/": func(a, b int) int {return a / b},
}
코드의 많은 부분이 생략되었지만 사칙연산을 수행하는 Eval 함수의 인자를 string 으로 받던 것에서 연산자 자료구조를 받음으로써 코드가 간결해졌습니다. opMap 처럼 맵이라는 자료구조에 함수를 담아 사용하는 방식 또한 가능하다는 점을 보면 될 것 같습니다.
4.3 메서드
위에서 지금까지 봤던 함수에 리시버가 붙으면 메서드 입니다. 자료형 T 에 대하여 메서드를 호출할 때 이 자료형 T 에 대한 리시버가 함수이름 앞에 붙습니다.
func (recv T) MethodName(p1 T1, p2 T2) R1
(recv T) 부분이 리시버입니다.
4.3.1 단순 자료형 메서드
type VertexID int
func (id VertexID) String() string {
return fmt.Sprintf("VertexID(%d)", id)
}
func ExampleVertexID_String() {
i := VertexID(100)
fmt.Println(i)
// Output:
// VertexID(100)
}
(id VertexID) 부분이 리시버로 들어가있고, i 가 VertexID 자료형이면 i.String() 과 같이 메서드 호출이 가능합니다.
4.3.2 문자열 다중 집합
자료형에 먼저 이름을 붙여야 메서드를 정의할 수 있습니다.
type MultiSet map[string]int
func (m MultiSet) Insert(val string) {
m[val]++
}
MultiSet 자료형에 집중해 이 자료를 다루는 메서드를 정의하였고, MultiSet 은 추상 자료형이 되었습니다.
4.3.3 포인터 리시버
위의 메서드들은 모두 포이넡 리시버가 아니며, 포이넡 리시버는 자료형이 포인터형인 리시버입니다. 포인터로 전달해야 할 경우 포인터 리시버를 사용해야 합니다.
type Graph [][]int
func ReadFrom(r io.Reader, adjList *[][]int) error
// ReadFrom 은 포인터 자료형이며 메서드 역시 포인터 리시버가 필요
func (adjList *Graph) ReadFrom(r io.Reader) error
// 이렇게 메서드의 자료형을 정의하면 내부 코드 변경이 필요 없음
Go 언어의 관습상 리시버의 이름을 길게 붙이지 않습니다.
4.3.4 공개 및 비공개
다른 객체지향을 지원하는 언어들은 public 혹은 private 와 같은 키워드를 사용해 접근을 조절할 수 있습니다. Go 의 경우 다른 예약어가 필요없이 하나의 법칙이 있는데, 메서드의 이름이 대문자로 시작하면 해당 메서드는 다른 모듈에서 호출이 가능하고, 소문자로 시작되면 다른 모듈에서 사용이 불가능 합니다. 이러한 법칙은 자료형, 변수, 상수, 함수 모두에 적용됩니다.
대문자로 작성해 공개된 요소들에는 반드시 주석을 달도록 합니다.
4.4 활용
4.4.1 타이머 활용하기
프로그램의 수행을 잠시 멈추고 싶을 땐 time.Sleep 함수를 사용합니다.
package main
import (
"fmt"
"time"
)
func CountDown(seconds int) {
for seconds > 0 {
fmt.Println(seconds)
time.Sleep(time.Second)
seconds--
}
}
func main() {
fmt.Println("Hi")
CountDown(5)
}
이 함수를 호출하면 1초에 한 줄씩 출력됩니다. 이와 같은 타이머를 블로킹 타이머라고 하는데, 1초동안 프로그램은 잠시 수행을 멈춥니다. 타이머를 이용할 때 기다리지않고 다른 일을 수행하는 경우, 이런 타이머는 넌 블로킹 타이머라고 합니다.
Go 에선 블로킹을 동기, 넌 블로킹을 비동기 라고도 합니다. time 이라는 모듈안에 Timer 를 이용하면 넌 블로킹 타이머를 이용할 수 있습니다.
4.4.2 path/filepath 패키지
해당 패키지는 파일 이름 경로를 다루는 패키지입니다. 그 중 Walk 라는 함수가 있는데, 이 함수는 지정된 디렉터리 경로 아래에 있는 파일들에 대해 어떤 일을 할 수 있는 함수 입니다. 이 함수는 각 파일에 대해 어떤 일을 할지 호출자가 결정할 수 있도록 고계 함수로 되어있습니다. 이러한 함수가 있다는게 중요한 것이 아니라, 고계 함수로 작성되어 있다는 점을 주의깊게 봐야할 것입니다.
4.5 Summary
- 코드의 덩어리를 추상화한 것이 서브루틴
- 함수에 값을 넘겨주고 받을 수 있음 -> 고계함수
- 포인터를 넘겨받으면 넘겨준 루틴의 변수 값 변경 가능
- 에러는 관례상 마지막 값으로 돌려줌
- 가변 인자를 사용할 땐 점 세개를 이용, 슬라이스에 점 세개를 이용해 가변 인자처럼 남겨줄 수 있음
- 함수의 이름 부분을 삭제하면 익명 함수, 함수 리터럴이 됨
- 함수 리터럴은 값으로 취급되므로 변수에 담을 수 있고, 함수로 넘겨주거나 받을 수 있음. 자료형에도 함수 담기 가능
- 클로저는 함수 리터럴이 있는 스코프 내의 변수들에 접근 가능
- 자료형에 이름을 붙이면 명명된 자료형으로 구분해 자료형 검사 가능. 명명된 자료형에 메서드 정의 가능
- 함수를 넘기고 받는것을 활용해 깊은 패턴의 추상화 가능
- 명명된 자료형에 메서드 정의할 수 있음
- 메서드의 리시버는 값 또는 포인터 사용할 수 있음.
- 패키지에 정의된 식별자 중 대문자로 시작하는 것만 외부에서 접근 가능하며 메서드도 동일
출처 : https://book.naver.com/bookdb/book_detail.nhn?bid=10337667
'Backend > Golang' 카테고리의 다른 글
Go 언어 - [6장] (웹 어플리케이션 작성) (0) | 2020.08.29 |
---|---|
Go 언어 - [5장] (구조체) (0) | 2020.08.22 |
Go 언어 - [3장] (문자열 및 자료구조) (0) | 2020.08.04 |
Go 언어 - [2장] (Go 환경 설정) (0) | 2020.08.02 |
Go 언어 - [1장] (Go 언어 소개) (0) | 2020.07.20 |