go特性总结(1.13~1.16)


本文摘自网络,作者,侵删。

go1.13

1.对数字字面量进行了改动

在1.13版本之前的golang仅支持十进制和十六进制的字面量,而其他语言使用广泛的二进制和八进制却不能支持例如下面代码就无法编译:

    fmt.Println(0b101)

    fmt.Println(0o10)

在go1.13中上述字面量语法已经被支持了你可以通过0b或0B前缀来表明一个二进制数字的字面量,以及用0o和0O来表明八进制字面量。值得注意的是虽然两种写法都可以,但是gofmt默认会全部转换为小写

1.13以下版本运行则会出现以下错误信息:

    # command-line-arguments

    usercode/file.go:6: syntax error: unexpected name, expecting )

    usercode/file.go:7: syntax error: unexpected name, expecting )   

在1.13版本以前的版本并不支持通过0b或0B来表明一个二进制数字的字面量,甚至用0o和0O来表明八进制字面量也是同样不支持的

最后对于数字字面量还有一个小小的改进,那就是现在可以用下划线分隔数字增加可读性。

    fmt.Println(100000000)

    fmt.Println(1_0000_0000)

    fmt.Println(0xff_ff_ff)

1.13返回结果:

100000000

100000000

16777215

分隔符可以出现在任意位置,但是像0x之类的算是一个完整的符号的中间不可以插入下划线,分隔符之间字符的数量没有规定必须相等,但为了可读性最好按照现有的习惯每3个数字或四个数字进行一次分隔

1.13以下版本运行则会出现以下错误信息:

# command-line-arguments

./code.go:9: syntax error: unexpected _0000_0000, expecting comma or )

./code.go:10: syntax error: unexpected _ff_ff, expecting comma or )

在1.13版本以前的版本并不支持通过下划线分割数字来增加可读性

2.越界索引报错的完善(运行时改进)

首先golang对数组和slice的越界引用是0容忍的,一旦越界就会panic,例如下面例子

package main

import "fmt"

func main() {

        arr := [...]int{1,2,3,4,5}

        for i := 0; i <= len(arr); i++ {

                fmt.Println(arr[i])

        }

}

1.13以下版本运行则会出现以下错误信息:

1

2

3

4

5

panic: runtime error: index out of range

1.13以前版本panic并不会把越界的值输出出来,虽然调用堆栈信息追溯起来不是很困难,可以方便得定位问题,但如果调用链较深或者你处于一个高并发程序之中,事情就变得麻烦了,要么我们日志调试并最终分析排除大量杂音来定位问题,要么依赖断点进行单步调式,无论哪种都需要耗费大量的精力而核心问题只是我们想到为什么会越界,再浅一步,我们有时候或许只要知道导致越界的值就可以大致确定问题的原因,遗憾的是panic提供的信息中不包含上述内容,直到golang1.13。而现在golang会将导致越界的值打印出来,无疑是雪中送碳:

1

2

3

4

5

panic: runtime error: index out of range [5] with length 5

goroutine 1 [running]:

main.main()

        /Users/chengaosheng/go/test/test.go:46 +0xec

exit status 2

当然,panic信息再完善也不是灵丹妙药,完善的单元测试和严谨的工作态度才是bug最好的预防针。

3.工具链改进

除了去除了godoc程序,最大的变化仍旧集中在go modules上。

 3.1 GOPROXY

其实这个变量在1.12中就引入了,这次为其加上了默认值https://proxy.golang.org,direct, 这是一个逗号分隔的列表,后面两个变量的值和它相同,其中direct表示不经过代理直接连接,如果设置为off,则进制下载任何package。

在go get等命令获取package时,会从左至右依次查找,如果都没有找到匹配的package,则会报错。

proxy的好处自然不用多说,它可以使国内开发者畅通无阻地访问某些国内环境无法获取的包。更重要的是默认的proxy是官方提供和维护的,比起第三方方案来说安全性有了更大的保障。

 3.2 GOSUMDB

这个变量实际上相当于指定了一个由官方管理的在线的go.sum数据库。具体介绍之前我们先来看看golang是如何验证packages的:

go get下载的package会根据go.mod文件和所有下载文件分别建立一个hash字符串,存储在go.sum文件中;

