【跟着我们学Golang】之异常处理

数字精灵号
• 阅读 3610

Java中的异常分为Error和Exception来处理,这里也以错误和异常两种,来分别讲一讲Go的异常处理。

Go 语言没有类似 Java 或 .NET 中的异常处理机制,虽然可以使用 defer、panic、recover 模拟,但官方并不主张这样做。Go 语言的设计者认为其他语言的异常机制已被过度使用,上层逻辑需要为函数发生的异常付出太多的资源。同时,如果函数使用者觉得错误处理很麻烦而忽略错误,那么程序将在不可预知的时刻崩溃。
Go 语言希望开发者将错误处理视为正常开发必须实现的环节,正确地处理每一个可能发生错误的函数。同时,Go 语言使用返回值返回错误的机制,也能大幅降低编译器、运行时处理错误的复杂度,让开发者真正地掌握错误的处理。
-- 摘自:C语言中文网

error接口

Go处理错误的思想

通过返回error接口的方式来处理函数的错误,在调用之后进行错误的检查。如果调用该函数出现错误,就返回error接口的实现,指出错误的具体内容,如果成功,则返回nil作为error接口的实现。

error接口声明了一个Error() string 的函数,实际使用时使用相应的接口实现,由函数返回error信息,函数的调用之后进行错误的判断从而进行处理。

// The error built-in interface type is the conventional interface for
// representing an error condition, with the nil value representing no error.
type error interface {
    Error() string
}

Error() 方法返回错误的具体描述,使用者可以通过这个字符串知道发生了什么错误。下面看一个例子。

package main

import (
    "errors"
    "fmt"
)

func main() {
    sources := []string{"hello", "world", "souyunku", "gostack"}
    fmt.Println(getN(0, sources))//直接调用,会打印两项内容,字符串元素以及error空对象
    fmt.Println(getN(1, sources))
    fmt.Println(getN(2, sources))
    fmt.Println(getN(3, sources))
    
    target, err := getN(4, sources)//将返回结果赋值
    if err != nil {//常见的错误处理,如果error不为nil,则进行错误处理
        fmt.Println(err)
        return
    }

    fmt.Println(target)
}

//定义函数获取第N个元素,正常返回元素以及为nil的error,异常返回空元素以及error
func getN(n int, sources []string) (string, error) {
    if n > len(sources)-1 {
        return "", fmt.Errorf("%d, out of index range %d", n, len(sources) - 1)
    }
    return sources[n], nil
}

/*
打印内容:
hello <nil>
world <nil>
souyunku <nil>
gostack <nil>
 4, out of index range 3
*/

常见的错误处理就是在函数调用结束之后进行error的判断,确定是否出现错误,如果出现错误则进行相应的错误处理;没有错误就继续执行下面的逻辑。

遇到多个函数都带有error返回的时候,都需要进行error的判断,着实会让人感到非常的苦恼,但是它的作用是很好的,其鲁棒性也要比其他静态语言要好的多。

自定义error

身为一个接口,任何定义实现了Error() string函数,都可以认为是error接口的实现。所以可以自己定义具体的接口实现来满足业务的需求。

error接口的实现有很多,各大项目也都喜欢自己实现error接口供自己使用。最常用的是官方的error包下的errorString实现。

// Copyright 2011 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.

// Package errors implements functions to manipulate errors.
package errors

// New returns an error that formats as the given text.
func New(text string) error {
    return &errorString{text}
}

// errorString is a trivial implementation of error.
type errorString struct {
    s string
}

func (e *errorString) Error() string {
    return e.s
}

可以看到,官方error包通过定义了errorString来实现了error接口,在使用的时候通过New(text string) error这个函数进行调用从而返回error接口内容(该函数在返回的时候是一个errorString类型的指针,但是定义的返回内容是error接口类型,这也举例说明了上节讲到的接口的内容)下面看例子。

package main

import (
    "errors"
    "fmt"
)

func main() {
    //直接使用errors.New来定义错误消息
    notFound := errors.New("404 not found")
    fmt.Println(notFound)

    //也可以使用fmt包中包装的Errorf来添加
    fmt.Println(fmt.Errorf("404: page %v is not found","index.html"))
}

/*
打印内容
404 not found
404: page index.html is not found
*/

