不一样的go语言-error
前言
go语言的error处理方式,在目前流行的编程语言中属于刺头。似乎天生就是用来有别于他人标记。TIOBE排行榜全十除了C语言,无一例外是try catch的阵营。而排在go之前的语言除了C与perl外,同样是try catch的忠实拥趸。那么go的设计者为什么要这么做呢,只是为博人眼球吗?
关于error
在go语言的定义中,error不一定表示一个错误,它也可以表示其他信息。在标准库中可以看到如文件尾io.EOF的定义,而第三方库中亦有如jdbc驱动中的sql.ErrNoRows的使用,由此可见,在go中error完全可以看作是一种特殊的返回值,以帮助调用方获知被调用函数的执行情况而决定后续的代码逻辑。error的设计,按照流行的说辞就是让程序员面向异常编程,脑海里要时刻记得处理error。虽然这并没有什么不对,毕竟现在也提倡防御性编程,但就因为eerror的笼统且go支持多返回值,导致错误处理代码严重影响正常的业务逻辑,多返回值带来的判断组合成倍的增长。我认为go的error设计并没有摆脱C语言的影子,甚至只是换了个方式来表达而已,仍然是值模式。此时的go 2.0草案已着手解决这个问题,只是终究还是回归了类try catch模式。
//当前go 1的错误处理方式
f, err := os.Open(fileName)
if err != nil {
    // handle error here
}
//使用GO 2草案中的错误处理方式
func CopyFile(src, dst string) error {
	handle err {//类try-catch模式
		return fmt.Errorf("copy %s %s: %v", src, dst, err)
	}
	r := check os.Open(src)
	defer r.Close()
	w := check os.Create(dst)
	handle err {
		w.Close()
		os.Remove(dst) // (only if a check fails)
	}
	check io.Copy(w, r)
	check w.Close()
	return nil
}
最佳实践
吐槽归吐槽,但还是要回归主题。go的异常处理由error、panic、recover组成,可在作用上等同于java的Exception、throw、catch/finally。异常处理是任何一门编程语言都需要考虑如何处理的事情,特别是对于想要成为系统性、项目性的语言。go的异常处理的特殊性使得其在诞生之初,就被吐槽得最多,因而也被研究实践得最多。不同的人使用同一种语言敲出的代码可以有完全不一样的感觉,自然也就有良莠之分。所以语言永远都只是一种工具,如何使用工具及如何用好工具才是关键。就像用java抛出Exception,当throws的细分异常超过一个时,你会立刻马上想到抛出一个大Exception。这就是工具的滥用,相信设计者最初的目的决不在此。java异常的严谨性在于可以让调用者清楚地看到将要调用的方法的可控性,无异常抛出声明的方法可以放心调用,不需要处理异常;反正则可以知道被调用的方法会抛出哪些细分的异常,然后在调用的地方小心地处理这些异常(虽然java的try-catch语法很啰嗦,也很丑)。当然这一切都建立在所有的团队成员都深刻地认识到异常的最佳实践的前提下。go也不例外,也有一些最佳实践。
  java将异常分为错误(error)、未检视异常(unchecked exception)、已检视异常(checked exception),未检视、已检视异常这两个概念即使多年的老鸟也依然分不清楚,包括我在内。其实简单地划分就是当前可妥善处理或当前无能为力。已检视异常属于当前可妥善处理的范围,即
程序可对这类异常提供分支、恢复等处理方案的异常,比如FileNotFoundException,解决方案可以是读取备选文件或者跳过;而error、未检视异常则属于当前无能为力的异常(在写代码的时候不知道或者意料不到),这类异常通常是在运行时才能发现且通常是程序员的错或者依赖的运行环境异常(如操作系统、JVM)。而go在规范上则不区分,并且建议要妥善处理每一个方法返回的异常,哪怕是只打印一行日志。一个典型的go处理异常的例子如下:
package main
import (
    "fmt"
	"github.com/go-redis/redis"
)
func main() {
    defer func() {
        //尝试处理panic
        //注意: recover只在defer函数中有效
        if p := recover(); p != nil {
            err, ok := p.(error)
            if ok {
                //此处只是简单的打印error
                fmt.Printf("panic, %s\n", err.Error())
            }
            //打印异常堆栈
			fmt.Printf("panic stack: %s\n", string(debug.Stack()))
		}
    }()
    //连接redis
    client := redis.NewClient(&redis.Options{
    	Addr: "localhost",
    	DB: 0,
    	MinIdleConns: 10,
    })
    pong, err := client.Ping().Result()
    if err != nil {
        //异常发生, 程序中断执行,转而执行defer声明的函数
    	panic(err)
    }
    fmt.Printf("连接redis成功, ping: %s\n", pong)
}
从上述代码中可以看到,如果调用了很多可能发生错误的方法,整个代码视界内,将会出现一堆的if err!= nil这样的语句,然后里面就一个打印错误日志的语句。总之让人有点不爽,但习惯了其实也还好。那么对于自己编写的方法,应该如何定义error呢?
| 最佳实践 | 方式 | 说明 | 
|---|---|---|
| 位置 | 最后 | 返回值列表最后一个值且不要返回多个error | 
| 统一 | 同类型的错误,消息体保持一致 | 使用error.New创建error或者使用pkg/errors包 | 
| 二值 | bool | 布尔逻辑类的方法使用bool代替error | 
| 释放资源 | 在操作资源时,定义defer函数用于资源释放 | defer函数会被按先后顺序入栈,执行时按相反的顺序出栈执行 | 
| 非法分支 | 按业务逻辑不应该出现的分支可使用panic中止执行,并交由recover处理 | - | 
| 非法参数 | 对于Must类开头的函数,使用panic | 这种设计可以避免调用方处理error,但调用方需小心处理panic,运行的程序不应该因为panic崩溃 | 
我认为异常处理的关键在于如何告知调用方你写的函数出错了,哪里出错了,出了什么错,然后交由调用方处理(语言给调用方提供工具中断程序或恢复异常等方式)。在这个指导方针下,我觉得java显然对调用方更为友好,从方法声明就可以知道方法会抛出哪些异常,不需要查看源代码,然后一个大代码块的try,catch各个细分的Exception(不分青红皂白直接catch Exception的程序员不是好程序员)。
除此之外,go有很多增加魔力值的点,比如type、defer、类型推断、闭包、channel、无括号、cgo,不足之处在于标准库太弱、没有泛型、切片与数组易混。此外,go没有函数重载,虽然这并不是什么大问题。
请关注公众号

