[Go] 함수

Jiwoo Kim·2021년 11월 4일
0

Go

목록 보기
6/11
post-thumbnail

Go의 서브 루틴은 함수라고 한다.

파라미터

파라미터를 포인터로 받아야 caller에서의 변수 값도 같이 변경할 수 있다.

슬라이스를 넘겨 주면 배열 포인터 길이 용량 이렇게 구조체의 세 가지 값이 전달된다.
배열 포인터가 가리키고 있는 실제 배열에 접근해서 그 값을 변경하면 caller의 배열도 그대로 영향을 받는다.

func AddOne(nums []int) {
    for i := range nums {
    	nums[i]++
    }
}

func ExampleAddOne() {
    n := []int{1, 2, 3}
    AddOne(n)
    fmt.Println(n)
    // Output:
    // [2 3 4]
}

반면, 슬라이스 포인터를 넘겨 주면 슬라이스 구조체의 값이 아니라, 슬라이스 구조체의 주소값이 전달된다.

func ReadFrom(r io.Reader, lines *[]string) error {}

이렇게 슬라이스 포인터를 넘겨 줘야만 배열 포인터 길이 용량을 변경할 수 있다.
즉, 슬라이스가 가리키는 배열 자체가 다른 배열로 대체되는 경우에는 포인터를 넘겨 줘야 한다.

포인터

  • 포인터로 넘어온 값은 *을 앞에 붙여서 값을 참조할 수 있다.
  • 변수 앞에 &를 붙이면 변수의 포인터 값을 얻을 수 있다.

가변인자

인자 개수가 정해져 있지 않은 경우, 아래와 같이 인자를 정의할 수 있다.

func WriteTo(w io.Wrtier, lines ...string) (int, error) {}
// 호출은 아래와 같이 한다.
WriteTo(w, "hello", "world", "!")
// 이미 slice가 있는 경우 아래와 같이 넘길 수 있다.
lines := []string{"hello", "world", "!"}
WriteTo(w, lines...)

반환값

값 여러개를 한 번에 반환할 수 있다. 관행상 error는 맨 마지막 파라미터로 반환한다.

func WriteTo(w io.Wrtier, lines []string) (int, error) {
    // ...
    return n, err
}

받을 땐 아래와 같이 받는다.

n, err := WriteTo(w, lines)

에러값

일반적인 흐름에서 에러를 처리할 때는 error를 돌려 받아서 사용한다.
panicexception과 비슷한 역할이다. 하지만 일반적이지 않은 심각한 에러 상황에서만 사용된다.

예외는 보통 발생한 곳과 처리하는 곳이 다른 경우가 많기 때문에, Go는 필요한 경우 caller에게 예외를 그대로 반환할 수 있다.

if err := MyFunc(); err != nil {
    return nil, err
}

새로운 에러는 errors.New(), fmt.Errorf()를 이용해서 생성할 수 있다.

Named return parameter

반환값에 이름을 붙여 사용할 수 있다. 명명된 결과 인자는 기본값으로 초기화된다.
많은 경우에 Named return parameter는 코드를 읽기 어렵게 만든다. 특별히 좋은 점이 있을 때만 사용하는 것이 좋다.

값으로 취급

Go 언어에서 함수는 일급 시민(First-class citizen)으로 분류된다. 즉, 함수도 으로 변수에 담길 수 있고 다른 함수로 넘기거나 돌려받을 수 있다는 것이다.

함수 리터럴 (function literal)

이름 없이 순수한 함수의 값만 표현한 것으로, 익명 함수라고도 부른다.

func Example_funcLiteral() {
    func() {
    	fmt.Println("Hello world!")
    }()
    
    printHelloWorld := func() {
    	fmt.Println("Hello world!")
    }
    printHelloWorld()
    
    // Output:
    // Hello world!
    // Hello world!
}

고계 함수 (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.Err(); err != nil {
    	return err
    }
    return nil
}

func ExampleReadFrom_Print() {
    r := strings.NewReader("a\nbb\nccc\n")
    err := ReadFrom(r, func(line string) {
    	fmt.Println("(", line, ")")
    })
    if err != nil {
    	fmt.Println(err)
    }
    
    // Output:
    // ( a )
    // ( bb )
    // ( ccc )
}

위 예시에서 ReadFrom()f func(line string)을 넘기고 받기 때문에 고계 함수가 된다.

클로저 (closure)

클로저는 외부에서 선언한 변수를 함수 리터럴 내에서 마음대로 접근할 수 있는 코드를 의미한다.

func ExampleReadFrom_append() {
    r := strings.NewReader("a\nbb\nccc\n")
    var lines []string
    err := ReadFrom(r, func(line string) {
    	lines = append(lines, line)
    })
    if err != nil {
    	fmt.Println(err)
    }
    fmt.Println(lines)
    
    // Output:
    // [a bb ccc]
}

위 예시에서 func(line string) 함수 리터럴이 lines 변수를 자유롭게 사용하고 있다.

