datetime:2022/1/16 11:33
author:nzb

Go2 新特性简明教程

Go 的演进

Go语言/golang 诞生于2007年,经过12年的发展,Go逐渐成为了云计算领域新一代的开发语言。Go语言在牺牲很少性能的情况下,语法简洁,功能强大。我是Python的重度用户,在学习Go时,却有一种在学习Python的感觉。并非语法相似,而是Go语言作为一门编译型语言,竟然能够像Python一样,少量的代码就能够完成尽可能多的事情。Go语言仿佛是C和Python的结合体。

Go是如何火起来的呢?我觉得有几个主要的原因,除了语言本身性能好,语法简单,易上手外。Go语言原生支持 GoroutineChannel ,极大地降低了并发和异步编程的复杂度。对于服务端编程,并发和异步尤其重要,相比之下,C++,Java等语言的并发和异步控制逻辑过于复杂。另外,杀手级应用Docker的出现起到了很大的推动作用。

Go语言也有很多令人诟病的地方,例如包管理机制,Go直到v1.6才默认开启了vendor机制,vendor机制非常简陋,简单说就是在项目目录下增加一个vendor文件夹,里面放第三方依赖。vendor机制是没有版本概念的,而且不能解决vendor目录嵌套的问题以及同名包函数冲突问题。后来社区涌现了大量的包管理工具,仅官方推荐的包管理工具就有15种之多,应用比较广泛的,如dep、govendor。直到v1.11,官方增加了Go modules机制,才算较为完整地解决了包管理的问题。

Go2 可以说是Go语言一个非常重要的里程碑,Go1 目前虽然已经到了1.12版本,事实上每一个版本很少涉及语法层面的变化,而且每个版本都是向前兼容的。较大的改动如下:

  • Go1.2 切片操作
var a = make([]int, 10)
var b = a[i:j:k]
  • Go1.4 for语言加强
// <= 1.3
for i, v := range x {
    // ...
}

for i := range x {
    // ...
}

// 1.4 新增
var times [5][0]int

for i := 0; i < len(times); i++ {
    // ...
}

for _ = range times {
    // ...
}
  • Go1.9 类型别名
type T1 = T2

Go 2 设计草案

为了进一步完善Go语言,提供更好的体验。Go语言社区目前发布了三类重要的设计草案,分别是错误处理(Error handling)、错误值(Error values)、泛型(Generics) ,这几个草案代表了社区重点关注的完善方向,但并不代表最终的实现。

错误处理(Error Handling)

Go1 的错误处理机制非常简单,通过返回值的方式,强迫调用者对错误进行处理,这种设计导致会在代码中写大量的 if 判断。例如:

func CopyFile(src, dst string) {
    r := os.Open(src)
    defer r.Close()

    w := os.Create(dst)
    io.Copy(w, r)
    w.Close()
}

IO操作容易引发错误,文件打开失败,创建失败,拷贝失败等都会产生错误。如果要对这个函数进行完整的错误处理,代码将变成这样:

func CopyFile(src, dst string) error {
    r, err := os.Open(src)
    if err != nil {
        return err
    }
    defer r.Close()

    w, err := os.Create(dst)
    if err != nil {
        return err
    }
    defer w.Close()

    if _, err := io.Copy(w, r); err != nil {
        return err
    }
    if err := w.Close(); err != nil {
        return err
    }
}

看似逻辑清晰,但不够优雅,充斥了大量重复的逻辑。这是Go错误处理机制的缺陷。同时,因为错误处理机制的繁琐,很多开发者在开发应用时,很少去检查并处理错误,程序的健壮性得不到保证。

为了解决这个问题,Go2 发布了一个设计草案供社区讨论,Go2将会完善错误处理机制,错误处理的语法将会简洁很多。

这个提案引入了handle errcheck关键字,上面的函数可以简化成:

func CopyFile(src, dst string) error {
    handle err {
        return fmt.Errorf("copy %s %s: %v", src, dst, err)
    }
    r := check os.Open(src)
    defer r.Close()

    w := check os.Create(dst)
    check io.Copy(w, r)
    check w.Close()
}

为什么不使用被Java、Python等语言采用的try关键字呢?比如写成:

data := try parseHexdump(string(hex))

上面的写法看似和谐,但try关键字直接应用在 error values 时,可读性就没那么好了:

data, err := parseHexdump(string(hex))
if err == ErrBadHex {
    ... special handling ...
}
try err

很明显,在这种场景下,check err显然比try err更有意义。

错误值(Error values)

同样由于错误处理机制设计得较为简陋,Go语言对Error values支持有限。任何值,只要实现了error 接口,都是错误类型。由于缺少细粒度的设计,在各种库当中,判断是否产生错误以及产生了哪类错误的方式多种多样,例如io.EOFos.IsNotExisterr.Error() 等,。另外,Go语言目前没有机制追溯到完整的错误链条。例如,

func funcB() error {
    if v, err := funcA(); if err != nil {
        return fmt.Errorf("connect to db: %v", err)
    }
}
func funcC() error {
    v, err := funcB()
    if err != nil {
        return fmt.Errorf("write users database: %v", err)
    }
}

funcC返回的错误信息是:

write users database: connect to db: open /etc/xx.conf: permission denied

每一层,用额外的字符串对错误进行封装,是目前最常用的方法,除了通过字符串解析,很难还原出完整的错误链条。

为了解决Error values缺少标准的问题,有2个提案,分别针对Error inspectionError formatting

  • 针对 Error inspection ,为error定义了一个可选的接口Unwrap,用来返回错误链上的下一个错误。
package errors

type Wrapper interface {
    Unwrap() error
}

例如,

// WriteError 实现 Unwrap 接口
func (e *WriteError) Unwrap() error { return e.Err }
  • 针对 Error format,定义了一个可选的接口Format,用来返回错误信息。
package errors

type Formatter interface {
    Format(p Printer) (next error)
}

例如,

func (e *WriteError) Format(p errors.Printer) (next error) {
    p.Printf("write %s database", e.Database)
    if p.Detail() {
        p.Printf("more detail here")
    }
    return e.Err
}

泛型(Generics)

Go语言当前可使用inferface{} ,允许函数参数和返回值是任何类型的值。但这过于灵活,很多时候需要在获取参数后使用类型断言,进而决定下一步的处理。对比C++/Java的标准容器,Go语言在泛型方面有很大不足,因此针对泛型的提案即希望弥补这方面的不足。提案希望能够支持以下功能:

type List(type T) []T
// 返回map的键
func Keys(type K, V)(m map[K]V) []K
// 去重过滤
func Uniq(<-chan T) <-chan T
// 合并
func Merge(chans ...<-chan T) <-chan T
// 使用自定义排序函数排序
func SortSlice(data []T, less func(x, y T) bool)

例如,我们需要返回一个map对象中所有的键,而希望这个键的类型可以是任意类型。

var ints List(int)
keysA := Keys(int, string)(map[int]string{1:"one", 2: "two"})
keysB := Keys(string, string)(map[string]string{"name":"geektutu", "age": "twenty"})
// [1, 2]

Go 2 新特性

Go2还未正式发布,发布后更新

参考:Go2 wiki - Github

results matching ""

    No results matching ""