ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • 5장 함수
    프로그래밍/Golang 2019. 11. 3. 22:22
    The Go Programming Language

    5장 함수

    함수는 여러 문장을 하나의 단위로 묶어 프로그램 내의 다른 부분에서 수차례 호출할 수 있게 함

    함수를 통해 큰 작업을 여러 작은 작업으로 불할하고 시간 및 공간적으로 분리된 여러 사람이 동시에 작성할 수 있게 함

    함수는 사용자에게 구현의 세부사항을 숨김

     

    5.1 함수 선언

    함수 선언에는 이름, 파라미터 목록, 부가적인 결과 목록, 본문이 있음

    func name(parameter-list) (result-list) { 
    	body
    }

    파라미터 목록은 함수 파라미터의 이름과 타입을 지정하며 이 인자는 호출자가 값이나 인자를 제공하는 지역 변수임

    결과 목록은 함수가 반환하는 값의 타입을 지정하며 한개의 이름 없는 결과를 반환하거나 결과를 반환하지 않을 경우 괄호를 사용할 필요가 없으며 보통 생략함

    결과 목록을 생략하면 아무 값도 반환하지 않으며 그 동작을 위해서만 호출되는 함수를 선언

    func hypot(x, y float64) float64 { 
    	return math.Sqrt(x*x + y*y)
    }
    fmt.Println(hypot(3, 4)) // "5"

    x와 y는 선언의 파라미터이고, 3과 4는 호출의 인수이며, 이 함수는 float64 값을 반환

    피라미터처럼 결과에도 이름을 붙일 수 있는데 각각의 이름은 해당 타입의 제로 값으로 초기화된 지역 변수를 선언

    결과 목록에 있는 함수는 panic을 호출하거나 break 없는 무한 for 루프 등의 명백하게 끝까지 수행할 수 없는 경우 외에는 반드시 return으로 끝나야 함

    같은 타입의 파라미터나 결과 목록은 간략하게 한 번만 쓸 수 있음

    unc f(i, j, k int, s, t string) { /* ... */ } f
    unc f(i int, j int, k int, s string, t string) { /* ... */ }

    다음은 int 타입의 파라미터 2개와 결과 1개를 선언하는 4가지 방법

    func add(x int, y int) int { return x + y } 
    func sub(x, y int) (z int) { z = x - y; return } 
    func first(x int, _ int) int { return x }
    func zero(int, int) int { return 0 }
    fmt.Printf("%T\n", add) // "func(int, int) int"
    fmt.Printf("%T\n", sub) // "func(int, int) int"
    fmt.Printf("%T\n", first)// "func(int, int) int"
    fmt.Printf("%T\n", zero)// "func(int, int) int"
    • 함수의 시그니처

    함수 타입은 함수의 시그니처(signature)라고도 함

    두 함수에서 파라미터 목록의 타입이 같고 결과 목록의 타입도 같다면 이 두 함수는 타입 또는 시그니처 값이 같다고 함

    파라미터와 결과의 이름은 타입과 관련이 없으며 간략하게 선언됐는지 여부도 타입에 영향을 주지 않음

    모든 함수는 호출시 각 파라미터를 선언한 순서대로 인자로 제공해야 함

    파라미터의 기본 값은 없음

    인자를 이름으로 지정할 수 없음

    파라미터와 결과의 이름은 문서 외에는 호출자에게 영향을 주지 않음

    파라미터는 함수 본문에서 초기 값이 호출자가 제공한 인자 값인 지역 변수

    함수의 파라미터와 명명된 결과 값은 함수의 제일 바깥쪽 지역 변수와 같은 어휘 블록에 속하는 변수

    인자는 값으로 전달되므로 함수는 각 인자의 복사본을 받음

    복사본 변경은 호출자에게 영향을 주지 않음

    인자가 포인터, 슬라이스, 맵, 함수, 채널 등의 참조형인 경우 호출자가 함수 내부에서 인자에 의해 간접적으로 참조된 값 변경에 영향을 받을 수 있음

    함수가 구현됐음을 나타내는 본문 없는 함수

    package math
    func Sin(x float64) float64 // implemented in assembly language

    5.2 재귀

    함수는 스스로를 직접 또는 간접적으로 재귀 호출할 수 있음

    재귀적인 데이터 구조를 처리하는 데 필수

    • golang.org/x/net/html

    HTML 파서를 제공하는 비표준 패키지

    html.Parse 함수는 바이트 시퀀스를 읽고 파싱한 후 HTML 문서 트리의 루트인 html.Node를 반환

    5.3 다중 값 반환

    함수는 결과를 1개 이상 반환 가능

    다중 값을 반환하는 함수를 호출한 결과는 값의 튜플임

    links, err := findLinks(url)

    반환 값 중 일부를 무시하려면 값을 빈 식별자에 할당

    links, _ := findLinks(url) // errors ignored
    Go의 가비지 콜렉터는 사용하지 않는 메모리를 재활용하지만 사용하지 않는 열린 파일이나 네트워크 접속 등의 운영체제 리소스는 해제하지 못함
    명시적으로 닫아야 함

    여러 파라미터를 받는 함수를 호출할 때 다중 값을 반환하는 함수를 단독 인수로 사용할 수 있음

    log.Println(findLinks(url))
    
    links, err := findLinks(url) 
    log.Println(links, err)

    잘 선택된 이름으로 함수 결과의 의미를 문서화할 수 있음

    이름은 특히 다음과 같이 함수가 동일한 타입의 여러 결과를 반환할 때 가치가 있음

    func Size(rect image.Rectangle) (width, height int) 
    func Split(path string) (dir, file string)
    func HourMinSec(t time.Time) (hour, minute, second int)

    관행적으로 마지막 bool 결과는 성공 여부를 나타냄

    error 결과에는 아무런 설명도 필요 없음

    • 단순 반환

    함수의 결과에 이름을 붙이면 반환문에서 피연산자를 생략할 수 있는데 이를 단순 반환(bere return)이라 함

    // CountWordsAndImages does an HTTP GET request for the HTML
    // document url and returns the number of words and images in it. 
    func CountWordsAndImages(url string) (words, images int, err error) {
    	resp, err := http.Get(url) 
        if err != nil {
    		return //단순 반환: return 0, 0, err
        }
    	doc, err := html.Parse(resp.Body) 
        resp.Body.Close()
    	if err != nil {
    		err = fmt.Errorf("parsing HTML: %s", err)
    		return //단순 반환: return 0, 0, err
        }
    	words, images = countWordsAndImages(doc)
    	return //단순 반환: return words, images, nil
    }
    func countWordsAndImages(n *html.Node) (words, images int) { /* ... */ }

    단순 반환은 각각의 이름 붙은 결과 변수를 순서대로 반환하는 단축 문법

    위 함수에서 각 반환문은 다음과 같다.

    return words, images, err

    많은 반환문과 여러 결과 값이 있는 함수에서는 단순 반환으로 코드 중복을 줄일 수 있는 있지만 코드를 이해하기 쉽게 하는 경우는 거의 없음

     

    5.4 오류

    어떤 함수는 항상 주어진 작업을 성공함(예: strings.Contains, strconv.FormatBool)

    전제 조건이 맞을 때에만 항상 성공하는 함수(예: time.Data 함수는 항상 자신의 컴포넌트(연도, 월 등)로 time.Time을 구성하며 마지막 인자(시간대)가 nil인 경우에만 패닉이 일어나며 패닉은 호출하는 코드에서 버그가 확실하며 잘 작성된 프로그램에서는 절대 발생하지 않음)

    오류는 패키지의 API 또는 애플리케이션의 사용자 인터페이스에서 중요한 부분이며 실패는 여러 예상되는 행동 중 하나에 불과함

    실패가 예상되는 함수는 보통 마지막에 부가적인 결과를 반환

    입출력처럼 실패에 다양한 원인이 있을 때는 실패를 호출자에게 설명할 수 있는 추가적인 결과의 타입은 error임

    내장 타입 error는 인터페이스 타입임

    error가 nil이면 성공이고 nil이아니면 실패이며 nil이 아닌 error에는 오류 메시지 문자열이 있어서 Error 메소드를 호출해 얻거나 fmt.Println(err) 또는 fmt.Printf("%v", err)로 출력할 수 있음

    보통 함수가 nil이 나닌 오류를 반환하면 다른 결과 값들은 정의되지 않은 것이며 무시해야 함

    어떤 함수는 오류시 결과를 일부 반환하는데 예를 들어 파일에서 읽는 도중에 오류가 발생하면 Read 호출은 그때까지 읽은 바이트 수와 문제에 대해 설명하는 error 값을 반환

    어떤 호출자는 올바른 동작을 위해 오류를 처리하기 전에 불완전한 데이터를 처리해야 할 수도 있으므로 이러한 함수에서는 결과를 명확하게 문서화하는 것이 중요

    Go는 실패를 예외처리(exception)가 아닌 일반 값으로 보고한다는 점에서 다른 많은 언어와 구별됨

    이러한 설계의 이유는 예외가 오류의 설명과 오류를 처리하기 위한 제어 흐름을 얽히게 하는 경향이 있어 바람직하지 않은 결과를 초래하기 때문

    Go에서는 if와 return 같은 통상적인 제어 흐름 방식으로 처리하는데 이는 오류 처리 조적에 더 많은 신경을 써야 한다는 점이 명백하며 바로 그 부분이 핵심

     

    5.4.1 오류 처리 전략

    함수 호출이 오류를 반환하면 호출자가 오류를 확인하고 적절한 조치를 취해야 함

    • 전략 1

    오류를 전파해 서브루틴에서 실패를 호출 루틴의 실패가 되게 하는 방법

    다음은 HTTP 오류를 호출자에게 반환

    resp, err := http.Get(url) 
    if err != nil {
    	return nil, err
    }

    새 오류 메시지를 생성

    doc, err := html.Parse(resp.Body) 
    resp.Body.Close()
    if err != nil {
    	return nil, fmt.Errorf("parsing %s as HTML: %v", url, err) 
    }

    fmt.Errorf 함수는 fmt.Sprintf를 이용해 오류 메시지를 생성하고 새 error 값을 반환

    이 값을 이용해 오류 메시지의 원본에 문맥 정보를 연속해서 추가함으로써 좀 더 서술적인 오류를 생성

    오류 메시지는 서로 연결돼 있는 경우가 많으므로 메시지 문자열 안의 대문자나 줄 바꿈은 피해야 함

    • 전략 2

    일시적이거나 예상치 못한 문제를 나타내는 오류는 일정한 지연시간을 두거나 재시도 횟수 또는 재시도 소요 시간을 제한하고 실패한 작업을 다시 해보는 것이 합리적일 수 있음

    gopl.io/ch5/wait
    // WaitForServer attempts to contact the server of a URL. 
    // It tries for one minute using exponential back-off.
    // It reports an error if all attempts fail.
    func WaitForServer(url string) error {
    	const timeout = 1 * time.Minute
    	deadline := time.Now().Add(timeout)
    	for tries := 0; time.Now().Before(deadline); tries++ {
    		_, err := http.Head(url) 
            if err == nil {
            	return nil // success
            }
    		log.Printf("server not responding (%s); retrying...", err)
    		time.Sleep(time.Second << uint(tries)) // exponential back-off 
        }
    	return fmt.Errorf("server %s failed to respond after %s", url, timeout) 
    }

     

    • 전략 3

    더 이상 진행할 수 없으면 호출자가 오류를 출력하고 프로그램을 깔끔하게 종료할 수 있지만 보통 이러한 과정은 프로그램에서 main 패키지의 역할임

    라이브러리 함수는 일반적으로 오류가 내부의 모순(즉, 버그)을 나타내지 않는 한 호출자에게 오류를 전파해야 함

    // (In function main.)
    if err := WaitForServer(url); err != nil {
    	fmt.Fprintf(os.Stderr, "Site is down: %v\n", err)
    	os.Exit(1) 
    }

    같은 효과를 얻는 좀 더 쉬운 방법은 log.Fatalf를 호출하는 것

    log.Fatalf는 기본적으로 다른 log 함수와 마찬가지로 오류 메시지의 앞에 시간과 날짜를 추가

    if err := WaitForServer(url); err != nil { 
    	log.Fatalf("Site is down: %v\n", err)
    }
    • 전략 4

    오류를 기록하고 필요시 기능을 약간 제한한 후 계속해도 될 때가 있음

    다음은 log 패키지 사용

    if err := Ping(); err != nil {
    	log.Printf("ping failed: %v; networking disabled", err)
    }
    

    다음은 표준 오류 스트림에 직접 출력

    if err := Ping(); err != nil {
    	fmt.Fprintf(os.Stderr, "ping failed: %v; networking disabled\n", err)
    }
    • 전략 5

    흔치 않지만 오류를 안전하게 무시해도 되는 경우

    dir, err := ioutil.TempDir("", "scratch") 
    if err != nil {
    	return fmt.Errorf("failed to create temp dir: %v", err) 
    }
    // ...use temp dir...
    os.RemoveAll(dir) // ignore errors; $TMPDIR is cleaned periodically

    os.RemoveAll 호출은 실패할 수도 있지만 운영체제가 주기적으로 임시 디렉토리를 정리하므로 프로그램에서 무시함

    이 경우 의도적으로 오류를 버렸지만 깜박하고 이 오류를 처리하지 않았더라도 프로그램 로직은 동일할 것임

    함수를 호출한 뒤에는 항상 반환되는 오류에 대해 고려하는 습관을 갖고 일부러 오류를 무시할 때에는 그 의도를 명백하게 문서화해야 함

     

    Go의 오류 처리에는 특유의 리듬이 있음

    일반적으로 오류를 확인한 후 실패를 성공 전에 처리함

    실패로 인해 함수가 반환돼야 한다면 성공에 대한 로직은 들여 쓰기 한 else 블록이 아닌 외부 수준에 작성함

    함수는 일반적으로 오류를 반환하는 초기 조건 확인 구문들과 그 다음부터 끝까지 최소한으로 들여 쓰기 한 함수 본문 부분으로 이뤄진 구조를 나타냄

     

    5.4.2 파일의 끝(EOF)

    파일에서 n 바이트 데이터를 읽으려 할 때 n을 파일 길이와 동일하게 설정했다면 실패를 나타내는 오류가 발생하지 않지만 호출자가 파일이 소진될 때까지 반복해 고정 크기의 청크를 읽으려 하는 경우 호출자가 파일 끝 오류와 그 외의 오류에 각기 다른게 응답해야 함

    이 때문에 io 패키지는 파일의 끝으로 인한 읽기 실패의 경우를 다른 오류와 구별해 io.EOF 오류로 보고함

    package io import "errors"
    // EOF is the error returned by Read when no more input is available. 
    var EOF = errors.New("EOF")

    호출자는 표준 입력에서 룬을 읽는 다음 루프에서와 같이 간단한 비교로 이 경우를 감지할 수 있음

    in := bufio.NewReader(os.Stdin) 
    for {
    	r, _, err := in.ReadRune() 
        if err == io.EOF {
        	break // finished reading
        }
    	if err != nil {
    		return fmt.Errorf("read failed: %v", err)
    	}
    	// ...use r... 
    }

    파일 끝에 도달한 뒤에는 그 사실 외에 보고할 정보가 없으므로 io.EOF는 고정된 오류 메시지 "EOF"를 갖음

     

    5.5 함수 값

    함수는 퍼스트 클래스 값

    함수 값이 다른 값과 마찬가지로 타입이 있고 이 값은 변수에 할당하거나 함수로 전달하거나 함수에서 반환할 수 있다는 의미

     

    5.6 익명 함수

    명명된 함수는 패키지 수준에서만 선언할 수 있지만 함수 리터럴로 표현식 내의 어디서나 함수 값을 나타낼 수 있음

    함수 리터럴은 함수 선언과 유사하게 작성하지만 func 키워드 뒤에 이름이 없으며 이를 익명 함수라 함

    함수 리터럴로 사용 시점에 함수를 정의할 수 있음

    전체 구문 환경(lexical environment)에 접근할 수 있으로 내부 함수에서 외부 변수를 참조할 수 있음

    gopl.io/ch5/squares
    // squares returns a function that returns
    // the next square number each time it is called. 
    func squares() func() int {
    	var x int
    	return func() int {
    		x++
    		return x * x 
        }
    }
    func main() {
    	f := squares()
    	fmt.Println(f()) // "1"
        fmt.Println(f()) // "4"
        fmt.Println(f()) // "9"
        fmt.Println(f()) // "16"
    }

    squares() 함수는 func() int 타입의 다른 함수를 반환

    squares를 호출하면 지역 변수 x를 생서한 후 호출될 때마다 x를 증가시키고 그 제곱 값을 반환하는 익명 함수를 반환

    squares를 한 번 더 호출하면 두 번째 x를 생성하고, 그 변수를 증가시키는 새 익명 함수를 반환

    squares는 함수 값이 코드 외에 상태도 가질 수 있음을 보여줌

    이와 같은 함수 값은 closure라는 기술로 구현되며 Go 개발자들은 보통 함수 값이라는 용어를 사용

     

    5.6.1 주의: 반복 변수 캡처

    구문 범위 규칙(lexical scope rules)의 함정

    var rmdirs []func()
    for _, dir := range tempDirs() {
    	os.MkdirAll(dir, 0755)
    	rmdirs = append(rmdirs, func() {
    	os.RemoveAll(dir) // NOTE: incorrect! 
        })
    }
    

    for 루프는 dir 변수가 선언되는 위치에 새 구문 블록을 생성

    이 루프로 생성된 모든 함수 값은 동일한 변수(특정 시점의 값이 아닌 저장 공간 위치의 주소)를 캡처하고 공유함

    이러한 문제를 해결하기 위해 중요한 변수 선언인 내부 변수(dir)를 자주 사용하며 이 변수는 외부 변수와 동일한 이름의 복사본임

    for _, dir := range tempDirs() {
    	dir := dir // declares inner dir, initialized to outer dir 
        // ...
    }
    

    5.7 가변 인자 함수

    가변 인자 함수(variadic functions)는 다양한 개수의 인자로 호출할 수 있음

    가변 인자 함수를 선언하려면 최종 파라미터 타입 앞에 생략 기호 "..."를 붙여서 함수에서 해당 타입의 인자를 제한 없이 받을 수 있다는 것을 나타냄

    gopl.io/ch5/sum
    func sum(vals ...int) int { 
    	total := 0
    	for _, val := range vals { 
        	total += val
    	}
        return total
    }
    

     

    호출은

    fmt.Println(sum()) // "0" 
    fmt.Println(sum(3)) // "3" 
    fmt.Println(sum(1, 2, 3, 4)) // "10"
    
    //마지막 인자 뒤에 생략 기호를 추가
    values := []int{1, 2, 3, 4} 
    fmt.Println(sum(values...)) // "10"

    5.8 연기된 함수 호출

    • defer

    defer 문은 defer 키워드가 앞에 붙은 일반 함수나 메소드 호출

    함수와 인자 표현식은 구문이 실행되는 시점에 평가되지만 실제 호출은 defer 문이 있는 함수가 정상적으로 반환문 또는 끝에 도달하거나 비정상적인 패닉이 일어나서 완료될 때까지 미뤄짐

    호출은 개수와 무관하게 지연

    지연된 역순으로 실행

    연기된 함수는 반환문이 함수의 결과 변수를 갱신한 이후에 실행됨

    익명 함수는 명명된 결과를 포함한 외부 함수의 변수에 접근할 수 있으므로 연기된 익명 함수도 함수의 결과를 볼 수 있음

    연기된 함수는 함수의 실행이 끝날 때까지 실행되지 않으므로 루프 안의 defer 문에는 특별히 주의해야 함

    for _, filename := range filenames { 
    	f, err := os.Open(filename)
    	if err != nil {
    		return err 
        }
    	defer f.Close() // NOTE: risky; could run out of file descriptors
    	// ...process f... 
    }

    위 코드는 모든 파일을 처리될 때까지 닫지 않으므로 파일 디스크립터 부족 문제(out of file descriptors)가 발생

    해결책 중 하나는 defer 문을 포함한 루프 본문을 반복 시마다 호출되는 다른 함수로 옮기는 것

    for _, filename := range filenames {
    	if err := doFile(filename); err != nil {
    		return err 
        }
    }
    func doFile(filename string) error { 
    	f, err := os.Open(filename)
    	if err != nil {
    		return err 
        }
    	defer f.Close()
    	// ...process f... 
    }

    5.9 패틱

    컴파일 시 수많은 실수를 잡아내지만 배열 범위 바깥쪽 참조나 nil 포인터 참조 등의 실행 시 검사가 필요한 경우 실수를 발견하면 패닉을 발생시킴

    보통 패닉 상황에서는 정상 실행이 중단되고 고루틴에 있는 모든 연기된 함수가 호출되며 프로그램이 로그 메시지와 함께 비정상 종료됨

    모든 패닉은 런타임에 일어나지 않을 수 있고 내장된 패닉 함수를 사용해 직접 패닉을 호출할 수 있음

    switch s := suit(drawCard()); s {
    case "Spades": // ...
    case "Hearts": // ...
    case "Diamonds": // ...
    case "Clubs": // ...
    default:
    	panic(fmt.Sprintf("invalid suit %q", s)) // Joker? 
    }

    5.10 복구

    어떤 방법으로든 복구하거나 최소한 종료하기 전에 정리할 수 있음

    내장된 recover 함수는 연기된 함수 안에서 호출되며 defer 구문이 들어 있는 함수가 패닉을 일으키면 recover가 현재의 패닉 상태를 끝내고 패닉 값을 반환함

    패닉을 일으킨 함수는 마지막 부분을 계속하지 않고 정상적으로 반환

    다른 때에 recover를 호출하면 아무런 영향 없이 nil을 반환

    func Parse(input string) (s *Syntax, err error) {
    	defer func() {
    		if p := recover(); p != nil {
    			err = fmt.Errorf("internal error: %v", p)
    		} 
        }()
    	// ...parser... 
     }

    parse의 연기된 함수는 패닉을 복구하고 패닉 값으로 오류 메시지를 생성

     

    '프로그래밍 > Golang' 카테고리의 다른 글

    Golang 관련 도서 다운로드 및 번역 문서  (0) 2019.11.13
    7장 인터페이스  (0) 2019.11.06
    6장 메소드  (0) 2019.11.04
    4장 복합 타입  (0) 2019.10.30
    Golang - 함수  (0) 2019.08.06
Designed by Tistory.