Go语言基础教程

  • 1. Go语言简介
  • 2. Go语言基础
  • 2.1 Go语言中的数值类型
  • 2.2 数值类型详细解
  • 2.3 Go语言中其他的一些内容
  • 2.3 流程控制
  • 2.4 函数
  • 2.5 错误处理
  • 小结
  • 参考文献


1. Go语言简介

Go(又称 Golang)是 Google 的 Robert Griesemer,Rob Pike 及 Ken Thompson 开发的一种静态强类型、编译型语言。Go 语言语法与 C 相近,但功能上有:内存安全,GC(垃圾回收),结构形态及 CSP-style 并发计算。
Go语言作为一门全新的静态类型开发语言,与当前的开发语言相比具备众多令人兴奋不已的新特性。
Go语言最为主要的特性如下所示:

  • 自动垃圾回收
  • 更加丰富的内置类型
  • 函数多返回值
  • 错误处理
  • 匿名函数和闭包
  • 类型和接口
  • 并发编程
  • 反射
  • 语言交互性

Go语言中最为主要的特性简单介绍一下几种,在后面的学习 过程中我们会详细介绍和学习。
类型和接口: Go语言的类型定义非常接近于C语言中的结构(struct)类型,并沿用了struct关键字。相比较而言,Go语言并没有和C++/Java一样设计一个非常复杂的类型系统,并不支持继承和重载,而是支持了最基本的类型组合功能。它引入了非常强大的“非侵入式”接口的概念,让开发者从以往对C++和Java开发中的接口管理问题中解脱出来。
并发编程: Go语言引入了goroutine的概念,使得它变成非常简单。通过在函数调用前面使用关键字go,既可以让函数以goroutine方式来执行,goroutine是一种比线程更加轻盈、更加节省资源的协程。Go语言通过系统的线程来多路派遣这些函数的执行,使得每个用go关键字执行的函数可以运行成为一个单位协程。当一个协程阻塞的时候,调度器就会自动把其他协程安排到另外的线程中去执行,从而实现了程序无等待并行化运行。而且调度的开销非常小,一颗CPU调度的规模不下于每秒百万次,这使得我们能够创建大量的goroutine,从而可以很轻松地编写高并发程序,达到我们想要的目的。Go语言实现了CSP(通信顺序进程,Communicating Sequential Process)模型来作为goroutine间的推荐通信方式。
反射: 反射(reflection)是在Java语言出现后迅速流行起来的一种概念。通过反射,你可以获取对象类型的详细信息,并可动态操作对象。反射是把双刃剑,功能强大但代码可读性并不理想。若非必要,我们并不推荐使用反射。反射最常见的使用场景是做对象的序列化(serialization,有时候也叫Marshal & Unmarshal)。例如,Go语言标准库的encoding/json、encoding/xml、encoding/gob、encoding/binary等包就大量依赖于反射功能来实现。
语言的交互性: 在Go代码中,可以按Cgo的特定语法混合编写C语言代码,然后Cgo工具可以将这些混合的C代码提取并生成对于C功能的调用包装代码。开发者基本上可以完全忽略这个Go语言和C语言的边界是如何跨越的。

2. Go语言基础

2.1 Go语言中的数值类型

Go语言中,数据类型用于声明函数和变量。
数据类型的出现时为了把数据分成所需内存大小不同的数据,Go语言中通常有以下的几种数据类型:

  • 布尔类型:布尔数据类型可以是常量true或者false。
  • 数字类型:包含整数类型(uint8,uint16,uint32,uint64,int8,int16,int32,int64)、浮点数类型(float32,float64)、复数类型(complex64和complex128)。另外还包括有byte类型(类似uint8),rune类型(类似int32,字符类型),uint(32位或者64位),int(与uint一样大小),uintptr(无符号整型,用于存放一个指针)
  • 字符串类型:字符串就是一串固定长度的字符连接起来的字符序列,Go语言中的字符串的字节使用UTF-8编码标识Unicode文本。
  • 错误类型:error
  • 派生类型:包括以下的基本类型
  • (a) 指针类型;
  • (b) 数组类型;
  • (c) 结构化类型(struct);
  • (d) Channel类型;
  • (f) 切片类型;
  • (g) 接口类型(interface);
  • (h) Map类型。

