前言: 由于去年的项目,合作方的服务使用的是Golang开发的,合作期间也进行过简单的探讨。加之时下Go、Rust这些语言大热🔥,最终决定把这门语言系统的学一下。开始以语言最基本特性和语法入手,毕竟都有过其他开发语言的学习经历,也基于Go语言自身的简洁性,学习和上手还都是比较顺畅的。学习的时候喜欢边看边敲做些笔记,前段时间又稍微整理完善了下就水成一份博客发一下吧。

  • 📖参考书籍:《Go语言趣学指南》(网上目前只找到英文版)、《Go语言实战》与《Go Web编程》,包含一些网上的博文与教程;
  • 🐶因为学习Golang之前,大家很多都是有了其他语言的基础,所以过于基本的语法就无需细究了;
  • 📓简单且系统地记录下相关知识点和一些自己的理解,源码地址:Github,博文:oschina传送门

NIL

可以将其与C#中的null类比。在Go中如果一个指针没有明确的指向,那么它的值就是nil。

var i int
var s string
var p *string

fmt.Printf("%v\n", i) //0
fmt.Printf("%v\n", s) // (空字符串)
fmt.Printf("%v\n", p) // <nil>

nil可能会引发Panic

如果对一个nil指针进行解引用会引发panic(引发Go程序崩溃的错误)。

var p *string
fmt.Printf("%v\n", p) // <nil>
fmt.Printf("%v\n", *p) //panic: runtime error: invalid memory address or nil pointer dereference

避免这种情况的方法可以在解引用之前先判断指针是否是nil

var nowhere *int

if nowhere != nil {
    fmt.Println(nowhere)
}

以往的编程经验告诉我们,在方法中如果入参或者接收者是指针类型,那么最好都要进行下空判断来确保安全。

func (p *person) birthday{
    if p == nil{
        return
    }
    p.age++
}

默认值是nil的情况

函数值

当变量被声明为函数类型,在没有被赋值的情况下,其就为nil值。

var fn func(a, b int) int
fmt.Println(fn == nil) //true

切片

同理,切片在声明之后没有使用复合字面量或者make函数赋值,其值便为nil。

var soup []string
fmt.Println(soup == nil) //true

但是一些内置函数和关键字都可以很好的解决nil切片的问题,比如len,append,caprange

//range可以处理nil
for _, ingredient := range soup {
    fmt.Println(ingredient)
}
//len、append也可以处理nil
fmt.Println(len(soup)) //0
soup = append(soup, "onion", "carrot")
fmt.Println(soup) //[onion carrot]

映射

同理,映射在声明之后没有使用复合字面量或者make函数赋值,其值便为nil。对nil映射的读取操作不会引发panic,但是写入操作则会引发panic

var souplist map[string]int
fmt.Println(souplist == nil) //true

measurement, ok := souplist["onion"]

if ok {
    fmt.Println(measurement)
}

for ingredient, measurement := range souplist {
    fmt.Println(ingredient, measurement)
}

//souplist["onion"] = 1 //panic: assignment to entry in nil map

接口

接口类型的变量在未被赋值时的零值是nil,并且它的接口类型和值都是nil。

var v interface{}
fmt.Printf("%T %v %v\n", v, v, v == nil) //<nil> <nil> true

值得注意的是,当接口类型的变量被赋值之后,接口就会在内部指向该变量的类型和值。先看下面的示例。

var v interface{}

var po *int
v = po
fmt.Printf("%T %v %v\n", v, v, v == nil) //*int <nil> false

在将po赋值给v之后,v的类型就变成了*int,虽然值仍然是nil,但是Go认定接口类型的变量只有在类型和值都为nil时才等于nil。所以v == nil的结果是false

//格式化变量 %#v 可以同时打印出变量的类型和值
fmt.Printf("%#v", v)    //(*int)(nil)

错误处理

处理错误