自己试着实现一个404notfound的异常

type NOTFoundError struct {
    name string
}

func (e *NOTFoundError) Error() string {
    return fmt.Sprintf("%s  is not found, please new again", e.name)
}

func NewNotFoundError(name string) error{
    return &NOTFoundError{name}
}

func runDIYError() {
    err := NewNotFoundError("your girl")

    // 根据switch,确定是哪种error
    switch err.(type) {
    case *NOTFoundError:
        fmt.Printf("error : %v \n",err)
    default: // 其他类型的错误
        fmt.Println("other error")
    }
}

/**调用runDIYError()结果
error : your girl  is not found, please new again 
*/

自己定义异常NotFoundError只是简单的实现Error() string函数,并在出错的时候提示内容找不到,不支持太多的功能,如果业务需要,还是可以继续扩展。

defer

在将panic和recover之前插播一下defer这个关键字,这个关键字在panic和recover中也会用到。

defer的作用就是指定某个函数在执行return之前在执行,而不是立即执行。下面是defer的语法

defer func(){}()

defer指定要执行的函数,或者直接声明一个匿名的函数并直接执行。这个还是结合实例进行了解比较合适。


func runDefer(){
    defer func() {
        fmt.Println("3")
    }()//括号表示定义function之后直接执行

    fmt.Println("1")

    defer func(index string) {
        fmt.Println(index)
    }("2")//括号表示定义function之后直接执行,如果定义的function包含参数,括号中也要进行相应的赋值操作
}

/**
执行结果:
1
2
3
*/

执行该函数能看到顺序打印出了123三个数字,这就是defer的执行过程。其特点就是LIFO,先进后出,先指定的函数总是在后面执行,是一个逆序的执行过程。

defer在Go中也是经常被用到的,而且设计的极其巧妙,举个例子

file.Open()
defer file.Close()//该语句紧跟着file.Open()被指定

file.Lock()
defer file.Unclock()// 该语句紧跟着file.Lock()被指定

像这样需要开关或者其他操作必须执行的操作都可以在相邻的行进行执行指定,可以说很好的解决了那些忘记执行Close操作的痛苦。

defer面试题

 
package main
 
import (
    "fmt"
)
 
func main() {
    defer_call()
}
 
func defer_call() {
    defer func() { fmt.Println("打印前") }()
    defer func() { fmt.Println("打印中") }()
    defer func() { fmt.Println("打印后") }()
 
    panic("触发异常")
}

考点:defer执行顺序
解答:
defer 是后进先出。
panic 需要等defer 结束后才会向上传递。 出现panic恐慌时候,会先按照defer的后入先出的顺序执行,最后才会执行panic。

结果:
打印后
打印中
打印前
panic: 触发异常
 --- 
//摘自:https://blog.csdn.net/weiyuefei/article/details/77963810
func calc(index string, a, b int) int {
    ret := a + b
    fmt.Println(index, a, b, ret)
    return ret
}
 
func main() {
    a := 1
    b := 2
    defer calc("1", a, calc("10", a, b))
    a = 0
    defer calc("2", a, calc("20", a, b))
    b = 1
}

考点:defer执行顺序
解答:
这道题类似第1题 需要注意到defer执行顺序和值传递 index:1肯定是最后执行的,但是index:1的第三个参数是一个函数,所以最先被调用calc("10",1,2)==>10,1,2,3 执行index:2时,与之前一样,需要先调用calc("20",0,2)==>20,0,2,2 执行到b=1时候开始调用,index:2==>calc("2",0,2)==>2,0,2,2 最后执行index:1==>calc("1",1,3)==>1,1,3,4

结果:
10 1 2 3
20 0 2 2
2 0 2 2
1 1 3 4

---
摘自:  https://blog.csdn.net/weiyuefei/article/details/77963810

defer 虽然是基础知识,其调用过程也非常好理解,但是往往在面试的过程中会出现一些比较绕的题目,这时候不要惊慌,只需要好好思考其执行的过程还是可以解出来的。

panic & recover

panic英文直译是恐慌,在Go中意为程序出现了崩溃。recover直译是恢复,其目的就是恢复恐慌。