变量的声明: Go语言中引入了var变量关键字来声明变量。声明变量的形式为

var identifier typename //声明一个变量
var identifier1,identifier2 typename //声明多个变量

举个例子:

var v1 int  //定义整型变量
var v2 string  //定义字符串变量
var v3 [20]float32  //定义浮点数数组变量
var v4 []uint  //定义无符号数组切片变量
var v5 struct{
    f int
}   //定义结构体变量
var v6 *int //定义一个指针
var v7 map[string]int //定义一个Map类型,key为string类型,value 为int类型
var v8 func(a int) int //定义一个函数变量
var(
    v9 int
    v10 uint64
) //多个变量一起声明

注意:Go语言中不需要使用分号作为结束标识符。
变量的初始化: 对于变量初始化,可以有以下的几种形式:

var v1 int =10
var v2 = 10 //编译器自动推导出v2的类型
v3 := 10 //编译器自动推导出v3的类型

注意“:=”左侧的变量不应该是已经被声明过的变量,否则会导致编译错误。
变量的赋值:

var v10 int
v10 = 50 // 表示变量的赋值操作
var v2 = 30
var v3 = 50
v2,v3 = v3,v2 //表示交换变量的值

常量:
字面常量指的是程序中硬编码的常量,另外Go语言中预定义了常量:true,false,iota。
举个例子,常量的定义如下所示:

const PI float64 = 3.1415926535
const zero = 0.0 //无类型的浮点数常量
const (
   size int64 = 1024
   eof = -1  //无类型整型常量
)
const u,v float32 = 0,3 //常量的多重赋值方式
const a,b,c = 3,4,"foo" //无类型整型和字符串常量

iota是一种比较特殊的预定义常量,在每一个const关键字出现时候被重置为0,然后在下一个const出现之前,每出现一次iota,其所代表的的数字会自动增加1。

const (//iota被重置为0
    c1 = iota
    c2 = iota
    c3 = iota
)//其中c1 =0,c2=1,c3 =2
const (
    i0 = 1 <<iota
    i1 = 1 <<iota
    i2 = 1 <<iota
)//其中i0=1,i1=2,i2=4
const(
    u =iota*42
    v float64 = iota*42
    w = iota*42
)//其中u=0,v = 42.0,w = 84
//可以省略itoa,例如下面的表示方法
const(
    a = iota
    b
    c
)//a =0,b=1,c=2

枚举常量是指的一系列相关系的常量,Go语言中不支持enum关键字的枚举常量,但可以有以下的定义方法:

const(
   Sunday = iota
   Monday
   Tuesday 
   Wednesday 
   Thursday 
   Friday 
   Saturday 
)

2.2 数值类型详细解

整型
整型是所有编程 语言中最为基础的数据类型。需要注意的是,int和int32在Go语言中被认为是两种不同的类型,编译器也不会将其做类型转换。例如下面的例子

var value2 int32
value1 := 64
value2 = value1 // 编译错误
value2 = int32(value1)//进行强制类型转换,编译正确

整型的数值运算包括+、-、*、/和%。
整型中比较运算符号包括>、<、==、>=、<=、!=这几种运算符。两种类型不同的整型不能作比较,但是各种类型的整型都可以直接与字面常量进行比较。
Go语言中支持位运算,位运算符号包括以下几种

运算

含义

x<<y

左移

x>>y

右移

x^y

异或

x&y

与运算

x

y

^x

取反

浮点数类型
浮点数包括两种类型float32和float64。注意浮点数不是一种精确表达的方式,并不能像整型一样==来判断两个浮点数是否相等,否则会导致不稳定的结果。
复数类型
复数类型是由两个实数构成,一个表示实部real,一个表示虚部imag。复数类型的声明表示如下所示:

