struct 내zerovalue 처리 방법

14606 단어 Go

흐름도



배경.


go는null의 안전한 언어가 아닙니다.뿐만 아니라 수신기는nil시 처리도 쓸 수 있다1.
nil 수신기의 활용 예시
package main

import "fmt"

type Name struct {
    First string
    Last  string
}

func (n *Name) String() string {
    if n == nil {
        return "N/A"
    }
    return fmt.Sprintf("%s, %s", n.Last, n.First)
}

func main() {
    name := &Name{"Taro", "Yamada"}
    fmt.Println(name.String()) // => Yamada, Taro
    name = nil
    fmt.Println(name.String()) // => N/A
}
한편,interface의zerovalue는nil이며, 이 방법을 호출하면 무효가 되어 유감입니다.
누르보의 예
package main

import (
    "bufio"
    "io"
    "os"
)

type Command struct {
    Input  io.Reader
    Output io.Writer
}

func (c *Command) Run() {
    scanner := bufio.NewScanner(c.Input)
    scanner.Scan()
    c.Output.Write(scanner.Bytes())
}

func main() {
    c := &Command{} // Oops! 初期化し忘れた。
    c.Run() // => SEGV
}
그럼 이 문제를 어떻게 처리해야 하나요?

오류 처리


물론 닐이 있는 이상 이 상황을 고려해 처리해야 한다.
상기 "잘못된 예시"의 오류 처리 버전
package main

import (
    "bufio"
    "errors"
    "io"
    "os"
)

type Command struct {
    Input  io.Reader
    Output io.Writer
}

func (c *Command) Run() error {
    if c.Input == nil || c.Output == nil {
        return errors.New("both Input and Output should be non-nil")
    }
    scanner := bufio.NewScanner(c.Input)
    scanner.Scan()
    c.Output.Write(scanner.Bytes())
    return nil
}

func main() {
    c := &Command{}
    fmt.Println(c.Run())
}
error 처리로 패닉을 사용하는 부작용이 너무 강하기 때문에 피하는 것이 좋다(see 부록).

기본값 준비


error를 대체하는 수단으로 기본값을 준비하는 방법도 자주 사용된다.
상기 "누르보의 예"의 기본값 처리 버전
package main

import (
    "bufio"
    "io"
    "os"
)

var DefaultInput io.Reader = os.Stdin
var DefaultOutput io.Writer = os.Stdout

type Command struct {
    Input  io.Reader
    Output io.Writer
}

func (c *Command) input() io.Reader {
    if c.Input != nil {
        return c.Input
    }
    return DefaultInput
}

func (c *Command) output() io.Writer {
    if c.Output != nil {
        return c.Output
    }
    return DefaultOutput
}

func (c *Command) Run() {
    scanner := bufio.NewScanner(c.input())
    scanner.Scan()
    c.output().Write(scanner.Bytes())
}

func main() {
    c := &Command{}
    c.Run()
}
예를 들어 http.Server&http.Server{}에서 사용한 가설일 수도 있고 기본값에 대한 설명이 많다.

총결산


Go에서 제로value를 마음대로 생성하기 때문에 error 처리를 정확하게 하고 기본값을 준비할 필요가 있습니다.
당연한 것 같지만 실제로 해보면 촌스러운 기술을 쓰기 때문에 타협하기 쉽다.
지금의 언어로 지능적으로 이 문제를 해결하는 것은 어려울 것 같다2. 근육으로 해결하는 것이 상책이다3.

부록


error에 오류가 없다는 전제로panic가 사용할 수 있는 상황을 고려해 보세요.

가능:assersion의 사용법.예를 들어 Must 함수입니다.


유명한 곳으로는 regexp.MustCompiletemplate.Must가 있다.
이것은 assersion의 생각에 가깝다.아니, Must 버전도 같이 준비해야지.

허용: 단독 함수에서 전달된 매개 변수가 정확하지 않은 경우


예: template.JSEscape.
호출자에게 파라미터를 검사하라고 요구할 수도 있다.

미묘함: 매개 변수로 발생하지만 내부 상태에 의존하는 상황


예로func (*ServeMux) Handle.
pattern이 로그인했을 때panic가 했지만 조금 위험한 규격입니다.

Bad: 내부 상태에서만 발생


상술한'누르보의 예'는 일치한다.매개 변수와 상관없이 내부 상태에서만 발생하는 상황.
recover를 전제로 편성했다면 받아들일 수 있었을 텐데 go에 맞지 않는 실상이었다.

부록: 첫 번째 그림 출처

@startuml
(*) --> "struct内のZero valueの対処"
if "デフォルト値で処理 vs. エラー処理" then
  -left-> [デフォルト値で処理] "デフォルト値を用意する"
  if "デフォルトを変更可能にするか?" then
    --> [yes] "グローバルにexportedな形で配置する"
    if "nilを入れられた時どうするか"
      --> [error] "安全 :)"
    else
      --> [panic] "ベストではないが妥協ラインではある :("
    endif
  else
    --> [no] "exportしない"
  endif
else
  -right-> [エラー処理] "エラーを適切に処理する"
  if "errorを返す vs. panicにする"
    --> [error] "安心 :)"
  else
    --> [panic] "ドキュメントに記載はするだろうが、あまりよくはない :("
  endif
endif
@enduml
그럼에도 불구하고 나는 가능한 한 피하는 것이 좋겠다고 생각한다.그나저나 Objective-c에서는 nil 수신기의 정보 발송이 무시되었다.Ruby(method mising에서 Hack하지 않는 경우) NoMethod Error에서 
나는 null이 안전할 필요는 전혀 없다고 생각하지만, 나는 일부의non-nil 보증을 원한다.func (r io.Reader non-nil) 이런 인상. 
주입 의존에 관해서는 struct를 이용한 embed의 복안이 있기 때문에 총괄하고 싶습니다. 

좋은 웹페이지 즐겨찾기