在其他语言里,宕机往往以异常的形式存在。底层抛出异常,上层逻辑通过 try/catch 机制捕获异常,没有被捕获的严重异常会导致宕机,捕获的异常可以被忽略,让代码继续运行。
Go 没有异常系统,其使用 panic 触发宕机类似于其他语言的抛出异常,那么 recover 的宕机恢复机制就对应 try/catch 机制。-- 摘自:C语言中文网

panic

程序崩溃就像遇到电脑蓝屏时一样,大家都不希望遇到这样的情况。但有时程序崩溃也能终止一些不可控的情况,以此来做出防范。出于学习的目的,咱们简单了解一下panic造成的崩溃,以及如何处理。先看一下panic的定义。

// The panic built-in function stops normal execution of the current
// goroutine. When a function F calls panic, normal execution of F stops
// immediately. Any functions whose execution was deferred by F are run in
// the usual way, and then F returns to its caller. To the caller G, the
// invocation of F then behaves like a call to panic, terminating G's
// execution and running any deferred functions. This continues until all
// functions in the executing goroutine have stopped, in reverse order. At
// that point, the program is terminated and the error condition is reported,
// including the value of the argument to panic. This termination sequence
// is called panicking and can be controlled by the built-in function
// recover.
func panic(v interface{})

从定义中可以了解到,panic可以接收任何类型的数据。而接收的数据可以通过recover进行获取,这个后面recover中进行讲解。

从分类上来说,panic的触发可以分为两类,主动触发和被动触发。

在程序运行期间,主动执行panic可以提前中止程序继续向下执行,避免造成更恶劣的影响。同时还能根据打印的信息进行问题的定位。

func runSimplePanic(){
    defer func() {
        fmt.Println("before panic")
    }()
    panic("simple panic")
}

/**
调用runSimplePanic()函数结果:
before panic
panic: simple panic

goroutine 1 [running]:
main.runSimplePanic()
    /Users/fyy/go/src/github.com/souyunkutech/gosample/chapter6/main.go:102 +0x55
main.main()
    /Users/fyy/go/src/github.com/souyunkutech/gosample/chapter6/main.go:18 +0x22
*/

从运行结果中能看到,panic执行后先执行了defer中定义的函数,再打印的panic的信息,同时还给出了执行panic的具体行(行数需要针对具体代码进行定论),可以方便的进行检查造成panic的原因。

还有在程序中不可估计的panic,这个可以称之为被动的panic,往往由于空指针和数组下标越界等问题造成。

func runBePanic(){
    fmt.Println(ss[100])//ss集合中没有下标为100的值,会造成panic异常。
}

/**
调用runBePanic()函数结果:
panic: runtime error: index out of range

goroutine 1 [running]:
main.runBePanic(...)
    /Users/fyy/go/src/github.com/souyunkutech/gosample/chapter6/main.go:106
main.main()
    /Users/fyy/go/src/github.com/souyunkutech/gosample/chapter6/main.go:21 +0x10f
*/

从运行结果中看到,数组下标越界,直接导致panic,panic信息也是有Go系统运行时runtime所提供的信息。

recover

先来简单看一下recover的注释。

// The recover built-in function allows a program to manage behavior of a
// panicking goroutine. Executing a call to recover inside a deferred
// function (but not any function called by it) stops the panicking sequence
// by restoring normal execution and retrieves the error value passed to the
// call of panic. If recover is called outside the deferred function it will
// not stop a panicking sequence. In this case, or when the goroutine is not
// panicking, or if the argument supplied to panic was nil, recover returns
// nil. Thus the return value from recover reports whether the goroutine is
// panicking.
func recover() interface{}

注释指明recover可以管理panic,通过defer定义在panic之前的函数中的recover,可以正确的捕获panic造成的异常。

结合panic来看一下recover捕获异常,并继续程序处理的简单实现。


import "fmt"

func main() {
    runError()

    fmt.Println("---------------------------")
    runPanicError()
}

type Student struct {
    Chinese int
    Math    int
    English int
}

var ss = []Student{{100, 90, 89},
    {80, 80, 80},
    {70, 80, 80},
    {70, 80, 60},
    {90, 80, 59},
    {90, 40, 59},
    {190, 40, 59},
    {80, 75, 66},
}