var value complex64
value1 = 3.2 +12i
value2 := 3.2 +12i
value3 := complex(3.2,12)
var real_num = real(value) //获取实部
var imag_num = imag(value) //获取虚部

字符串类型
字符串类型是一种基本类型。举个例子:

var str string //声明字符串
str = "Hello"
ch := str[0] //取字符串中的第一个元素

注意,字符串不能够在初始化之后被修改。字符串操作有连接字符串(使用“+”连接字符串)、取字符串长度(使用内置函数len())、取字符(s[k])。举个例子:

str := "Hello word!"
n := len(str)
for i :=0;i<n;i++{
    ch := str[i]
    fmt.Println(i,ch);
}
for k,ch :=range str{
   fmt.Println(k,ch)
}

Go语言中支持两种方式遍历字符串,一种是以字节数组的方式遍历字符串,这种情况下,每一个字符类型都是byte类型;另一种是以Unicode字符遍历字符串,这种情形下,则字符类型变为rune。
数组类型
数组是Go语言中最为常见的数据结构之一。以下为常见的几种数组的声明方法:

var value1 [32]byte  //长度为32的byte数组
var value2 [2*N]struct{x,y int32} //复杂类型的数组
var value3 [1000]*float64  //指针数组
var value4 [3][5]int //二维数组
var value5 [2][2][2]float64 //三维数组

注意,数组长度确定之后就不能够再更改。可以使用数组下标来访问数组中的元素,数组下标从0开始,len(array)-1表示的是最后一个元素的下标。举个例子:

for i:=0;i<len(array);i++{
   fmt.println("Element ",i," of array is ",array[i])
}

Go语言中还提供一个关键字range,用于便捷地遍历容器中的元素。当然,数组也是range支持的范围,range关键字返回容器的key以及对应的value值。需要注意的是,Go语言中数组是一个值类型,所有的值类型变量在赋值和作为参数传递时候都将会产生一次复制的动作。如果将数组作为函数的参数类型,则在调用的时候参数的数据将发生数据的复制,并不能修改传入的数组内容。所以函数内的操作均是所传入数组的一个副本信息。
数组切片
数组在创建之后,它的长度并不能修改,但是数组切片的操作弥补了这样的一个缺点。数组切片是一个Go语言中重要的数据类型,数组切片的数据结构可以抽象为以下的三个变量:

  • 一个指向原生数组的指针;
  • 数组切片中的元素的个数;
  • 数组切片已经分配的存储空间。
    数组切片得创建有两种方法,一种是基于数组,一种是直接创建。
    举个例子:
package main
import "fmt"
func main(){
   //定义数组
   var myarr [10]int = [10]int{1,2,3,4,5,6,7,8,9,10}
   var myslice []int = myarr[:6]
   fmt.Println("The elements of myarr ")
   for _,v := range myarr{
      fmt.Print(v," ")
   }
   fmt.Println()
   fmt.Println("The elements of myslice ")
   for _,v := range myslice{
      fmt.Print(v," ")
   }
   fmt.Println()
}

这是一个比较完整一点的例子。可以看到,Go语言支持使用myarr[first:last]的方法来对数组进行切片操作。另一种是直接创建的操作,举个例子:

myslice1 := make([]int,5) //创建一个初始元素个数为5的数组切片,元素的初始值为0
myslice2 := make([]int,5,10) //创建一个初始元素为5的数组切片,元素的初始值为0,并且预留10个元素的存储空间
myslice3 := []int{1,2,3,4,5,6,7,8,9,10,} //直接创建并初始化包含10个元素的数组切片

元素的遍历方法和数组相同。数组切片另外一个强大的功能就是能够实现动态地增减元素的个数。cap()函数是返回数组切片分配的空间大小,len()函数是返回数组切片中当前所存储的元素个数。
也可以在数组切片的后面继续添加元素,如下面的append()的内置函数:

myslice = append(myslice,1,2,3)
tmpslice := []int{89,56,71}
myslice = append(myslice,tmpslice...)