在Go语言中,error类型是专门为错误而设的一种内置类型,有点类型C#中的Exception类型(但是error不捕获也不会使程序崩溃)。由于Go允许函数有多个返回值,所以在Go语言中一种较为常见的写法来传递发生的错误的信息,就是将错误信息写在返回值(一般为最后一个返回值)。举个栗子

//第二参数为错误(error)类型
files, err := ioutil.ReadDir(".")

//如果其不为空,则是发生了异常
if err != nil {
    fmt.Println(err)
    os.Exit(1)
}

for _, file := range files {
    fmt.Println(file.Name())
}

优雅的错误处理

通过之前的说法,error都会在返回值中返回。接下来有一个需求是写一个函数,函数中创建一个文件并向其中写入文本。根据以往经验告诉我们,在执行这些功能时随时都有可能发生异常,文件创建时名称不合法,权限不足,目录不存在等等,在写入文本时也会遇到各种异常,这就需要我们针对所有可能发生的异常进行相应的处理。这个函数一般的写法可以这样写。

func proverbs(name string) error {
	//创建文件
	f, err := os.Create(name)
	if err != nil {
		return err
	}
	//写文本信息
	_, err = fmt.Fprintln(f, "Errors are values.")
	if err != nil {
		f.Close()
		return err
	}
	//写文本信息
	_, err = fmt.Fprintln(f, "Don't just check errors, handle them gracefully.")
	if err != nil {
		f.Close()
		return err
	}
	//写文本信息
	_, err = fmt.Fprintln(f, "Don't Panic.")
	f.Close()
	return err
}

功能可以正常实现,但是容易发现这其中存在两个明显的问题。

  1. 在每次出现错误后都需要显式的调用f.Close()
  2. 每次写一行文本信息都要检测异常,语法显得很臃肿

关键字defer

为了保证文件能够正确被关闭(f.Close()),可以使用defer关键字。defer是延迟的意思,defer关键字的功能就是延迟执行被它标记的操作。被defer标记的操作,Go语言会在函数返回之前触发。有点像在C#的try...cathc...finally中,我们将这些操作写在finally块中的道理一样。
使用defer关键字之后的代码

func proverbsWithDefer(name string) error {
	f, err := os.Create(name)
	if err != nil {
		return err
	}
	//使用defer关键字,表示在函数退出之前,执行f.Close()
	defer f.Close()

	_, err = fmt.Fprintln(f, "Errors are values.")
	if err != nil {
		return err
	}
	_, err = fmt.Fprintln(f, "Don't just check errors, handle them gracefully.")
	if err != nil {
		return err
	}
	_, err = fmt.Fprintln(f, "Don't Panic.")
	return err
}

错误处理

我们可以声明一个新的类型safeWriter,在写入文件的过程中发生了错误,那么它将错误存储起来而不是直接返回它,之后当writerln尝试在此写入相同的文件时,如果发现之前已有错误,那么将不会再执行后续的操作。

type safeWriter struct {
	w   io.Writer
	err error
}

func (sw *safeWriter) writeln(s string) {
	if sw.err != nil {
		return
	}
	_, sw.err = fmt.Fprintln(sw.w, s)
}

func proverbsGracefully(name string) error {
	f, err := os.Create(name)
	if err != nil {
		return err
	}
	defer f.Close()
	sw := safeWriter{w: f}
	sw.writeln("Errors are values.")
	sw.writeln("Don't just check errors, handle them gracefully.")
	sw.writeln("Don't Panic.")
	return sw.err
}

这种写法背后的思想比写法本身重要的多。

新的错误

在出现错误时,我们可以通过创建并返回新的错误值来通知调用者出现了什么问题。在C#中我们是可以通过继承Exception作为基类来创建自定义的异常类型,那在Go中,error包包含了一个构造函数,它接受一个代表错误消息的字符串作为参数。通过这个构造函数可以创建并返回自定义的错误。
接下来用一个数独的例子来举例。

const rows, columns = 9, 9

//模拟一个9*9的数独网格
type Grid [rows][columns]int8

func inBound(row, column int) bool {
	if row < 0 || row >= rows {
		return false
	}
	if row < 0 || row >= columns {
		return false
	}
	return true
}