func runError() {

    i := 0

    for ; i < len(ss); i++ {
        flag, err := checkStudent(&ss[i])
        if err != nil {
            fmt.Println(err)
            return
        }//遇到异常数据就会立即返回,不能处理剩余的数据
        //而且,正常逻辑中参杂异常处理,使得程序并不是那么优雅

        fmt.Printf("student %#v,及格? :%t \n", ss[i], flag)
    }

}

func checkStudent(s *Student) (bool, error) {
    if s.Chinese > 100 || s.Math > 100 || s.English > 100 {
        return false, fmt.Errorf("student %#v, something error", s)
    }

    if s.Chinese > 60 && s.Math > 60 && s.English > 60 {
        return true, nil
    }

    return false, nil
}

func runPanicError() {
    i := 0
    defer func() {
        if err := recover(); err != nil {
            fmt.Println(err)
        }
        i ++//跳过异常的数据,继续处理剩余的数据
        for ; i < len(ss); i ++ {
            fmt.Printf("student %#v,及格? :%t \n", ss[i], checkStudentS(&ss[i]))
        }
    }()

    for ; i < len(ss); i++ {
        fmt.Printf("student %#v,及格? :%t \n", ss[i], checkStudentS(&ss[i]))
    }

}

func checkStudentS(s *Student) bool {
    if s.Chinese > 100 || s.Math > 100 || s.English > 100 {
        panic(fmt.Errorf("student %#v, something error", s))
    }

    if s.Chinese > 60 && s.Math > 60 && s.English > 60 {
        return true
    }

    return false
}
结果:

student main.Student{Chinese:100, Math:90, English:89},及格? :true 
student main.Student{Chinese:80, Math:80, English:80},及格? :true 
student main.Student{Chinese:70, Math:80, English:80},及格? :true 
student main.Student{Chinese:70, Math:80, English:60},及格? :false 
student main.Student{Chinese:90, Math:80, English:59},及格? :false 
student main.Student{Chinese:90, Math:40, English:59},及格? :false 
student &main.Student{Chinese:190, Math:40, English:59}, something error
---------------------------
student main.Student{Chinese:100, Math:90, English:89},及格? :true 
student main.Student{Chinese:80, Math:80, English:80},及格? :true 
student main.Student{Chinese:70, Math:80, English:80},及格? :true 
student main.Student{Chinese:70, Math:80, English:60},及格? :false 
student main.Student{Chinese:90, Math:80, English:59},及格? :false 
student main.Student{Chinese:90, Math:40, English:59},及格? :false 
student &main.Student{Chinese:190, Math:40, English:59}, something error
student main.Student{Chinese:80, Math:75, English:66},及格? :true 

从结果中可以看出runPanicError函数将全部正常的数据都输出了,并给出了是否及格的判断,runError并没有全部将数据输出,而是遇到错误就中止了后续的执行,导致了执行的不够彻底。

panic和recover的用法虽然简单,但是一般程序中用到的却很少,除非你对panic有着很深的了解。但也可以通过Panic来很好的美化自己的代码,从程序上看,runPanicError中的异常处理与正常逻辑区分开,也使得程序看起来非常的舒畅-_-!

相对于那些对panic和recover掌握非常好的人来说,panic和recover能随便用,真的可以御剑飞行那种;但是如果掌握不好的话,还是尽可能的使用相对简单但不失高效又能很好的解决问题的error来处理就好了,以此来避免过度的使用从而造成的意外影响。毕竟我们的经验甚少,复杂的事物还是交给真正的大佬比较合适。

总结

Go中的异常处理相对比Java这些有着相对完善的错误处理机制的语言来说,还是显得非常的低级的,这也是Go一直被大家诟病的一点,但Go的更新计划中也有针对异常处理的改善,相信用不了多久就能看到不一样的错误处理机制。

源码可以通过'github.com/souyunkutech/gosample'获取。

关注我们的「微信公众号」

【跟着我们学Golang】之异常处理


首发微信公众号:Go技术栈,ID:GoStack

版权归作者所有,任何形式转载请联系作者。

作者:搜云库技术团队

出处:https://gostack.souyunku.com/...