copy()内置函数是用于将内容从一个数组切片复制到另外一个数组切片的操作,大小不一的时候回按照较小的数组切片来进行内容的复制操作。举个例子

slice1 := []int{1,2,3,4,5}
slice2 := []int{7,8,9}
copy(slice1,slice2) // 复制slice2的3个元素到slice1的前三个位置
copy(slice2,slice1) //复制slice1的前三个元素到slice2中

map类型
map类型是一堆键值对未排序的组合,相当于C++中的std::map<>,Java中的Hashmap<>,python中的dictionary类型。举个例子,变量的使用方法如下所示:

var mymap map[string] float64 //声明一个变量
myMap = make(map[string] float64,100) // 制定一个存储大小的map类型
mymap = map[string] float64{"5879":65.58} //初始化map
mymap["7789"] = 23.74 // 元素的赋值操作
delete(mymap,"7789") //元素的删除操作

内置的delete函数用于删除容器中的键值对,如果键值不存在,那么这个函数调用什么都不发生,也不会有其他的作用。但是如果传入map的变量值是nil,该函数调用会导致程序抛出异常。
查找方式举个例子

value,ok := myMap["1234"]
if ok{
    //表示找到了相对应的键值
}

2.3 Go语言中其他的一些内容

注释: 注释和C/C++/Java中的注释一致,使用下面两种注释方式:

// 这是一个单行注释的方式
/*注释1
这是一个多行注释的方式
注释2*/

标识符: 标识符是用来命名变量、类型等程序实体。一个标识符实际上就是一个或者是多个字母(A~Z和a~z)数字(0~9)、下划线组成序列标志,但是第一个字符必须是字母或者是下划线而不能是数字。
关键字:
Go语言中的25个关键字或者是保留字

break

default

func

interface

select

case

defer

go

map

struct

chan

else

goto

package

switch

const

fallthrough

if

range

type

continue

for

import

return

var

除了这些还有36个预定义标识符

append

bool

byte

cap

close

complex

complex64

complex128

uint16

copy

false

float32

float64

imag

int

int8

int16

uint32

int32

int64

iota

len

make

new

nil

panic

uint64

print

println

real

recover

string

true

uint

uint8

uintptr

2.3 流程控制

一般地,流程控制语句起到以下的几种作用:

  • 选择,即根据条件跳转到不同的执行序列;
  • 循环,即根据条件反复执行某一个序列;
  • 跳转,即根据条件返回到某一个执行序列;

Go语言中支持的流程控制语句有以下的几种:

  • 条件语句,if 、else、else if
  • 选择语句,switch、case和select
  • 循环语句,for和range
  • 跳转语句,goto

当然,控制语句中也包括break、continue和fallthrough关键字。
条件语句:
条件语句的使用如下所示:

if condition1{
    //sentence1
}
else if condition2{
   //sentence2
}
else{
   //condition3
}

