Go 1.13 이후의 error 검사

55815 단어 Go

Go 1.13 이후의 error 검사


기사 목록

  • Go 1.13 이후의 error 검사
  • 시작
  • error가 어디에서 왔는가
  • error
  • 검사 방법
  • 1.13+error
  • 확인 방법
  • Unwrap
  • errors.Is
  • errors.As
  • 자리 표시자%w
  • 감사
  • 시작


    만약 Go가 많은 비난을 받고 있다면, Go 중의 error의 처리는 반드시 토조 차트 10위 안에 들어갈 수 있을 것이다.기왕 try 문장의 제의가 거절당하고 거절당했으니 우리도 오래된if로 오류를 선별할 수밖에 없다.Go 공식은 error의 계륵 문제를 의식하지 못한 것이 아니다. 그래서 Go 1.13에서 새로운 해결 방안을 제시했다. 전체적으로 말하자면'3개api+하나의 형식 점유자'이다.

    error


    Go에서 error는 어디에서 왔습니까?Go를 능숙하게 사용하는 사람들은 반드시 다음과 같은 몇 가지 방법을 알고 있다.
  • errors.New
  • fmt.Errorf
  • 함수 호출 후 얻은 error
  • 를 직접 되돌려줍니다.
  • 구조체를 정의하고 error 인터페이스(type error Interface { Error() string }
  • 를 실현한다
    파일 열기를 예로 들면 위의 처리 방식에 따라 코드는 다음과 같습니다.
    // errors.New
    func openConfigFile(path string) error {
    	_, err := os.Open(path)
    	if err != nil {
    	    //   error  
    		return errors.New("can not open the specific file")
    	}
    	return nil
    }
    
    // fmt.Errorf
    func openConfigFile(path string) error {
    	_, err := os.Open(path)
    	if err != nil {
    	    //   error  ,  error
    		return fmt.Errorf("can not open the specific file, reason: %v", err)
    	}
    	return nil
    }
    
    // return error that called function returned
    func openConfigFile(path string) error {
    	_, err := os.Open(path)
    	if err != nil {
    	    //   error, 
    		return err
    	}
    	return nil
    }
    
    //   error
    type OpenErr struct {
    	Err error  //   error
    }
    
    //   error  
    func (*OpenErr) Error() string {
    	return "can not open the specific file"
    }
    
    func openConfigFile(path string) error {
    	_, err := os.Open(path)
    	if err != nil {
    		return &OpenErr{Err:err}
    	}
    	return nil
    }
    

    상술한 네 가지 방법은 각각 천추가 있다.방법 1, errors.New는 새 error를 되돌려줍니다. 저장된 데이터는 우리가 전송한 텍스트 ("can not open the specific file") 입니다.이런 방식을 채택하는 것은 통상적으로 호출자에게 오류가 발생했다는 것을 알려주기 위해서이지만, 실제 오류의 세부 사항은 폭로하기를 원하지 않는다.호출자에 대해서 말하자면, 그는 무슨 잘못이 생겼는지에 대해 그다지 관심을 가지지 않고, 단지 잘못이 있었는지에 대해서만 신경을 쓴다.
    방법 2는 방법 1과 같고 원시 오류를 숨길 수도 있지만 보통 원시 오류의 문자열 설명을 함께 되돌려줍니다.이 처리 방식에서 "can not open the specific file"은 추가 제시어이고, "reason:% v"는 오류 디테일을 표시합니다.일반적으로 fmt를 호출합니다.Errorf의 더 큰 확률은 코드를 해석하는 것이 아니라 보여 주는 오류가 발생하는 것이다.
    방법 3, 일반적으로 함수 호출자는 error의 실제 유형이나 실제 값에 따라 다음 실행 전략을 확정해야 한다.즉, 이것은 error를 해석해야 하기 때문에 함수는'플레인'error를 되돌려줍니다.이렇게 하는 단점은 error에 추가 정보를 추가할 수 없다는 것이다.
    방법4는 상술한 세 가지 방법의 집합으로 볼 수 있으며 원시적인 error를 보존할 수 있을 뿐만 아니라 추가 정보도 추가할 수 있다.이 메타데이터를 마음대로 조합하여 호출자가 원하는 모양을 모두 가지고 있다.단점은 코드를 좀 더 써야 한다는 것이다.

    오류 확인 방법


    지금 error가 생겼습니다. 우리는 어떻게 오류를 검사해야 합니까?1.13 이전에 흔히 볼 수 있는 것은: 1.비교 값;2. 유형 비교.
    정부 원본을 예로 들면 더욱 설득력이 있다.우리 먼저 비교해 보자.
    func (db *DB) QueryContext(ctx context.Context, 
                               query string, 
                               args ...interface{}) (*Rows, error) {
    	var rows *Rows
    	var err error
    	for i := 0; i < maxBadConnRetries; i++ {
    		rows, err = db.query(ctx, query, args, cachedOrNewConn)
    		if err != driver.ErrBadConn {  //  
    			break
    		}
    	}
    	if err == driver.ErrBadConn {  //  
    		return db.query(ctx, query, args, alwaysNewConn)
    	}
    	return rows, err
    }
    
    func (db *DB) Query(query string, 
                        args ...interface{}) (*Rows, error) {
    	return db.QueryContext(context.Background(), query, args...)
    }
    
    var ErrBadConn = errors.New("driver: bad connection")
    

    상술한 것은 sql.Query의 원본 코드로 ErrBadConn 오류를 처리하는 것이 바로 비교 값을 통해 이루어진 것이다.
    비교 유형 처리 error는 Go 원본 코드에서 더욱 자주 사용되는데 주로 이런 처리 방식이 더욱 유연하고 오류 정보가 풍부하다.
    코드의 주요 목적은 어떤 사이트를 방문하는 것이다.방문 과정에서 여러 가지 이상이 발생할 수 있지만, 우리는 시간 초과 이상만 처리합니다.
    // http  
    client := &http.Client{
    	Timeout:       3 * time.Second,  // 3  
    }
    //   url
    _, err := client.Get("http://www.meiyuouzhegewangzhan.com")
    if err != nil {
    	if os.IsTimeout(err) { //   err
    		fmt.Println("timeout")
    	} else {  //   err
    		fmt.Println("other errors")
    	}
    }
    

    위에서 알 수 있듯이 오류가 시간 초과 이상인지 판단하려면 호출os.IsTimeout이 필요합니다. 원본 코드는 다음과 같습니다.
    func IsTimeout(err error) bool {
        //  
    	terr, ok := underlyingError(err).(timeout)
    	return ok && terr.Timeout()
    }
    
    func underlyingError(err error) error {
       //   err
    	switch err := err.(type) {
    	case *PathError:
    		return err.Err
    	case *LinkError:
    		return err.Err
    	case *SyscallError:
    		return err.Err
    	}
    	//   err,  
    	return err
    }
    
    IsTimeoutunderlyingError든 유형 비교를 통해 서로 다른 오류 유형을 처리한다.또한 Path Error, Link Error, SysCall Error를 계속 따라가면 이런 error는 모두 앞서 언급한 네 번째 방법이라는 것을 알게 될 것이다.그것들은 원시 error를 포장했지만 일정한 상황에서 그것을 꺼낸다 (예를 들어 상기 코드의 err.Err.

    1.13 + 오류 확인 방법


    과거에 error를 검사하는 방식이 좀 번거롭다는 것을 인정할 수 밖에 없다. 특히 error가 너무 많이 포장되었을 때.만약에 error가 3층으로 포장되었다고 가정하면 가장 안쪽에 있는 error를 검사해야 합니다. 이것은 코드가 이렇게 써야 한다는 것을 의미합니다.
    e1, _ := e.(Err1)
    e2, _ := e1.(Err2)
    e3, _ := e2.(Err3)
    if e3 == targetErr {
        // handle
    }
    

    이러한 번거로움을 피하기 위해 1.13부터 Go는 체인의 error를 검사하는 두 가지 방법을 제공했다.비교값errors.Is과 비교유형errors.As이다.error 체인 검사를 지원하려면 구조체 (또는 다른 오류 형식) 에 익명 인터페이스 interface { Unwrap() error } 를 실행해야 합니다.

    Unwrap


    Unwrap 인터페이스는 구조체의 원시 error를 되돌려 주는 데 사용됩니다.PathError의 소스 코드 예:
    type PathError struct {
    	Op   string
    	Path string
    	Err  error
    }
    
    func (e *PathError) Unwrap() error { return e.Err }
    

    errors.Is


    errors.Is는 값을 통해 오류를 비교적 확인합니다. 여기에 오류가 있기 쉽습니다.많은 error가 바늘 형식이기 때문에, 바늘 형식의 비교는 바늘 변수가 저장된 주소가 같은지 비교하는 것이다.코드로 설명하면 이해하기 쉽습니다.
    err1 := errors.New("error")
    err2 := errors.New("error")
    
    //  
    fmt.Println(err1 == err2) // false
    
    //  
    err1Elem := reflect.ValueOf(err1).Elem().Interface()
    err2Elem := reflect.ValueOf(err2).Elem().Interface()
    fmt.Println(err1Elem == err2Elem) // true
    

    err1과 err2는 본질적으로 오류 정보까지 같지만 직접적으로 비교하면false를 얻을 수 있다.이것은 err1과 err2가 다른 주소를 가지고 있기 때문이다.두 번째 방식은 두 개의 바늘이 가리키는 구조체를 추출하여 두 구조체의 값을 비교하여true의 결과를 얻는 것이다.
    공식 패키지가 이런 문제에 대한 처리는 종종 오류 변수를 정의하고 그 후의 전체 프로그램이 실행되는 동안 값을 부여한 오류 변수를 사용합니다. 같은 오류의 바늘이 항상 같은 주소를 가리키는지 확인하십시오.
    // go  
    var (
    	// ErrInvalid indicates an invalid argument.
    	// Methods on File will return this error when the receiver is nil.
    	ErrInvalid = errInvalid() // "invalid argument"
    
    	ErrPermission = errPermission() // "permission denied"
    	ErrExist      = errExist()      // "file already exists"
    	ErrNotExist   = errNotExist()   // "file does not exist"
    	ErrClosed     = errClosed()     // "file already closed"
    	ErrNoDeadline = errNoDeadline() // "file type does not support deadline"
    )
    
    func errInvalid() error    { return oserror.ErrInvalid }
    func errPermission() error { return oserror.ErrPermission }
    func errExist() error      { return oserror.ErrExist }
    func errNotExist() error   { return oserror.ErrNotExist }
    func errClosed() error     { return oserror.ErrClosed }
    func errNoDeadline() error { return poll.ErrNoDeadline }
    

    파일의 존재 여부를 판단하는 두 가지 방법:
    var err error
    f, err := os.Open(" ")
    defer f.Close()
    
    //  1
    if os.IsNotExist(err) { //   if stmt
    	fmt.Println(" ")
    }
    //  2
    if errors.Is(err, os.ErrNotExist) { //   if stmt
    	fmt.Println(" ")
    }
    

    그래서 상기 특성에 따라errors.Is는 error 체인을 거의 처리할 수 없습니다.똑똑한 공식 라이브러리가 어떻게 이런 점을 생각하지 못했을까. 해결 방안은 원본에서 찾아야 한다.
    // go  
    func Is(err, target error) bool {
    	...
    	isComparable := reflectlite.TypeOf(target).Comparable()
    	for {
    	    //  
    		if isComparable && err == target {
    			return true
    		}
    		//   Is  
    		if x, ok := err.(interface{ Is(error) bool }); ok && x.Is(target) {
    			return true
    		}
    		...
    		if err = Unwrap(err); err == nil {
    			return false
    		}
    	}
    }
    

    errors.Is는 선진적인 값을 비교할 것입니다. 만약 실패하면 Is가 Is를 호출하기 때문에 사용자 정의 error에 Is를 실현하는 방법을 줄 수 있습니다. 이것은 error가 같은 처리 논리를 작성하는 데 사용됩니다.
    // Err1
    type Err1 struct {
    	Err error
    }
    func (e *Err1) Error() string { return "err1" }
    func (e *Err1) Unwrap() error { return e.Err }
    func (e * Err1) Is(other error) bool {
    	v1 := reflect.ValueOf(e)
    	v2 := reflect.ValueOf(other)
    	//  
    	if v1.IsNil() || v2.IsNil() {
    		return false
    	}
    	//  
    	v1 = v1.Elem()
    	if v2.Kind() == reflect.Ptr {
    		v2 = v2.Elem()
    	}
    	return v1.IsValid() && v2.IsValid() && v1.Interface() == v2.Interface()
    }
    
    // Err2
    type Err2 struct {
    	Err error
    }
    func (e *Err2) Error() string { return "err2" }
    func (e *Err2) Unwrap() error { return e.Err }
    func (e *Err2) Is(other error) bool {
    	v1 := reflect.ValueOf(e)
    	v2 := reflect.ValueOf(other)
    	//  
    	if v1.IsNil() || v2.IsNil() {
    		return false
    	}
    	//  
    	v1 = v1.Elem()
    	if v2.Kind() == reflect.Ptr {
    		v2 = v2.Elem()
    	}
    	return v1.IsValid() && v2.IsValid() && v1.Interface() == v2.Interface()
    }
    
    // Err3
    type Err3 struct {
    	Err error
    }
    func (e *Err3) Error() string { return "err3" }
    func (e *Err3) Unwrap() error { return e.Err }
    func (e *Err3) Is(other error) bool {
    	v1 := reflect.ValueOf(e)
    	v2 := reflect.ValueOf(other)
    	//  
    	if v1.IsNil() || v2.IsNil() {
    		return false
    	}
    	//  
    	v1 = v1.Elem()
    	if v2.Kind() == reflect.Ptr {
    		v2 = v2.Elem()
    	}
    	return v1.IsValid() && v2.IsValid() && v1.Interface() == v2.Interface()
    }
    
    //   error
    func genErr() error {
    	return &Err1{
    		Err: &Err2{
    			Err: &Err3{
    				Err: nil,
    			},
    		},
    	}
    }
    
    func main() {
    	err := genErr()
    	err3 := &Err3{Err:nil}
    	fmt.Println(errors.Is(err, err3))
    }
    

    코드가 분명히 거추장스러워져서 아래를 내려다보면 더 우아한 방법이 있다.

    errors.As


    오류 체인이 존재할 때, 우리는 유형으로 오류를 포지셔닝하고, errors를 사용하는 경향이 있다.오류가 아닌.Is.이렇게 하면 불필요한 Is 방법을 피할 수 있다.
    // Err1
    type Err1 struct {
    	Err error
    }
    func (e *Err1) Error() string { return "err1" }
    func (e *Err1) Unwrap() error { return e.Err }
    
    // Err2
    type Err2 struct {
    	Err error
    }
    func (e *Err2) Error() string { return "err2" }
    func (e *Err2) Unwrap() error { return e.Err }
    
    // Err3
    type Err3 struct {
    	Err error
    }
    func (e *Err3) Error() string { return "err3" }
    func (e *Err3) Unwrap() error { return e.Err }
    
    //   error
    func genErr() error {
    	return &Err1{
    		Err: &Err2{
    			Err: &Err3{
    				Err: nil,
    			},
    		},
    	}
    }
    
    func main() {
    	err := genErr()
    	var err3 *Err3
    	fmt.Println(errors.As(err, &err3)) //    
    }
    

    자리 표시자%w


    대부분의 경우 우리는 사용자 정의 오류 유형을 크게 조작할 필요가 없고, 두 번째 방식fmt.Errorf을 즐겨 사용한다.1.13부터%w 자리 표시자를 사용하면 원시 error를 감싸서 err 필드에 넣고 msg에 새 error 문자열을 구축합니다. 대응하는 구조체는 wrapError입니다.
    type wrapError struct {
    	msg string
    	err error
    }
    
    func (e *wrapError) Error() string {
    	return e.msg
    }
    
    func (e *wrapError) Unwrap() error {
    	return e.err
    }
    

    즉, 다음과 같은 사용법은 원시 error를 잃지 않고 오히려 error 체인을 구축하여 errors를 편리하게 할 수 있다.Is 및 errors.As에서 오류 검사를 진행합니다.
    if _, err := os.Open("xxx.txt") {
        return fmt.Errorf("failed open, reason: %w", err)
    }
    

    감사

  • Working with Errors in Go 1.13
  • 참조

    좋은 웹페이지 즐겨찾기