func (g *Grid) Set(row, column int, digit int8) error {
	if !inBound(row, column) {
		return errors.New("out of bound")
	}
	g[row][column] = digit
	return nil
}

func main() {
	var g Grid
	myErr := g.Set(10, 0, 5)
	if myErr != nil {
		fmt.Printf("An error occurred: %v\n", myErr)  //An error occurred: out of bound
		os.Exit(1)
	}
}

按需返回错误

在Go的很多包中,都会声明并导出一些变量用来表示他们可能会返回的错误。继续延续之前的数独的例子,可以声明两个错误变量。

var (
	ErrBounds = errors.New("out of bounds")
	ErrDigit  = errors.New("invalid digit")
)

【注意】 按照惯例,Go的错误类型都用Err打头。

声明之后,我们就不用去临时声明errors.New("out of bounds")了,直接返回ErrBound就可以了。

if !inBound(row, column) {
		//return errors.New("out of bound")
		return ErrBounds
	}

返回特定的错误,方法的调用者就可以根据具体的错误类型进行不同的错误处理了。

自定义错误类型

虽然errors.New()可以创建自定义的错误消息,但是有时候还是不够用。error类型是一个内置的接口,无论什么类型,只要实现了一个返回字符串的Error()方法,就隐式满足了error接口,这样就可以基于这个接口创建出新的错误类型。

type error interface{
    Error() string
}

返回多个错误

当代码执行中遇到多个错误是,比如之前的数独代码,当传入的位置越界了,值又是一个非法值,那么这时候与其每次返回一个错误,不如让方法进行多次检查一次性返回所有错误。

type SudokuError []error

//Error返回一个或多个用逗号分隔的错误
func (se SudokuError) Error() string {
	var s []string
	for _, err := range se {
		s = append(s, err.Error())
	}
	return strings.Join(s, ", ")
}

func (g *Grid) Set(row, column int, digit int8) error {
	var errs SudokuError
	if !inBound(row, column) {
		//return errors.New("out of bound")
		//return ErrBounds
		errs = append(errs, ErrBounds)
	}
	if !validDigit(digit) {
		errs = append(errs, ErrDigit)
	}
	if len(errs) > 0 {
		return errs
	}
	g[row][column] = digit
	return nil
}

类型断言

上面的例子中,返回值之前会将值从SudokuError类型转为error接口类型,如果想单独访问每个错误就必须进行类型转换。

var g Grid
errs := g.Set(10, 0, 15)
if errs != nil {
    if sudokuError, ok := errs.(SudokuError); ok {
        fmt.Printf("%d error(s) occurred:\n", len(sudokuError))
        for _, e := range sudokuError {
            fmt.Printf("- %v\n", e)
        }
    }
    os.Exit(1)
}

上面errs.(SudokuError)断言err的类型为SudokuError

不要惊恐(Panic)

Go语言中没有提供异常机制,但是有名为panic的类似机制,前面也都有提及。如同C#中的Exception出现一样,Go遇到Panic后,程序会崩溃。在其他语言中,如果发生异常,没有人捕捉的话这个异常会一层一层的向上抛,一直抛到main函数之类的调用栈顶。处理这些异常会用到大量的try...catch...finally...throw...等等。相比之下Go语言的错误值提供了一个简单且灵活的机制来替代异常,促使开发者考虑错误,而不是像异常处理那样默认将其忽略,有助于生成更可靠的软件。

  • 如果想要引发恐慌panic,可以这样
panic("OMG, i'm sorry")

【注意】 panic在退出前会执行所有defer延迟的操作,而os.Exit(1)则不会这样,所以panic比os.Exit(1)还好点。当然,择情处理。

  • 当然Go也提供了“反悔”的办法,为了防止panic让程序崩溃,可以使用recover函数
defer func() {
    if e := recover(); e != nil {
        fmt.Println(e) //OMG, i'm sorry
    }
}()

panic("OMG, i'm sorry")