条件语句中需要注意的是:
(a) 条件语句不需要使用括号将它包含起来;
(b) 无论语句内存在几条语句,花括号{}都是必须存在的;
© 左边的花括号{必须与if或者else同时在一行;
(d) 在if之后,条件语句之前,可以添加变量初始化语句,使用“;”间隔;
(e) 在有返回值得函数中,不允许将“最终的” return语句包含在if…else…语句中,否则编译失败。
选择语句:
选择语句如下所示:

switch value{
    case a:
        //sentence1
    case b:
        //sentence2
    case c:
       fallthrough
    case d:
       //sentence3
       //表示case c的情况下执行sentence3
    ......
    case z:
        //sentenceN
    default:
       //default sentence
}

switch语句后面的表达式也是非必须的,例如

switch {
   case num>=0 && num<=3:
      fmt.Println("AAAAAAAAAA")
   case num>3 &&num<=6:
      fmt.Println("BBBBBBBBBB")
   default:
      fmt.Println("default sentence")
}

switch语句中需要注意的是:

  • 左花括号{必须与switch处于同一行;
  • 条件表达式不限制为常量或者正数;
  • 单个case中,可以出现多个结果选项;
  • 与C语言规则不同,Go语言中不需要使用break来明确退出一个case;
  • 只有在case中明确添加fallthrough关键字,才会继续执行紧跟的下一个case;
  • 可以不设定switch之后的条件表达式,这种情况下与if… else …的作用一致。

循环语句
在Go语言循环语句中,仅仅支持for关键字。在for的关键字后面的条件表达式不需要用圆括号包含起来。举个例子:

for k:=0;k<N;k++{
    fmt.Println(myarr[k])
}
//下面的语句可以实现while循环和do while循环
num := 0
for{
    num++
    fmt.Println(num,"A")
    if num == 10{
        break
    }
}
//同时也支持多重赋值表达式
a := []int{1,2,3,4,5,6,7,8,9,10}
for i,j:=0,len(a)-1;i<j;i,j=i+1,j-1{
    a[i],a[j] = a[j],a[i]
}

循环语句中需要注意的内容:

  • 左花括号{必须与for同处于一行;
  • Go语言中for循环与C语言中一样,允许在循环条件中定义和初始化变量,唯一的区别是,Go语言中并不支持以逗号为间隔的多个赋值语句,必须使用平行赋值的方式进行初始化多个变量;
  • Go语言中的for循环同样支持continue和break来控制循环,更高级的break可以选择中断哪一个循环条件。举个例子:
func myfunc(){
    JLoop:
        for k:=0;k<5;k++{
            for j:=0;j<10;j++{
                if(j>5){
                    break JLoop
                }
                fmt.Println(j)
            }
        }
    ......
}

跳转语句:
Go语言中仍然支持goto语句。举个例子:

func myfunc(){
    i := 0
    HERE:
    fmt.Prinln(i)
    i++
    if i<10{
    	goto HERE
    }
}

2.4 函数

函数是基本的代码块,用于执行一个任务。
Go语言中至少包含有一个main()函数。
Go语言中函数的定义如下所示:

func function_name([parameters list])[return types]{
	//sentences
}

func表示函数的声明
function_name 表示函数的名称,函数名和参数列表一起构成了函数签名。
parameters list 表示参数列表,参数就像是一个占位符,当函数被调用的时候,可以将值传递给参数,这个值称为实际参数。参数列表指定的是参数类型、顺序以及参数个数。参数是可选择的,函数可以不包含参数。
return types 指的是返回类型,函数返回一列值,return types是该列值的数据类型,也可以不返回任何值。
Go语言中是支持多个返回值的。
在函数中最为值得看到的一点是不定参数,这指的是传入的参数的个数为不定数量,但是首先需要将函数定义为接受不定参数类型,举个例子:

func myfunc(args ...int){
   for _,v :=range args{
      fmt.Println(v)
   }
}
//下面这个是任意类型的不定参数的表示方法
func Printf(format string,args...interface{}){
    //sentences
}

interdface 在Go语言中通常指的是任意类型的变量类型,举个简单的例子:

package main

import "fmt"

func MyPrintf(args ...interface{}){
   for _,arg := range args{
      switch arg.(type){
      case int:
         fmt.Println(arg," is an int value.")
      case string:
         fmt.Println(arg," is a string value")
      case int64:
         fmt.Println(arg, " is an int64 value")
      default:
         fmt.Println(arg," is an unknown type.")
      }
   }
}
func main(){
   var v1 int = 1
   var v2 int64 = 234
   var v3 string = "hello"
   var v4 float32 = 1.3456748
   MyPrintf(v1,v2,v3,v4)
}

一般地,Go语言中存在这样的一个规则:即小写字母开头的函数只在本包内可见,大写字母开头的函数才能被其他包使用。形如…type格式的类型只能作为函数的参数类型存在,并且是最后一个参数。从内部实现机理上来说,类型…type本质上是一个数组切片,也就是[]type,所以这也是传入的参数args可以使用for循环来获得每个传入的参数。
Go语言中支持多个参数的返回。特别地,如果调用了一个具有多个返回值的方法,但是并不关心其中的某个返回值,可以简单地使用一个下划线“_”来跳过这个返回值。举个例子:

n,_ := f.Read(buf)

匿名函数与函数闭包
匿名函数指的是没有函数名称的函数,Go语言中支持随时在代码中定义匿名函数。匿名函数是由一个不带有函数的声明和函数体组成的。当然匿名函数可以直接赋值给一个变量或者是直接运行。举个例子:

fn = func(x,y int) int{
   return x+y
}
q := func (x,y int) int{
   return x+y
}(5,6)
//或者是fn(5,6)
fmt.Println(q)

函数闭包是程序设计中重要的一个基本形式。闭包是可以包含自由(未绑定到特定对象)变量的代码块,这些变量不在这个代码块内或者任何全局上下文,而是在定义代码块的环境中定义。要执行的代码块(由于自由变量包含在代码块中,所以这些自由变量以及它们引用的对象没有被释放)为自由变量提供绑定的计算环境(作用域)。
所以说,支持函数闭包的多数语言可以存储作为变量,甚至作为参数传递给其他函数,最重要的是能够被函数动态地创建和返回。
举一个较为复杂一点的例子:

import "fmt"

func main(){
   var j int = 5
   a := func()(func()){
      var i int = 10
      return func(){
         fmt.Printf("i,j: %d,%d\n",i,j)
      }
   }()

   a()
   j*=2
   a()
}

上面的例子中,变量a保存了闭包函数,其中这个函数引用了局部变量i和j,i的值被隔离,闭包函数外部不能被修改,但是j变量可以修改。所以只有内部函数才能够访问变量i,而无法通过其他途径访问到这个,所以保证了变量i的安全性。

2.5 错误处理

错误处理是任何语言中需要考虑的一个重要问题,最常见的是C++/Java中异常(exception)和try-catch语句的引入,以及python中异常(exception)和try-except语句的处理。下面即介绍Go语言中的异常处理机制
error接口
Go语言中存在一个关于错误处理的标准模式,即error接口,接口的定义如下所示:

type error interface{
    Error() string
}

Go语言中的大多数函数中,错误的返回基本上是以下的格式:将error作为多种返回值中的最后一个,但是这并非是强制要求:

func test(param int)(n int ,err error){
    //excute sentences
    ....
}

调用代码的时候是这样处理错误的情况:

n,err := test()
if err !=nil{
   // 错误处理过程
}else{
  //使用返回值n
}

Go语言中还提供了自定义类型的error。由于error在标准库中被定义为一个接口类型,所以自定义error只要拥有了Error方法,就实现了error接口。举个例子,这里我们用一个结构体扩展这个错误类型:

import "os"

type PathError struct{
   Path string
   Err error
}

func (this*PathError)Error()string{
   return this.Path+":"+this.Err.Error()
}
func New(Path string,Err error)*PathError{
   return&PathError{Path,Err}
}
func read_file(filename string) (*os.File,*PathError){
   fptr,err := os.Open(filename)
   if err !=nil{
      return nil,New(filename,err)
   }
   return fptr,nil
}

关键字defer
defer函数是Go语言中最大的一个亮点,它的主要作用是延时调用函数。在C++语言等其他语言中常常会遇到文件打开和文件关闭过程中常常遇到的一些资源释放问题,例如:

#include<iostream>
#include<string>
#include<fstream>
using std::endl;
using std::cout;
int main() {
 std::string filename = "test.txt";
 std::fstream file(filename,std::ios::in);
 std::string line = "";
 while(file){
  file>>line;
  cout<<line<<endl;
 }
 file.close();
}
with open(filename,mode="r",encoding="utf-8) as f:
    while True:
        line = f.readline()
        if not line:
           break
        print(line)

这些语言中都存在这释放资源的问题,Go语言中也不例外,defer关键字提供了这样一个延时调用,使得能够在代码中得到充分的应用。举个例子:

import (
   "bufio"
   "fmt"
   "os"
)

func read_file(filename string)  {
   fptr,err :=os.Open(filename)
   defer fptr.Close()
   if (err !=nil){
      panic("There 're some wrong things happened!")
   }
   buf := bufio.NewScanner(fptr)
   for {
      if !buf.Scan(){
         break
      }
      line := buf.Text()
      fmt.Println(line)
   }
}

defer语法定义如下所示:

defer xxx(arg0,arg1,arg2,......)

defer表示对紧跟其后的xxx()函数延迟到defer语句所在的当前函数返回的时候再进行调用。
defer函数主要有以下几个基本要点:

  • 延迟对函数进行调用
  • 即时对函数的参数进行求值计算
  • 根据defer顺序反序调用

延迟对函数的调用,举个例子:

package main

import "fmt"
func test(){
	defer fmt.Println("defer called in test!")
	fmt.Println("begin test!")
	fmt.Println("end test!")
}
func main(){
	defer fmt.Println("defer called in main!")
	fmt.Println("begin!")
	test()
	fmt.Println("end!")
}

最后的结果是

begin!
begin test!
end test!
defer called in test!
end!
defer called in main!

defer函数可以即时对参数进行求值处理。举个例子:

package main

import "fmt"
func test_print(num uint){
	fmt.Printf("%d in test_print.\n",num)
}
func main(){
	var test_num uint = 100
	defer test_print(test_num)
	test_num = 200
	test_print(test_num)
}

最后的结果为

200 in test_print.
100 in test_print.

特别地,如果一个函数中有多个defer函数的话,那么函数的调用时反序调用的方式。举个例子:

package main

import "fmt"
func main(){
	defer fmt.Println("derfer 01")
	fmt.Println("main function 01")
	defer fmt.Println("derfer 02")
	fmt.Println("main function 02")
	defer fmt.Println("derfer 03")
	fmt.Println("main function 03")
}

最后得到的结果为

main function 01
main function 02
main function 03
derfer 03
derfer 02
derfer 01

特别地,如果一句话defer并不能进行完成所有的资源释放的功能,可以采用匿名函数的方式进行资源的最后释放:

defer func(){
   //进行资源的释放语句,多个语句
}()

panic()和recover()
Go语言中引入了两个基本的内置函数panic()和recover()函数,这两个函数以报告和处理运行时错误和程序中的错误场景:

func panic(interface{})
func recover(interface{})

当一个好眼熟执行过程中调用panic()函数的时候,正常的函数执行流程将会被终止,但是函数中之前使用到的defer关键字延迟执行的语句正常进行,之后该函数将返回到调用函数,并导致逐层向上执行panic流程,直到所属的goroutine中所有正在执行的函数被终止。错误信息将被报告,包括在调用panic()函数时候传入的参数信息,这个过程被称作是错误的处理流程。
recover()函数用于终止错误处理流程。一般情况下,recover()函数应该在一个使用defer关键字的函数中执行以有效截取错误处理流程。如果没有在发生异常的goroutine中明确调用recover(),会导致该goroutine所属的进行打印异常信息后直接退出。

举个例子:

package main

import "fmt"
func test_f(){
	fmt.Println("before panic function alled!")
	panic("Called panic function.")
	fmt.Println("after panic function alled!")
}
func main(){
	defer func(){
		fmt.Println("defer function begin!")
		if err:=recover();err!=nil{
			fmt.Println(err)
			fmt.Println("using recover function.")
		}
		fmt.Println("defer function end!")
	}()
	test_f()
}

特别注意的是,recover()函数定义在defer所指向的匿名函数当中,否则函数并不能捕获panic()所生成的错误信息。以上的函数中,无论test_func()中是否触发了panic()函数,但是都会执行defer所指向的匿名函数。

小结

本小结学习了Go语言中基本的语法和一些最基础的流程,重点是熟练应用这些内容,相信有了这样的一个基础,下面学习Go语言中高级的内容就容易多了。

参考文献

[1] Go语言编程