不一样的go语言-error的更多相关文章
- GO语言Error处理
		Go语言没有提供像Java.C#.Python语言中的try...catch异常处理方式,而是通过函数返回值逐层往上抛.好处就是避免漏掉本应处理的错误.坏处是代码啰嗦. 错误与异常区别 错误指的是可能 ... 
- c语言 error C4996: 'strupr': The POSIX name for this item is deprecated. Instead, use the ISO C and C++ conformant name
		问题: 在使用visual studio 2013,进行调试执行代码时,出现如下错误: error C4996: 'strupr': The POSIX name for this item is d ... 
- c语言-error C2440: “static_cast”: 无法从“UINT (__thiscall CHyperLink::* )(CPoint)”转换为“LRESULT (__thiscall CWnd::* )(CPoint)”
		出现这个错误的原因可是“人力不可抗拒”之原因造成的,因为旧版本的 ON_WM_NCHITTEST 宏使用了 UINT (__thiscall CWzButton::* )(CPoint); 类型的类成 ... 
- 07. Go 语言接口
		Go 语言接口 接口本身是调用方和实现方均需要遵守的一种协议,大家按照统一的方法命名参数类型和数量来协调逻辑处理的过程. Go 语言中使用组合实现对象特性的描述.对象的内部使用结构体内嵌组合对象应该具 ... 
- golang error错误处理
		error定义 数据结构 go语言error是一普通的值,实现方式为简单一个接口. // The error built-in interface type is the conventional i ... 
- flask框架(二):简单的登录demo
		一:main.py # -*- coding: utf-8 -*- # @Author : Felix Wang # @time : 2018/7/3 22:58 from flask import ... 
- 3_JSP
		一. 引言 1.1 现有问题 在之前学习Servlet时, 服务器通过Servlet响应客户端页面, 有什么不足之处? 开发方式麻烦: 继承父类, 覆盖方法, 配置web.xml或注解 代码修改麻烦: ... 
- [转载]VS2012编译C语言scanf函数error的解决方法
		在VS 2012 中编译 C 语言项目,如果使用了 scanf 函数,编译时便会提示如下错误: error C4996: 'scanf': This function or variable may ... 
- 与非java语言使用RSA加解密遇到的问题:algid parse error, not a sequence
		遇到的问题 在一个与Ruby语言对接的项目中,决定使用RSA算法来作为数据传输的加密与签名算法.但是,在使用Ruby生成后给我的私钥时,却发生了异常:IOException: algid parse ... 
随机推荐
- Confluence 6 跟踪你安装中的自定义修改
			在 Confluence 中的系统信息(System Information)部分,有一个 修改(Modification)的选项.在这个选项中列出了自你 Confluence 安装以来,你 Conf ... 
- 【VBA】数组定义时,括号内的数值n为最大下标,其长度为n+1
			定义数组 dim arr(9) as integer注意这是数组的长度为10,而9指的是最大下标值. 所以在redim和赋值的时候要特别小心,防止错位. 
- python之多线程通信
			共享变量通信 事实上共享变量通信是会造成线程安全的,除非我们对这个共享变量是有足够了解的,如非必要就不要使用共享变量在线程间进行通信 Queue通信 理解不深入,暂不写 
- mysql数据库之基本操作和存储引擎
			一.知识储备 数据库服务器:一台计算机(对内存要求比较高) 数据库管理系统:如mysql,是一个软件 数据库:oldboy_stu,相当于文件夹 表:student,scholl,class_list ... 
- django----Form详细信息
			Form类: 创建Form类时,主要涉及到 [字段] 和 [插件],字段用于对用户请求数据的验证,插件用于自动生成HTML; Django内置字段 Field required=True, 是否允许为 ... 
- MariaDB修改默认字符集
			MariaDB修改默认字符集,以及创建数据库授权 1 . 修改server默认字符集utf8 [root@aws my.cnf.d]# vim /etc/my.cnf.d/server.cnf [my ... 
- 实现用VB.Net/(C#)开发K/3 BOS 插件的真正可行方法
			转了这一篇文章,原来一直想用C#做k3的插件开发,vb没有C#用的爽呀,这篇文章写与2011年,看来我以前没有认真去找这个方法呀. https://blog.csdn.net/chzjxgd/arti ... 
- EF Fluent API
			EntityTypeConfiguration 它可以为一个实体类,配置一个模型. Entity Framework 6 一次加载许多个 Fluent API 映射 如果对于许多实体有大量映射,则 O ... 
- Redis的搭建和Redis的集群搭建
			1.Redis的官网:https://redis.io/ Redis的测试网站:http://try.redis.io/ 2.参考博客:https://www.cnblogs.com/maf ... 
- [转] 设置div的overflow:scroll,但是在手机上滑动的时候有点卡顿
			设置div的overflow:scroll,但是在手机上滑动的时候有点卡顿,所以在这个div上加一个css: -webkit-overflow-scrolling : touch; 在苹果手机上使用- ... 