생성기 (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(gen1(), gen2(), gen2(), gen2())
    // Output:
    // 1 2 3
    // 4 1 2 3
}
  • NewIntGenerator()는 int를 반환하는 함수를 반환하는 고계 함수
  • 반환되는 함수 리터럴은 클로저다. next 변수는 각 함수마다 분리되어 있다.

명명된 자료형 (named type)

type runes []rune
type MyFunc func() int

자료형에 이름을 붙일 수 있다. 이를 통해 얻을 수 있는 장점은 아주 많은데, 우선 컴파일 시점에 타입을 검증해서 예기치 못한 에러를 방지할 수 있다는 점이 있다.

type VertexID int
type EdgeID int

func NewVertexIDGenerator() func() VertexID {
    var next VertexID
    return func() VertexID {
    	next++		// int로 표현되는 자료형이라 바로 연산 가능하다.
        return next
    }
}

func NextEdgeIDGenerator() func() EdgeID {
    var next int		// int로 선언한 경우
    return func() EdgeID {
    	next++
        return EdgeID(next)	// 형변환을 명확하게 해줘야 한다.
    }
}

같은 int 타입도 이름을 다르게 붙여서 확실하게 구분할 수 있다.

명명된 함수형

자료형에 alias를 부여할 수 있는 것처럼, 함수 이름도 사용자가 정의할 수 있다. 역시 컴파일 시점에 자료형 검사가 이루어진다.

type BinOp func(int, int) int

func OpThreeAndFour(f BinOp) {
    fmt.Println(f(3, 4))
}

위의 함수를 아래와 같이 호출할 수 있다.

OpThreeAndFour(func (a, b int) int {
	return a + b
})

파라미터의 함수 리터럴 func (a, b int) int가 명명되지 않은 함수형이기 때문에 자동으로 호환된다.

type BinOp func(int, int) int
type BinSub func(int, int) int

하지만 위와 같이 양쪽 모두 명명된 함수형인 경우 표현이 같더라도 상호 호환되지 않는다.

인자 고정

type MultiSet map[string]int
type SetOp func(m MultiSet, val string)

func Insert(m MultiSet, val string) {...}

func BindMap(f SetOp, m MultiSet) func(val string) {
	return func(val string) {
    	f(m, val)
    }
}

Insert()의 첫 인자인 m을 고정한 함수를 이용하는 것처럼 사용할 수 있다.

m := NewMultiSet()
ReadFrem(r, BindMap(Insert, m))

패턴의 추상화

func NewIntGenerator() func() int {
    var next int
    return func() int {
    	next++
        return next
    }
}

func NewVertexIDGenerator() func() VertexID {
    gen := NewIntGenerator()
    return func() int {
    	return VertexID(gen())
    }
}

반복되는 패턴은 코드를 재사용함으로서 추상화할 수 있다. 하지만 굳이 그럴 필요가 없는 경우도 많기 때문에 적절히 사용해야 한다.

자료구조에 담은 함수

map과 같은 자료구조에 함수를 value로 담아 사용할 수 있다.

type BinOp func(int, int) 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 },
}

메서드

리시버(receiver)가 붙은 함수를 메서드라고 한다.

자료형 T에 대해 메서드를 호출할 때 이 자료형 T에 대한 리시버가 함수 이름, 즉 메서드 이름 앞에 붙는다. 리시버 부분의 모양은 인자 목록과 같지만, 함수 이름 앞에 온다는 것이 다르다.

func (recv T) MethodName(p1 T1, p2 T2) R1

recv에 담겨 있는 자료에 대한 연산들을 메서드로 정의할 수 있다. T는 명명된 자료형이어야 한다. 또한, 리시버의 이름은 길게 붙이지 않는 것이 컨벤션이다.

단순 자료형 메서드

type VertexID int

func (id VertexID) String() string {
    return fmt.Sprintf("VertexID(%d)", id)
}

이렇게 메서드를 정의하면 id.String()과 같이 호출할 수 있다.
또한, 단순 자료형을 추상 자료형으로 만들어 메서드를 정의함으로써 추상화 할 수 있다.

포인터 리시버

포인터 리시버(Pointer receiver)는 자료형이 포인터형인 리시버다. 리시버 역시 다른 인자들과 마찬가지로 값으로 전달되기 때문에, 포인터로 전달해야 할 경우에는 포인터 리시버를 사용해야 한다.

func WriteTo(w io.Writer, adjList [][]int) error
func ReadFrom(r io.Reader, adjList *[][]int) error

위와 같은 함수 정의에 리시버를 적용해 메서드로 변환하면 아래와 같이 바뀐다.

type Graph [][]int

func (adjList Graph) WriteTo(w io.Writer) error
func (adjList *Graph) ReadFrom(r io.Reader) error

ReadFrom()에 있는 리시버가 포인터 리시버의 예시다.

0개의 댓글