下载的package会被cache,每次编译或者手动go mod verify时会重新计算与go.sum中的值比较,出现不一致就会报安全错误。

这个机制是建立在本地的cache在整个开发生命周期中不会变动之上的(因为依赖库的版本很少会进行更新,除非出现重大安全问题),上述机制可以避免他人误更新依赖或是本地的恶意篡改,然而现在更多的安全问题是发生在远程环境的,因此这一机制有很大的安全隐患。

好在加入了GOSUMDB,它的默认值为“sum.golang.org”,国内部分地区无法访问,可以改为“sum.golang.google.cn”。现在的工作机制是这样的:

go get下载包并计算校验和,计算好后会先检查是否已经出现在go.sum文件中,如果没有则去GOSUMDB中检查,校验和一致则写入go.sum文件;否则报错

如果对应版本的包的校验和已经在go.sum中,则不会请求GOSUMDB,其余步骤和旧机制一样。

安全性得到了增强。

3.3 GOPRIVATE

最后要介绍的是GOPRIVATE,默认为空,你可以在其中使用类似Linux glob通配符的语法来指定某些或某一类包不从proxy下载,比如某些rpc套件自动生成的package,这些在proxy中并不会存在,而且即使上传上去也没有意义,因此你需要把它写入GOPRIVATE中。

还有一个与其类似的环境变量叫GONOPROXY,值的形式一样,作用也基本一样,不过它会覆盖GOPRIVATE。比如将其设为none时所有的包都会从proxy进行获取。

4.标准库的新功能

每次新版本发布都会给标准库带来大把的新功能新特性,这次也不例外。

4.1 判断变量是否为0值

golang中任何类型的0值都有明确的定义,然而遗憾的是不同的类型0值不同,特别是那些自定义类型,如果你要判断一个变量是否0值那么将会写出复杂繁琐而且扩展困难的代码。

因此reflect中新增了这一功能简化了操作:

package main

import (

        "fmt"

        "reflect"

)

func main() {

        a := 0

        b := 1

        c := ""

        d := "a"

        fmt.Println(reflect.ValueOf(a).IsZero()) // true

        fmt.Println(reflect.ValueOf(b).IsZero()) // false

        fmt.Println(reflect.ValueOf(c).IsZero()) // true

        fmt.Println(reflect.ValueOf(d).IsZero()) // false

}

当然,反射一劳永逸的代价是更高的性能消耗,所以具体取舍还要参照实际环境。

4.2 错误处理的革新

其实算不上革新,只是对现有做法的小修小补。golang团队始终有觉得error既然是值那就一定得体现value的equal操作的怪癖,所以整体上还是很怪。

首先要介绍错误链(error chains)的概念。

在1.13中,我们可以给error实现一个Unwrap的方法,从而实现对error的包装,比如:

type PermError {

        os.SyscallError

        Pid uint

        Uid uint

}

func (err *PermError) String() string {

        return fmt.Sprintf("permission error:\npid:%v\nuid:\ninfo:%v", err.Pid, err.Uid, err.SyscallError)

}

func (err *PermError) Error() string {

        return err.String()

}

// 重点在这里

func (err *PermError) Unwrap() error {

        return err.SyscallError

}

假设我们包装了一个基于SyscallError的权限错误,包括了所有因为权限问题而触发的error。String和Error方法都是常规的自定义错误中会实现的方法,我们重点看Unwrap方法。

Unwrap字面意思就是去包装,也就是我们把包装好的上一层错误重新分离出来并返回。os.SyscallError也实现了Unwrap,于是你可以继续向上追溯直达最原始的没有实现Unwrap的那个error为止。我们称从PermError开始到最顶层的error为一条错误链。

如果我们用→指向Unwrap返回的对象,会形成下面的结构:

PermError → os.SyscallError → error

还可以出现更复杂的结构:

A → Err1 ___________

|

V

B → Err2 → Err3 → error

这样无疑提升了错误的表达力,如果不想自己单独定义一个错误类型,只想附加某些信息,可以依赖fmt.Errorf:

newErr := fmt.Errorf("permission error:\npid:%v\nuid:\ninfo:%w", pid, uid, sysErr)

sysErr == newErr.(interface {Unwrap() error}).Unwrap()