点赞
收藏
评论区
推荐文章
如何优雅的处理异常
Java语言按照错误严重性,从throwale根类衍生出Error和Exception两大派系。本文从异常的定义、处理异常的方式、如何优雅的抛出异常以及处理异常等方面来聊聊如何异常这件事
Wesley13 Wesley13
4年前
java异常处理
_1.异常的分类_Error:称为错误,有java虚拟机生成并抛出,包括动态链接失败、虚拟机错误等,程序对其不做处理。Exception:所以异常类的父类,其子类对应了各种各样可能出现的异常,一般需要用户显示的声明或捕获。RuntimeException:一类特殊的异常,如被0除,数组下标超范围等,其产生比较频繁,处理比较麻烦,如果显示
Wesley13 Wesley13
4年前
03.Android崩溃Crash库之ExceptionHandler分析
目录总结00.异常处理几个常用api01.UncaughtExceptionHandler02.Java线程处理异常分析03.Android中线程处理异常分析04.为何使用setDefaultUncaughtExceptionHandler前沿上一篇整体介绍了crash崩溃
Wesley13 Wesley13
4年前
Java 的Throwable、error、exception的区别
1.  什么是异常?异常本质上是程序上的错误,包括程序逻辑错误和系统错误。比如使用空的引用(NullPointerException)、数组下标越界(IndexOutOfBoundsException)、内存溢出错误等。Throwable类是Java语言中所有错误或异常的超类。有两个重要的子类:Exception(异常)和Error(错误),
Wesley13 Wesley13
4年前
初探 Objective
作者:Cyandev,iOS和MacOS开发者,目前就职于字节跳动0x00前言异常处理是许多高级语言都具有的特性,它可以直接中断当前函数并将控制权转交给能够处理异常的函数。不同语言在异常处理的实现上各不相同,本文主要来分析一下ObjectiveC和C这两个语言。为什么要把ObjectiveC和
Wesley13 Wesley13
4年前
Java异常
异常分为两种:Exception、ErrorException:异常,可以捕捉到,进行处理以后可以让程序继续正常执行Error:错误,不能捕捉,只能修改代码,重新执行ThrowableException(RuntimeException非运行时异常)throw:抛出指定的异常throws:用在方法声明处,声明该方法可能发生
Stella981 Stella981
4年前
PlayJava Day020
1.异常Exception补充:①错误(Error)指的是致命性错误,一般无法处理②异常以类的形式封装程序可以处理的异常对应的类是java.lang.Exception及其子类运行时异常对应的类是java.lang.RuntimeException错误异常对应的类是java.lang.Error③异常相关类的继承树:java.la
Wesley13 Wesley13
4年前
Java异常处理的最佳实践
Java异常处理的最佳实践为什么要有最佳实践我们在写程序是不可避免的要对代码进行异常处理,但是有时对异常的处理会使我们的程序变的更加糟糕,这是我们所不想看到的。所以,我们再进行异常处理时需要遵循一定的套路,来降低异常处理对我们程序的影响。异常产生的原因一般来说,java中的异常会
Wesley13 Wesley13
4年前
Java异常架构
Java异常简介Java异常是Java提供的一种识别及响应错误的一致性机制。Java异常机制可以使程序中异常处理代码和正常业务代码分离,保证程序代码更加优雅,并提高程序健壮性。在有效使用异常的情况下,异常能清晰的回答what,where,why这3个问题:异常类型回答了“什么”被抛出,异常堆栈跟踪回答了“在哪“抛出,异常信息
小万哥 小万哥
2年前
C++异常和错误处理机制:如何使您的程序更加稳定和可靠
在C编程中,异常处理和错误处理机制是非常重要的。它们可以帮助程序员有效地处理运行时错误和异常情况。本文将介绍C中的异常处理和错误处理机制。什么是异常处理?异常处理是指在程序执行过程中发生异常或错误时,程序能够捕获并处理这些异常或错误的机制。例如,当
小万哥 小万哥
1年前
C++ 异常处理机制详解:轻松掌握异常处理技巧
C异常处理C异常处理机制允许程序在运行时处理错误或意外情况。它提供了捕获和处理错误的一种结构化方式,使程序更加健壮和可靠。异常处理的基本概念:异常:程序在运行时发生的错误或意外情况。抛出异常:使用throw关键字将异常传递给调用堆栈。捕获异常:使用
数字精灵号
数字精灵号
Lv1
你就在旁边却感觉隔了一个世纪.
文章
7
粉丝
0
获赞
0