fmt.Errorf新的占位符%w只能在一个格式化字符串中出现一次,他会把error的信息填充进去,然后返回一个实现了Unwrap的新error,它返回传入的那个error。另外提案里的Wrapper接口目前还没有实现,但是标准库用了我在上面的做法暂时实现了Wrapper的功能。

因为错误链的存在,我们不能在简单的用等于号基于判断基于值的error了,但好处是我们现在还可以判断基于类型的error。

为了能继续让error表现自己的值语义,errors包里增加了Is和As以及辅助它们的Unwrap函数。

Unwrap

errors.Unwrap会调用传入参数的Unwrap方法,As和Is使用它来追溯整个错误链。

像上一小节的代码就可以简化成这样:

newErr := fmt.Errorf("permission error:\npid:%v\nuid:\ninfo:%w", pid, uid, sysErr)

sysErr == errors.Unwrap(newErr).Unwrap()

我们提到等于号的比较很多时候已经不管用了,有的时侯一个error只是对另一个的包装,当这个error产生时另一个也已经发生了,这时候我们只需要比较处于上层的error值即可,这时候你就需要errors.Is帮忙了:

newErr := fmt.Errorf("permission error:\npid:%v\nuid:\ninfo:%w", pid, uid, sysErr)

errors.Is(newErr, sysErr)

errors.Is(newErr, os.ErrExists)

你永远也不知道程序会被怎样扩展,也不知道error之间的关系未来会怎样变化,因此总是用Is代替==是不会犯错的。

不过凡事总有例外,例如io.EOF就不需要使用Is去比较,因为它程序意义上算不上是error,而且一般也不会有人包装它。

As

除了传统的基于值的判断,对某个类型的错误进行处理也是一个常见需求。例如前文的A,B都来自error,假设我们现在要处理所有基于这个error的错误,常见的办法是switch进行比较或者依赖于基类的多态能力。

显而易见的是switch判断的做法会导致大量重复的代码,而且扩展困难;而在golang里没有继承只有组合,所以有运行时多态能力的只有interface,这时候我们只能借助错误链让errors.As帮忙了:

// 注意As的第二个参数只能是你需要判断的类型的指针,不可以直接传一个nil进去

var p1 *os.SyscallError

var p2 *os.PathError

errors.As(newErr, &p1)

errors.As(newErr, &p2)

如果p1和p2的类型在newErr所在的错误链上,就会返回true,实现了一个很简陋的多态效果。As总是用于替代if _, ok := err.(type); ok这样的代码。

当然,上面的函数一方面让你少写了很多代码,另一方面又严重依赖反射,特别是错误链很长的时候需要反复追溯多次,所以这里有两条忠告:

不要过渡包装,没什么是加间接层解决不了的,但是中间层太多不仅影响性能也会干扰后续维护;

如果你实在在意性能,而且保证不存在对现有error的扩展(例如io.EOF),那么使用传统方案也无伤大雅。

go1.14

 1.工具

1.1.go build等命令默认将会使用 -mod=vendor,如果需要使用mod cache需要显示指定 -mod=mod。

1.2.go mod init 设置go.mod文件是-mod=readonly,go.mod是只读模式的。

1.3.go mod tidy之外的go命令不再编辑go.mod文件

1.4.除非明确要求或已经要求该版本,否则 go get 将不再升级到该模块的不兼容主要版本。直接从版本控制中获取时,go list 还会忽略此模块的不兼容版本

1.5.在 module 模式下,go 命令支持 SVN 仓库

阅读剩余部分

相关阅读 >>

如何通过Go语言将utc和gmt时间处理为标准北京时间?

分析Go中的类型比较

Golang nil 小知识

什么是量化交易|量化交易平台

手撸Golang 行为型设计模式 命令模式

Golang使用kafka报错c.client.config.config.consumer.offsets.commitinterval undefined问题

安装Go第三方库的方法

Golang 入门系列(五)Go语言中的面向对象

[系列] Go - 学习 grpc.dial(target string, opts …dialoption) 的写法

php的局限性怎么破?php与Go可完美结合

更多相关阅读请进入《Go》频道 >>




打赏

取消

感谢您的支持,我会继续努力的!

扫码支持
扫码打赏,您说多少就多少

打开支付宝扫一扫,即可进行扫码打赏哦

分享从这里开始,精彩与您同在

评论

管理员已关闭评论功能...