go语言顺序编程
Al_xin · · 3139 次点击 · · 开始浏览1、变量
声明:
Go语言的变量声明方式与C和C++语言有明显的不同。对于纯粹的变量声明,Go语言引入了关键字var,而类型信息放在变量名之后,实例如下:
2
3
4
5
6
7
8
9
10
var v2 string
var v4 [10]int //数组
var v4 []int //数组切片
var v5 struct{
f int
}
var v6 *int //指针
var v7 map[string]int //key-string value-int
var v8 func(a int) int //函数
对于声明变量时需要进行初始化的场景,var关键字可以保留,但不再是必要的元素。
2
3
var v2=10
v3:=10
go语言的变量赋值与多数语言一致,但go语言中提供了C/C++程序员期盼多年的多重赋值功能,比如下面这个交换i和j变量的语句:
我们在使用传统的强类型语言编程时,经常会出现这种情况,即在调用函数时为了获取一个值,却因为该函数返回多个值而不得不定义一堆没有用的变量。在Go中这种情况可以通过结合使用多重返回和匿名变量来避免这种丑陋的写法,让代码看起来更加优雅。
假设GetName()函数的定义如下,它返回3个值,分别为firstName、lastName和nickName:
2
3
4
5
6
{
return "May","Chan","Chibi Maruko"
}
//只想获得nickName,则函数调用语句可以用如下方式编写
_,_,nickName:=GetName()
2、常量
常量定义:
2
3
4
5
6
7
8
9
const zero=0.0
const(
size int64=1024
eof=-1
)
const u,v float32=0,3
const a,b,c=3,4,"foo"
const mask=1<<3
Go语言预定义了这些常量:true、false和iota
iota比较特殊,可以被认为是一个可被编译器修改的常量,在每一个const关键字出现时被重置为0,然后在下一个const出现之前,每出现一次iota,其所代表数字会自动增1.
从以下例子可以基本理解iota的用法:
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
c0=iota //0
c1=iota //1
c2=iota //2
)
const(
a=1<<iota //1
b=1<<iota //2
c=1<<iota //4
)
const(
u=iota*42 //0
v float64=iota*42 //42
w=iota*42 //84
)
const x=iota //0
const y=iota //0
枚举:
枚举指一系列相关的常量,比如下面关于一个星期中每天的定义。通过上一节的例子,我们看到可以用const后跟一对圆括号方式定义一组常量,这种定义法在Go语言中通常用于定义枚举值。Go语言并不支持众多其他语言明确支持的enum关键字。
2
3
4
5
6
7
8
9
10
Sunday=iota
Monday
Tuesday
Wednesday
Thursday
Friday
Saturday
numberofDays//这个常量没有导出
)
通Go语言的其他符号一样,以大写字母开头的常量在包外可见。
以上例子中numberofDays为包内私有,其他符号则可被其他包访问。
3、类型
基础类型:
布尔类型:bool
整型:int8、byte、int16、int、uint、uintptr等
浮点类型:float32、float64
复数类型:complex64、complex128
字符串:string
字符类型:rune
错误类型:error
此外,Go语言也支持以下这些复合类型:
指针(pointer)
数组(array)
切片(slice)
字典(map)
通道(chan)
结构体(struct)
接口(interface)
浮点数比较:
2
3
4
5
func IsEqual(f1,f2,p float64) bool{
return math.Fdim(f1,f2)<p
}
常规数组声明:
2
3
4
5
6
[32]byte //长度为32的数组,每个元素为一个字节
[2*N] struct{x,y int32} //复杂类型数组
[1000]*float64 //指针数组
[3][5]int //二维数组
[2][2][2]float64 //等同[2]([2]([2]float64))
值类型:
需要特别注意的是,在Go语言中数组是一个值类型,所有的值类型变量再赋值和作为参数传递时都产生一次复制动作。如果将数组作为函数的参数类型,则在函数调用时该参数将发生数据复制。因此,函数体中无法修改传入的数组的内容,因为函数内操作的只是所传入数组的一个副本。
下面是一个例子:
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
import "fmt"
func modify(array [5]int){
array[0]=10
fmt.Println("the modify array is:",array)
}
func main(){
array:=[5]int{1,2,3,4,5}
modify(array)
fmt.Println("the original array is:",array)
}
the modify array is: [10 2 3 4 5]
the original array is: [1 2 3 4 5]
从执行结果可以看出,函数modify()内操作的那个数组跟main()中传入的数组是两个不同的实例。这个问题,可以采用数组切片功能来达成。
数组切片:
基于数组的切片,数组切片可以基于一个已存在的数组创建。例如如下:
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
import "fmt"
func main(){
var array [10]int=[10]int{1,2,3,4,5,6,7,8,9,10}
var Slipce []int=array[:5]
fmt.Println("the original array is:")
for _,v :=range array{
fmt.Print(v," ")
}
fmt.Println("\nthe splice array is:")
for _,v := range Slipce{
fmt.Print(v," ")
}
fmt.Println()
}
the original array is:
1 2 3 4 5 6 7 8 9 10
the splice array is:
1 2 3 4 5
也可以直接创建数组切片,具体如下所示:
2
3
4
mySlice1:=make([]int,5) //初始元素个数为5,的数组切片,元素初始值为0
mySlice2:=make([]int,5,10)
mySlice3:=[]int{1,2,3,4,5}
2
3
4
5
6
7
8
9
import "fmt"
func main(){
myslice:=make([]int,5,10)
fmt.Println("the length use cap:",cap(myslice))
fmt.Println("the length use len:",len(myslice))
}
打印结果如下:
the length use cap: 10
the length use len: 5
如果需要往上例中mySlice已包含的5个元素后面继续新增元素,可以使用append()函数。
2
3
4
slice2:=[]int{5,3,4}
copy(slice2,slice1) //只会复制slice1的前三个元素到slice2中
copy(slice1,slice2) //只会复制slice2的3个元素到slice1的前3个位置
map:
map是一堆键值对的未排序集合。比如身份证号码作为唯一键来标识一个人的信息,则可以使用map进行定义。
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
import "fmt"
type Personinfo struct{
ID string
name string
Address string
}
func main(){
var personDB map[string] Personinfo
personDB=make(map[string] Personinfo)
personDB["12345"]=Personinfo{"12345","Jhon","Room 1"}
personDB["1"]=Personinfo{"1","Smith","Room 2"}
person,ok:=personDB["12"]
if ok{
fmt.Println("Found Person",person.name," with ID 12")
}else{
fmt.Println("Not Found the ID 12")
}
}
元素删除:
delete(myMap,"1234")
4、流程控制
if语句
2
3
4
5
6
7
8
if x==0{
return 5
}else{
return x
}
}
选择语句:
2
3
4
5
6
7
8
9
10
11
12
13
14
case 0:
fmt.Println("0")
case 1:
fmt.Println("1")
case 2:
fallthrough
case 3:
fmt.Println("3")
case 4,5,6:
fmt.Println("4,5,6")
default:
fmt.Println("Default")
}
只有在case中明确添加fallthrough关键字,才会继续执行紧跟的下一个case
循环语句:
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
for i:=0;i<10;i++{
sum+=i
}
//无限for循环写法
sum:=0
for{
sum++
if sum > 100{
break
}
}
a:=[]int{1,2,3,4,5,6}
for i,j:=0,len(a)-1;i<j;i,j=i+1,j-1{
a[i],a[j]=a[j],a[i]
}
跳转语句:
2
3
4
5
6
7
8
9
i:=0
HERE:
fmt.Println(i)
i++
if i<10{
goto HERE
}
}
5、函数
函数构成代码执行的逻辑结构。在Go语言中,函数的基本组成为:关键字func、函数名、参数列表、返回值、函数体和返回语句。
需要注意的是,小写字母开头的函数只是本包内可见,大写字母开头的函数才能被其他包使用。这个规则也适用于类型和变量的可见性。
任意类型的不定参数:
之前的例子中将不定参数类型约束为int,如果你希望传任意类型,可以指定类型为interface{}。下面是Go语言标准库中fmt.Printf()的函数原型:
2
3
//...
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
import "fmt"
func MyPrintf(args ...interface{}){
for _,arg := range args{
switch arg.(type){
case int:
fmt.Println(arg,"is a int value")
case int64:
fmt.Println(arg,"is a int64 value")
case string:
fmt.Println(arg,"is a string value")
default:
fmt.Println(arg,"is unkown value")
}
}
}
func main(){
var v1 int=1
var v2 int64=2
var v3 string="foo"
var v4 float32=1.234
MyPrintf(v1,v2,v3,v4)
}
1 is a int value
2 is a int64 value
foo is a string value
1.234 is unkown value
匿名函数与闭包:
匿名函数是指不需要定义函数名的一种函数实现方式,JavaScript、C#和Objective-C等语言都提供了匿名函数特性,当然也包含Go语言。
1、匿名函数
在Go里面,函数可以像普通变量一样被传递或者使用,这与C语言的回调函数比较类似。不同的是,Go语言支持随时在代码里定义匿名函数。
匿名函数由一个不带函数名的函数声明和函数体组成,如下所示:
2
3
return a*b<int(z)
}
2
3
4
5
6
7
8
return x+y
}
func(ch chan int){
ch<-ACK
}(reply_cha) //花括号后直接跟参数列表表示函数调用
2、闭包
Go的匿名函数是一个闭包
闭包是可以包含自由(未绑定到特定对象)变量的代码块,这些变量不在这个代码块内或者任何全局上下文中定义,而是在定义代码块的环境中定义,要执行代码块(由于自由变量包含在代码块中,所以这些自由变量以及它们引用的对象没有被释放)为自由变量提供绑定的计算环境(作用域)
闭包的价值在于可以作为函数对象或者匿名函数,对于类型系统而言,这意味着不仅要表示数据还要表示代码。支持闭包的多数语言都将函数作为第一级对象,就是说这些函数可以存储到变量中作为参数传递给其他函数,最重要的是能够被函数动态创建和返回。
Go语言中的闭包同样也引用到函数外的变量。闭包的实现确保只要闭包还被使用,那么被闭包引用的变量会一直存在:
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
import "fmt"
func main(){
var j int=5
a:=func()(func()){
var i int=10
return func(){
fmt.Printf("i,j is %d,%d\n",i,j)
}
}()
a()
j*=2
a()
}
输出结果如下所示:
1、变量
声明:
Go语言的变量声明方式与C和C++语言有明显的不同。对于纯粹的变量声明,Go语言引入了关键字var,而类型信息放在变量名之后,实例如下:
2
3
4
5
6
7
8
9
10
var v2 string
var v4 [10]int //数组
var v4 []int //数组切片
var v5 struct{
f int
}
var v6 *int //指针
var v7 map[string]int //key-string value-int
var v8 func(a int) int //函数
对于声明变量时需要进行初始化的场景,var关键字可以保留,但不再是必要的元素。
2
3
var v2=10
v3:=10
go语言的变量赋值与多数语言一致,但go语言中提供了C/C++程序员期盼多年的多重赋值功能,比如下面这个交换i和j变量的语句:
我们在使用传统的强类型语言编程时,经常会出现这种情况,即在调用函数时为了获取一个值,却因为该函数返回多个值而不得不定义一堆没有用的变量。在Go中这种情况可以通过结合使用多重返回和匿名变量来避免这种丑陋的写法,让代码看起来更加优雅。
假设GetName()函数的定义如下,它返回3个值,分别为firstName、lastName和nickName:
2
3
4
5
6
{
return "May","Chan","Chibi Maruko"
}
//只想获得nickName,则函数调用语句可以用如下方式编写
_,_,nickName:=GetName()
2、常量
常量定义:
2
3
4
5
6
7
8
9
const zero=0.0
const(
size int64=1024
eof=-1
)
const u,v float32=0,3
const a,b,c=3,4,"foo"
const mask=1<<3
Go语言预定义了这些常量:true、false和iota
iota比较特殊,可以被认为是一个可被编译器修改的常量,在每一个const关键字出现时被重置为0,然后在下一个const出现之前,每出现一次iota,其所代表数字会自动增1.
从以下例子可以基本理解iota的用法:
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
c0=iota //0
c1=iota //1
c2=iota //2
)
const(
a=1<<iota //1
b=1<<iota //2
c=1<<iota //4
)
const(
u=iota*42 //0
v float64=iota*42 //42
w=iota*42 //84
)
const x=iota //0
const y=iota //0
枚举:
枚举指一系列相关的常量,比如下面关于一个星期中每天的定义。通过上一节的例子,我们看到可以用const后跟一对圆括号方式定义一组常量,这种定义法在Go语言中通常用于定义枚举值。Go语言并不支持众多其他语言明确支持的enum关键字。
2
3
4
5
6
7
8
9
10
Sunday=iota
Monday
Tuesday
Wednesday
Thursday
Friday
Saturday
numberofDays//这个常量没有导出
)
通Go语言的其他符号一样,以大写字母开头的常量在包外可见。
以上例子中numberofDays为包内私有,其他符号则可被其他包访问。
3、类型
基础类型:
布尔类型:bool
整型:int8、byte、int16、int、uint、uintptr等
浮点类型:float32、float64
复数类型:complex64、complex128
字符串:string
字符类型:rune
错误类型:error
此外,Go语言也支持以下这些复合类型:
指针(pointer)
数组(array)
切片(slice)
字典(map)
通道(chan)
结构体(struct)
接口(interface)
浮点数比较:
2
3
4
5
func IsEqual(f1,f2,p float64) bool{
return math.Fdim(f1,f2)<p
}
常规数组声明:
2
3
4
5
6
[32]byte //长度为32的数组,每个元素为一个字节
[2*N] struct{x,y int32} //复杂类型数组
[1000]*float64 //指针数组
[3][5]int //二维数组
[2][2][2]float64 //等同[2]([2]([2]float64))
值类型:
需要特别注意的是,在Go语言中数组是一个值类型,所有的值类型变量再赋值和作为参数传递时都产生一次复制动作。如果将数组作为函数的参数类型,则在函数调用时该参数将发生数据复制。因此,函数体中无法修改传入的数组的内容,因为函数内操作的只是所传入数组的一个副本。
下面是一个例子:
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
import "fmt"
func modify(array [5]int){
array[0]=10
fmt.Println("the modify array is:",array)
}
func main(){
array:=[5]int{1,2,3,4,5}
modify(array)
fmt.Println("the original array is:",array)
}
the modify array is: [10 2 3 4 5]
the original array is: [1 2 3 4 5]
从执行结果可以看出,函数modify()内操作的那个数组跟main()中传入的数组是两个不同的实例。这个问题,可以采用数组切片功能来达成。
数组切片:
基于数组的切片,数组切片可以基于一个已存在的数组创建。例如如下:
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
import "fmt"
func main(){
var array [10]int=[10]int{1,2,3,4,5,6,7,8,9,10}
var Slipce []int=array[:5]
fmt.Println("the original array is:")
for _,v :=range array{
fmt.Print(v," ")
}
fmt.Println("\nthe splice array is:")
for _,v := range Slipce{
fmt.Print(v," ")
}
fmt.Println()
}
the original array is:
1 2 3 4 5 6 7 8 9 10
the splice array is:
1 2 3 4 5
也可以直接创建数组切片,具体如下所示:
2
3
4
mySlice1:=make([]int,5) //初始元素个数为5,的数组切片,元素初始值为0
mySlice2:=make([]int,5,10)
mySlice3:=[]int{1,2,3,4,5}
2
3
4
5
6
7
8
9
import "fmt"
func main(){
myslice:=make([]int,5,10)
fmt.Println("the length use cap:",cap(myslice))
fmt.Println("the length use len:",len(myslice))
}
打印结果如下:
the length use cap: 10
the length use len: 5
如果需要往上例中mySlice已包含的5个元素后面继续新增元素,可以使用append()函数。
2
3
4
slice2:=[]int{5,3,4}
copy(slice2,slice1) //只会复制slice1的前三个元素到slice2中
copy(slice1,slice2) //只会复制slice2的3个元素到slice1的前3个位置
map:
map是一堆键值对的未排序集合。比如身份证号码作为唯一键来标识一个人的信息,则可以使用map进行定义。
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
import "fmt"
type Personinfo struct{
ID string
name string
Address string
}
func main(){
var personDB map[string] Personinfo
personDB=make(map[string] Personinfo)
personDB["12345"]=Personinfo{"12345","Jhon","Room 1"}
personDB["1"]=Personinfo{"1","Smith","Room 2"}
person,ok:=personDB["12"]
if ok{
fmt.Println("Found Person",person.name," with ID 12")
}else{
fmt.Println("Not Found the ID 12")
}
}
元素删除:
delete(myMap,"1234")
4、流程控制
if语句
2
3
4
5
6
7
8
if x==0{
return 5
}else{
return x
}
}
选择语句:
2
3
4
5
6
7
8
9
10
11
12
13
14
case 0:
fmt.Println("0")
case 1:
fmt.Println("1")
case 2:
fallthrough
case 3:
fmt.Println("3")
case 4,5,6:
fmt.Println("4,5,6")
default:
fmt.Println("Default")
}
只有在case中明确添加fallthrough关键字,才会继续执行紧跟的下一个case
循环语句:
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
for i:=0;i<10;i++{
sum+=i
}
//无限for循环写法
sum:=0
for{
sum++
if sum > 100{
break
}
}
a:=[]int{1,2,3,4,5,6}
for i,j:=0,len(a)-1;i<j;i,j=i+1,j-1{
a[i],a[j]=a[j],a[i]
}
跳转语句:
2
3
4
5
6
7
8
9
i:=0
HERE:
fmt.Println(i)
i++
if i<10{
goto HERE
}
}
5、函数
函数构成代码执行的逻辑结构。在Go语言中,函数的基本组成为:关键字func、函数名、参数列表、返回值、函数体和返回语句。
需要注意的是,小写字母开头的函数只是本包内可见,大写字母开头的函数才能被其他包使用。这个规则也适用于类型和变量的可见性。
任意类型的不定参数:
之前的例子中将不定参数类型约束为int,如果你希望传任意类型,可以指定类型为interface{}。下面是Go语言标准库中fmt.Printf()的函数原型:
2
3
//...
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
import "fmt"
func MyPrintf(args ...interface{}){
for _,arg := range args{
switch arg.(type){
case int:
fmt.Println(arg,"is a int value")
case int64:
fmt.Println(arg,"is a int64 value")
case string:
fmt.Println(arg,"is a string value")
default:
fmt.Println(arg,"is unkown value")
}
}
}
func main(){
var v1 int=1
var v2 int64=2
var v3 string="foo"
var v4 float32=1.234
MyPrintf(v1,v2,v3,v4)
}
1 is a int value
2 is a int64 value
foo is a string value
1.234 is unkown value
匿名函数与闭包:
匿名函数是指不需要定义函数名的一种函数实现方式,JavaScript、C#和Objective-C等语言都提供了匿名函数特性,当然也包含Go语言。
1、匿名函数
在Go里面,函数可以像普通变量一样被传递或者使用,这与C语言的回调函数比较类似。不同的是,Go语言支持随时在代码里定义匿名函数。
匿名函数由一个不带函数名的函数声明和函数体组成,如下所示:
2
3
return a*b<int(z)
}
2
3
4
5
6
7
8
return x+y
}
func(ch chan int){
ch<-ACK
}(reply_cha) //花括号后直接跟参数列表表示函数调用
2、闭包
Go的匿名函数是一个闭包
闭包是可以包含自由(未绑定到特定对象)变量的代码块,这些变量不在这个代码块内或者任何全局上下文中定义,而是在定义代码块的环境中定义,要执行代码块(由于自由变量包含在代码块中,所以这些自由变量以及它们引用的对象没有被释放)为自由变量提供绑定的计算环境(作用域)
闭包的价值在于可以作为函数对象或者匿名函数,对于类型系统而言,这意味着不仅要表示数据还要表示代码。支持闭包的多数语言都将函数作为第一级对象,就是说这些函数可以存储到变量中作为参数传递给其他函数,最重要的是能够被函数动态创建和返回。
Go语言中的闭包同样也引用到函数外的变量。闭包的实现确保只要闭包还被使用,那么被闭包引用的变量会一直存在:
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
import "fmt"
func main(){
var j int=5
a:=func()(func()){
var i int=10
return func(){
fmt.Printf("i,j is %d,%d\n",i,j)
}
}()
a()
j*=2
a()
}
输出结果如下所示:
i,j is 10,5
i,j is 10,10
在上面的例子中,变量a指向的闭包函数引用了局部变量i和j,i的值被隔离,在闭包外不能被修改,改变j的值以后,再次调用a,发现结果是修改过的值。
在变量a指向的闭包函数中,只有内部的匿名函数才能访问变量i,而无法通过其他途径访问到,因此保证了i的安全性。
6、错误处理
漂亮的错误处理规范是Go语言最大的亮点之一。
1、error接口
Go语言引入了一个关于错误处理的标准模式,即error接口,该接口的定义如下:
2
3
4
5
6
7
8
9
10
11
12
Error() string
}
//当syscall.Stat()失败返回err时,将该err包装到了一个PathError对象返回:
func Stat(name string)(fi FileInfo,err error){
var stat syscall.Stat_t
err=syscall.Stat(name,&stat)
if err!=nil{
return nil,&PathError("stat",name,err)
}
return fileInfoFromStat(&stat,name),nil
}
2、defer
关键字defer是Go语言引入的一个非常有意思的特性,相信很多C++程序员都写过类似下面的代码:
2
3
4
5
6
7
8
9
10
11
12
13
File _f;
public:
file_closer(FILE f):_f(f){}
~file_closer(){ if(f) fclose(f);}
};
//在需要使用地方
void f(){
FILE f=open_file("file.txt");//打开一个文件句柄
file_closer _closer(f);
//对f句柄进行操作
}
Go语言使用defer关键字简简单单的解决了这个问题,比如以下的例子:
2
3
4
5
6
7
8
9
10
11
12
13
srcFile,err:=os.Open(src)
if err!=nil{
return
}
defer srcFile.Close()
dstFile,err:=os.Create(dstName)
if err!=nil{
return
}
defer dstFile.Close()
return io.Copy(dstFile,srcFile)
}
即使其中的Copy()函数抛出异常,Go仍然会保证dstFile和srcFile会被正常关闭。
3、panic()和recover()
Go 语言引入了两个内置函数panic()和recover()以报告和处理运行时错误和程序中错误场景:
2
func recover() interface{}
从panic()的参数类型interface{} 我们可以得知,该函数接受任意类型的数据,比如整型、字符串、对象等。调用方法很简单,下面是几个例子:
2
3
panic("network broken")
panic(Error("file not exists"))
下面是一个例子:
2
3
4
5
6
if r:=recover;r!=nil{
log.Printf("Runtime error caught:%v",r)
}
}()
foo()
7 、完整实例
现在我们用本章学到的知识来实现一个完整的程序。我们准备开发一个排序算法的比较程序,从命令行指定输入的数据文件和输出的数据文件,并指定对应的排序算法。该程序的用法如下所示:
USAGE:sorter -i <in> -o <out> -a <qsort|bubblesort>
一个具体的执行过程如下:
$./sorter -i in.dat -o out.dat -a qsort
The sorting process costs 10us to complete.
当然,如果输入不合法,应该给出对应的提示,接下来我们一步步实现这个程序。
1、程序结构
我们将该函数分成两类:主程序和排序算法函数。每个排序算法都包装成一个静态库,虽然现在看起来似乎有些多此一举,但这只是为了顺便演示包之间的依赖方法。
假设我们的程序根目录为~/goyard/sorter,因此需要再环境变量GOPATH中添加这个路径。根目录结构如下:
<sorter>
|--<src>
|--<sorter>
|--sorter.go
|--<algorithms>
|--<qsort>
|--qsort.go
|--qsort_test.go
|--<bublesort>
|--bublesort.go
|--bublesort_test.go
|--<pkg
|--<bin>
其中sorter.go是主程序,qsort.go用于实现快速排序,bubblesort.go用于实现冒泡排序。
下面我们先定义一下排序算法函数的函数原型:
func QuickSort(int [] int)[] int
func BubbleSort(int []int)[]int
2、主程序
我们的主程序需要做的工作包含一下几点:
1、获取并解析命令行输入
2、从对应文件中读取输入数据
3、调用对应的排序函数
4、将排序结果输出到对应的文件中
5、打印排序所花费的时间的信息
接下来我们一步步地编写程序。
1、命令行参数
Go语言标准库提供了用于快速解析命令行参数的flag包。对于本示例的参数需求,我们可以利用flag包进行实现,如下代码所示:
2
3
4
5
6
7
8
9
10
11
12
13
14
15
import "fmt"
import "flag"
var infile *string=flag.String("i","infile","File contains values for sorting")
var outfile *string=flag.String("o","outfile","File contains values sorted")
var algorithm *string=flag.String("a","algorithm","The algorithm to sort")
func main(){
flag.Parse()
if infile!=nil{
fmt.Println("the infile is ",*infile,", the outfile is ",*outfile,", the algorithm is ",*algorithm)
}
}
$ go build sorter.go
$ ./sorter -i unsorted.dat -o sorted.dat -a bubblesort
infile = unsorted.dat outfile = sorted.dat algorithm = bubblesort
可以看到,传入的各个命令行参数已经被正确读取到各个变量中。flag包使用起来非常方便,大大简化了C语言时代解析命令行参数的过程。
2、读取输入文件
我们需要先从一个文件中把包含内容读取到数组中,将该数组排好序后再写回到另一个文件中,因此还需要学习如何在Go语言中操作文件。
我们先设计输入文件的格式。输入文件是一个纯文本文件,每一行是一个需要被排序的数字。下面是一个示例unsorted.dat 文件内容:
123
3064
3
64
490
然后需要逐行从这个文件中读取内容,并解析为int类型的数据,再添加到一个int类型的数组切片中。接下来我们实现这部分功能,如下所示:
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
import "fmt"
import "flag"
import "bufio"
import "io"
import "os"
import "strconv"
var infile *string=flag.String("i","unsorted.dat","File contains values for sorting")
var outfile *string=flag.String("o","sorted.dat","File contains values sorted")
var algorithm *string=flag.String("a","qsort","The algorithm to sort")
func readValues(infile string)(values []int,err error){
file,err:=os.Open(infile)
if err!=nil{
fmt.Println("Failed to open the file:",infile)
return
}
defer file.Close()
br:=bufio.NewReader(file)
values=make([]int,0)
for{
line,isPrefix,err1:=br.ReadLine()
if err1!=nil{
if err1!=io.EOF{
err=err1
}
break
}
if isPrefix{
fmt.Println("A too long line is unexpected.")
return
}
str:=string(line)
value,err1:=strconv.Atoi(str)
if err1!=nil{
err=err1
return
}
values=append(values,value)
}
return
}
func main(){
flag.Parse()
if infile!=nil{
fmt.Println("the infile is ",*infile,", the outfile is ",*outfile,", the algorithm is ",*algorithm)
}
values,err:=readValues(*infile)
if err==nil{
fmt.Println("ReadValue:",values)
}else{
fmt.Println("Read Error:",err)
}
}
在实现readValues()函数的过程中,我们用到了os,io,bufio和strconv等Go语言标准包,用于读写和字符串处理。熟练掌握这些包的基本用法,将会大幅度提高使用Go语言的工作效率。
我们还示范了数组切片的使用,并使用defer关键字以确保关闭文件句柄。
3、写到输出文件
在数据处理结束后,我们需要将排序结果输出到另一个文本文件。这个过程比较简单,因此这里只列出writeValues()函数的实现,读者可以自行对照Go语言标准库以熟悉相关包的用法。
有疑问加站长微信联系(非本文作者)
入群交流(和以上内容无关):加入Go大咖交流群,或添加微信:liuxiaoyan-s 备注:入群;或加QQ群:692541889
关注微信- 请尽量让自己的回复能够对别人有帮助
- 支持 Markdown 格式, **粗体**、~~删除线~~、
`单行代码` - 支持 @ 本站用户;支持表情(输入 : 提示),见 Emoji cheat sheet
- 图片支持拖拽、截图粘贴等方式上传
收入到我管理的专栏 新建专栏
1、变量
声明:
Go语言的变量声明方式与C和C++语言有明显的不同。对于纯粹的变量声明,Go语言引入了关键字var,而类型信息放在变量名之后,实例如下:
2
3
4
5
6
7
8
9
10
var v2 string
var v4 [10]int //数组
var v4 []int //数组切片
var v5 struct{
f int
}
var v6 *int //指针
var v7 map[string]int //key-string value-int
var v8 func(a int) int //函数
对于声明变量时需要进行初始化的场景,var关键字可以保留,但不再是必要的元素。
2
3
var v2=10
v3:=10
go语言的变量赋值与多数语言一致,但go语言中提供了C/C++程序员期盼多年的多重赋值功能,比如下面这个交换i和j变量的语句:
我们在使用传统的强类型语言编程时,经常会出现这种情况,即在调用函数时为了获取一个值,却因为该函数返回多个值而不得不定义一堆没有用的变量。在Go中这种情况可以通过结合使用多重返回和匿名变量来避免这种丑陋的写法,让代码看起来更加优雅。
假设GetName()函数的定义如下,它返回3个值,分别为firstName、lastName和nickName:
2
3
4
5
6
{
return "May","Chan","Chibi Maruko"
}
//只想获得nickName,则函数调用语句可以用如下方式编写
_,_,nickName:=GetName()
2、常量
常量定义:
2
3
4
5
6
7
8
9
const zero=0.0
const(
size int64=1024
eof=-1
)
const u,v float32=0,3
const a,b,c=3,4,"foo"
const mask=1<<3
Go语言预定义了这些常量:true、false和iota
iota比较特殊,可以被认为是一个可被编译器修改的常量,在每一个const关键字出现时被重置为0,然后在下一个const出现之前,每出现一次iota,其所代表数字会自动增1.
从以下例子可以基本理解iota的用法:
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
c0=iota //0
c1=iota //1
c2=iota //2
)
const(
a=1<<iota //1
b=1<<iota //2
c=1<<iota //4
)
const(
u=iota*42 //0
v float64=iota*42 //42
w=iota*42 //84
)
const x=iota //0
const y=iota //0
枚举:
枚举指一系列相关的常量,比如下面关于一个星期中每天的定义。通过上一节的例子,我们看到可以用const后跟一对圆括号方式定义一组常量,这种定义法在Go语言中通常用于定义枚举值。Go语言并不支持众多其他语言明确支持的enum关键字。
2
3
4
5
6
7
8
9
10
Sunday=iota
Monday
Tuesday
Wednesday
Thursday
Friday
Saturday
numberofDays//这个常量没有导出
)
通Go语言的其他符号一样,以大写字母开头的常量在包外可见。
以上例子中numberofDays为包内私有,其他符号则可被其他包访问。
3、类型
基础类型:
布尔类型:bool
整型:int8、byte、int16、int、uint、uintptr等
浮点类型:float32、float64
复数类型:complex64、complex128
字符串:string
字符类型:rune
错误类型:error
此外,Go语言也支持以下这些复合类型:
指针(pointer)
数组(array)
切片(slice)
字典(map)
通道(chan)
结构体(struct)
接口(interface)
浮点数比较:
2
3
4
5
func IsEqual(f1,f2,p float64) bool{
return math.Fdim(f1,f2)<p
}
常规数组声明:
2
3
4
5
6
[32]byte //长度为32的数组,每个元素为一个字节
[2*N] struct{x,y int32} //复杂类型数组
[1000]*float64 //指针数组
[3][5]int //二维数组
[2][2][2]float64 //等同[2]([2]([2]float64))
值类型:
需要特别注意的是,在Go语言中数组是一个值类型,所有的值类型变量再赋值和作为参数传递时都产生一次复制动作。如果将数组作为函数的参数类型,则在函数调用时该参数将发生数据复制。因此,函数体中无法修改传入的数组的内容,因为函数内操作的只是所传入数组的一个副本。
下面是一个例子:
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
import "fmt"
func modify(array [5]int){
array[0]=10
fmt.Println("the modify array is:",array)
}
func main(){
array:=[5]int{1,2,3,4,5}
modify(array)
fmt.Println("the original array is:",array)
}
the modify array is: [10 2 3 4 5]
the original array is: [1 2 3 4 5]
从执行结果可以看出,函数modify()内操作的那个数组跟main()中传入的数组是两个不同的实例。这个问题,可以采用数组切片功能来达成。
数组切片:
基于数组的切片,数组切片可以基于一个已存在的数组创建。例如如下:
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
import "fmt"
func main(){
var array [10]int=[10]int{1,2,3,4,5,6,7,8,9,10}
var Slipce []int=array[:5]
fmt.Println("the original array is:")
for _,v :=range array{
fmt.Print(v," ")
}
fmt.Println("\nthe splice array is:")
for _,v := range Slipce{
fmt.Print(v," ")
}
fmt.Println()
}
the original array is:
1 2 3 4 5 6 7 8 9 10
the splice array is:
1 2 3 4 5
也可以直接创建数组切片,具体如下所示:
2
3
4
mySlice1:=make([]int,5) //初始元素个数为5,的数组切片,元素初始值为0
mySlice2:=make([]int,5,10)
mySlice3:=[]int{1,2,3,4,5}
2
3
4
5
6
7
8
9
import "fmt"
func main(){
myslice:=make([]int,5,10)
fmt.Println("the length use cap:",cap(myslice))
fmt.Println("the length use len:",len(myslice))
}
打印结果如下:
the length use cap: 10
the length use len: 5
如果需要往上例中mySlice已包含的5个元素后面继续新增元素,可以使用append()函数。
2
3
4
slice2:=[]int{5,3,4}
copy(slice2,slice1) //只会复制slice1的前三个元素到slice2中
copy(slice1,slice2) //只会复制slice2的3个元素到slice1的前3个位置
map:
map是一堆键值对的未排序集合。比如身份证号码作为唯一键来标识一个人的信息,则可以使用map进行定义。
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
import "fmt"
type Personinfo struct{
ID string
name string
Address string
}
func main(){
var personDB map[string] Personinfo
personDB=make(map[string] Personinfo)
personDB["12345"]=Personinfo{"12345","Jhon","Room 1"}
personDB["1"]=Personinfo{"1","Smith","Room 2"}
person,ok:=personDB["12"]
if ok{
fmt.Println("Found Person",person.name," with ID 12")
}else{
fmt.Println("Not Found the ID 12")
}
}
元素删除:
delete(myMap,"1234")
4、流程控制
if语句
2
3
4
5
6
7
8
if x==0{
return 5
}else{
return x
}
}
选择语句:
2
3
4
5
6
7
8
9
10
11
12
13
14
case 0:
fmt.Println("0")
case 1:
fmt.Println("1")
case 2:
fallthrough
case 3:
fmt.Println("3")
case 4,5,6:
fmt.Println("4,5,6")
default:
fmt.Println("Default")
}
只有在case中明确添加fallthrough关键字,才会继续执行紧跟的下一个case
循环语句:
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
for i:=0;i<10;i++{
sum+=i
}
//无限for循环写法
sum:=0
for{
sum++
if sum > 100{
break
}
}
a:=[]int{1,2,3,4,5,6}
for i,j:=0,len(a)-1;i<j;i,j=i+1,j-1{
a[i],a[j]=a[j],a[i]
}
跳转语句:
2
3
4
5
6
7
8
9
i:=0
HERE:
fmt.Println(i)
i++
if i<10{
goto HERE
}
}
5、函数
函数构成代码执行的逻辑结构。在Go语言中,函数的基本组成为:关键字func、函数名、参数列表、返回值、函数体和返回语句。
需要注意的是,小写字母开头的函数只是本包内可见,大写字母开头的函数才能被其他包使用。这个规则也适用于类型和变量的可见性。
任意类型的不定参数:
之前的例子中将不定参数类型约束为int,如果你希望传任意类型,可以指定类型为interface{}。下面是Go语言标准库中fmt.Printf()的函数原型:
2
3
//...
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
import "fmt"
func MyPrintf(args ...interface{}){
for _,arg := range args{
switch arg.(type){
case int:
fmt.Println(arg,"is a int value")
case int64:
fmt.Println(arg,"is a int64 value")
case string:
fmt.Println(arg,"is a string value")
default:
fmt.Println(arg,"is unkown value")
}
}
}
func main(){
var v1 int=1
var v2 int64=2
var v3 string="foo"
var v4 float32=1.234
MyPrintf(v1,v2,v3,v4)
}
1 is a int value
2 is a int64 value
foo is a string value
1.234 is unkown value
匿名函数与闭包:
匿名函数是指不需要定义函数名的一种函数实现方式,JavaScript、C#和Objective-C等语言都提供了匿名函数特性,当然也包含Go语言。
1、匿名函数
在Go里面,函数可以像普通变量一样被传递或者使用,这与C语言的回调函数比较类似。不同的是,Go语言支持随时在代码里定义匿名函数。
匿名函数由一个不带函数名的函数声明和函数体组成,如下所示:
2
3
return a*b<int(z)
}
2
3
4
5
6
7
8
return x+y
}
func(ch chan int){
ch<-ACK
}(reply_cha) //花括号后直接跟参数列表表示函数调用
2、闭包
Go的匿名函数是一个闭包
闭包是可以包含自由(未绑定到特定对象)变量的代码块,这些变量不在这个代码块内或者任何全局上下文中定义,而是在定义代码块的环境中定义,要执行代码块(由于自由变量包含在代码块中,所以这些自由变量以及它们引用的对象没有被释放)为自由变量提供绑定的计算环境(作用域)
闭包的价值在于可以作为函数对象或者匿名函数,对于类型系统而言,这意味着不仅要表示数据还要表示代码。支持闭包的多数语言都将函数作为第一级对象,就是说这些函数可以存储到变量中作为参数传递给其他函数,最重要的是能够被函数动态创建和返回。
Go语言中的闭包同样也引用到函数外的变量。闭包的实现确保只要闭包还被使用,那么被闭包引用的变量会一直存在:
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
import "fmt"
func main(){
var j int=5
a:=func()(func()){
var i int=10
return func(){
fmt.Printf("i,j is %d,%d\n",i,j)
}
}()
a()
j*=2
a()
}
输出结果如下所示:
1、变量
声明:
Go语言的变量声明方式与C和C++语言有明显的不同。对于纯粹的变量声明,Go语言引入了关键字var,而类型信息放在变量名之后,实例如下:
2
3
4
5
6
7
8
9
10
var v2 string
var v4 [10]int //数组
var v4 []int //数组切片
var v5 struct{
f int
}
var v6 *int //指针
var v7 map[string]int //key-string value-int
var v8 func(a int) int //函数
对于声明变量时需要进行初始化的场景,var关键字可以保留,但不再是必要的元素。
2
3
var v2=10
v3:=10
go语言的变量赋值与多数语言一致,但go语言中提供了C/C++程序员期盼多年的多重赋值功能,比如下面这个交换i和j变量的语句:
我们在使用传统的强类型语言编程时,经常会出现这种情况,即在调用函数时为了获取一个值,却因为该函数返回多个值而不得不定义一堆没有用的变量。在Go中这种情况可以通过结合使用多重返回和匿名变量来避免这种丑陋的写法,让代码看起来更加优雅。
假设GetName()函数的定义如下,它返回3个值,分别为firstName、lastName和nickName:
2
3
4
5
6
{
return "May","Chan","Chibi Maruko"
}
//只想获得nickName,则函数调用语句可以用如下方式编写
_,_,nickName:=GetName()
2、常量
常量定义:
2
3
4
5
6
7
8
9
const zero=0.0
const(
size int64=1024
eof=-1
)
const u,v float32=0,3
const a,b,c=3,4,"foo"
const mask=1<<3
Go语言预定义了这些常量:true、false和iota
iota比较特殊,可以被认为是一个可被编译器修改的常量,在每一个const关键字出现时被重置为0,然后在下一个const出现之前,每出现一次iota,其所代表数字会自动增1.
从以下例子可以基本理解iota的用法:
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
c0=iota //0
c1=iota //1
c2=iota //2
)
const(
a=1<<iota //1
b=1<<iota //2
c=1<<iota //4
)
const(
u=iota*42 //0
v float64=iota*42 //42
w=iota*42 //84
)
const x=iota //0
const y=iota //0
枚举:
枚举指一系列相关的常量,比如下面关于一个星期中每天的定义。通过上一节的例子,我们看到可以用const后跟一对圆括号方式定义一组常量,这种定义法在Go语言中通常用于定义枚举值。Go语言并不支持众多其他语言明确支持的enum关键字。
2
3
4
5
6
7
8
9
10
Sunday=iota
Monday
Tuesday
Wednesday
Thursday
Friday
Saturday
numberofDays//这个常量没有导出
)
通Go语言的其他符号一样,以大写字母开头的常量在包外可见。
以上例子中numberofDays为包内私有,其他符号则可被其他包访问。
3、类型
基础类型:
布尔类型:bool
整型:int8、byte、int16、int、uint、uintptr等
浮点类型:float32、float64
复数类型:complex64、complex128
字符串:string
字符类型:rune
错误类型:error
此外,Go语言也支持以下这些复合类型:
指针(pointer)
数组(array)
切片(slice)
字典(map)
通道(chan)
结构体(struct)
接口(interface)
浮点数比较:
2
3
4
5
func IsEqual(f1,f2,p float64) bool{
return math.Fdim(f1,f2)<p
}
常规数组声明:
2
3
4
5
6
[32]byte //长度为32的数组,每个元素为一个字节
[2*N] struct{x,y int32} //复杂类型数组
[1000]*float64 //指针数组
[3][5]int //二维数组
[2][2][2]float64 //等同[2]([2]([2]float64))
值类型:
需要特别注意的是,在Go语言中数组是一个值类型,所有的值类型变量再赋值和作为参数传递时都产生一次复制动作。如果将数组作为函数的参数类型,则在函数调用时该参数将发生数据复制。因此,函数体中无法修改传入的数组的内容,因为函数内操作的只是所传入数组的一个副本。
下面是一个例子:
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
import "fmt"
func modify(array [5]int){
array[0]=10
fmt.Println("the modify array is:",array)
}
func main(){
array:=[5]int{1,2,3,4,5}
modify(array)
fmt.Println("the original array is:",array)
}
the modify array is: [10 2 3 4 5]
the original array is: [1 2 3 4 5]
从执行结果可以看出,函数modify()内操作的那个数组跟main()中传入的数组是两个不同的实例。这个问题,可以采用数组切片功能来达成。
数组切片:
基于数组的切片,数组切片可以基于一个已存在的数组创建。例如如下:
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
import "fmt"
func main(){
var array [10]int=[10]int{1,2,3,4,5,6,7,8,9,10}
var Slipce []int=array[:5]
fmt.Println("the original array is:")
for _,v :=range array{
fmt.Print(v," ")
}
fmt.Println("\nthe splice array is:")
for _,v := range Slipce{
fmt.Print(v," ")
}
fmt.Println()
}
the original array is:
1 2 3 4 5 6 7 8 9 10
the splice array is:
1 2 3 4 5
也可以直接创建数组切片,具体如下所示:
2
3
4
mySlice1:=make([]int,5) //初始元素个数为5,的数组切片,元素初始值为0
mySlice2:=make([]int,5,10)
mySlice3:=[]int{1,2,3,4,5}
2
3
4
5
6
7
8
9
import "fmt"
func main(){
myslice:=make([]int,5,10)
fmt.Println("the length use cap:",cap(myslice))
fmt.Println("the length use len:",len(myslice))
}
打印结果如下:
the length use cap: 10
the length use len: 5
如果需要往上例中mySlice已包含的5个元素后面继续新增元素,可以使用append()函数。
2
3
4
slice2:=[]int{5,3,4}
copy(slice2,slice1) //只会复制slice1的前三个元素到slice2中
copy(slice1,slice2) //只会复制slice2的3个元素到slice1的前3个位置
map:
map是一堆键值对的未排序集合。比如身份证号码作为唯一键来标识一个人的信息,则可以使用map进行定义。
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
import "fmt"
type Personinfo struct{
ID string
name string
Address string
}
func main(){
var personDB map[string] Personinfo
personDB=make(map[string] Personinfo)
personDB["12345"]=Personinfo{"12345","Jhon","Room 1"}
personDB["1"]=Personinfo{"1","Smith","Room 2"}
person,ok:=personDB["12"]
if ok{
fmt.Println("Found Person",person.name," with ID 12")
}else{
fmt.Println("Not Found the ID 12")
}
}
元素删除:
delete(myMap,"1234")
4、流程控制
if语句
2
3
4
5
6
7
8
if x==0{
return 5
}else{
return x
}
}
选择语句:
2
3
4
5
6
7
8
9
10
11
12
13
14
case 0:
fmt.Println("0")
case 1:
fmt.Println("1")
case 2:
fallthrough
case 3:
fmt.Println("3")
case 4,5,6:
fmt.Println("4,5,6")
default:
fmt.Println("Default")
}
只有在case中明确添加fallthrough关键字,才会继续执行紧跟的下一个case
循环语句:
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
for i:=0;i<10;i++{
sum+=i
}
//无限for循环写法
sum:=0
for{
sum++
if sum > 100{
break
}
}
a:=[]int{1,2,3,4,5,6}
for i,j:=0,len(a)-1;i<j;i,j=i+1,j-1{
a[i],a[j]=a[j],a[i]
}
跳转语句:
2
3
4
5
6
7
8
9
i:=0
HERE:
fmt.Println(i)
i++
if i<10{
goto HERE
}
}
5、函数
函数构成代码执行的逻辑结构。在Go语言中,函数的基本组成为:关键字func、函数名、参数列表、返回值、函数体和返回语句。
需要注意的是,小写字母开头的函数只是本包内可见,大写字母开头的函数才能被其他包使用。这个规则也适用于类型和变量的可见性。
任意类型的不定参数:
之前的例子中将不定参数类型约束为int,如果你希望传任意类型,可以指定类型为interface{}。下面是Go语言标准库中fmt.Printf()的函数原型:
2
3
//...
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
import "fmt"
func MyPrintf(args ...interface{}){
for _,arg := range args{
switch arg.(type){
case int:
fmt.Println(arg,"is a int value")
case int64:
fmt.Println(arg,"is a int64 value")
case string:
fmt.Println(arg,"is a string value")
default:
fmt.Println(arg,"is unkown value")
}
}
}
func main(){
var v1 int=1
var v2 int64=2
var v3 string="foo"
var v4 float32=1.234
MyPrintf(v1,v2,v3,v4)
}
1 is a int value
2 is a int64 value
foo is a string value
1.234 is unkown value
匿名函数与闭包:
匿名函数是指不需要定义函数名的一种函数实现方式,JavaScript、C#和Objective-C等语言都提供了匿名函数特性,当然也包含Go语言。
1、匿名函数
在Go里面,函数可以像普通变量一样被传递或者使用,这与C语言的回调函数比较类似。不同的是,Go语言支持随时在代码里定义匿名函数。
匿名函数由一个不带函数名的函数声明和函数体组成,如下所示:
2
3
return a*b<int(z)
}
2
3
4
5
6
7
8
return x+y
}
func(ch chan int){
ch<-ACK
}(reply_cha) //花括号后直接跟参数列表表示函数调用
2、闭包
Go的匿名函数是一个闭包
闭包是可以包含自由(未绑定到特定对象)变量的代码块,这些变量不在这个代码块内或者任何全局上下文中定义,而是在定义代码块的环境中定义,要执行代码块(由于自由变量包含在代码块中,所以这些自由变量以及它们引用的对象没有被释放)为自由变量提供绑定的计算环境(作用域)
闭包的价值在于可以作为函数对象或者匿名函数,对于类型系统而言,这意味着不仅要表示数据还要表示代码。支持闭包的多数语言都将函数作为第一级对象,就是说这些函数可以存储到变量中作为参数传递给其他函数,最重要的是能够被函数动态创建和返回。
Go语言中的闭包同样也引用到函数外的变量。闭包的实现确保只要闭包还被使用,那么被闭包引用的变量会一直存在:
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
import "fmt"
func main(){
var j int=5
a:=func()(func()){
var i int=10
return func(){
fmt.Printf("i,j is %d,%d\n",i,j)
}
}()
a()
j*=2
a()
}
输出结果如下所示:
i,j is 10,5
i,j is 10,10
在上面的例子中,变量a指向的闭包函数引用了局部变量i和j,i的值被隔离,在闭包外不能被修改,改变j的值以后,再次调用a,发现结果是修改过的值。
在变量a指向的闭包函数中,只有内部的匿名函数才能访问变量i,而无法通过其他途径访问到,因此保证了i的安全性。
6、错误处理
漂亮的错误处理规范是Go语言最大的亮点之一。
1、error接口
Go语言引入了一个关于错误处理的标准模式,即error接口,该接口的定义如下:
2
3
4
5
6
7
8
9
10
11
12
Error() string
}
//当syscall.Stat()失败返回err时,将该err包装到了一个PathError对象返回:
func Stat(name string)(fi FileInfo,err error){
var stat syscall.Stat_t
err=syscall.Stat(name,&stat)
if err!=nil{
return nil,&PathError("stat",name,err)
}
return fileInfoFromStat(&stat,name),nil
}
2、defer
关键字defer是Go语言引入的一个非常有意思的特性,相信很多C++程序员都写过类似下面的代码:
2
3
4
5
6
7
8
9
10
11
12
13
File _f;
public:
file_closer(FILE f):_f(f){}
~file_closer(){ if(f) fclose(f);}
};
//在需要使用地方
void f(){
FILE f=open_file("file.txt");//打开一个文件句柄
file_closer _closer(f);
//对f句柄进行操作
}
Go语言使用defer关键字简简单单的解决了这个问题,比如以下的例子:
2
3
4
5
6
7
8
9
10
11
12
13
srcFile,err:=os.Open(src)
if err!=nil{
return
}
defer srcFile.Close()
dstFile,err:=os.Create(dstName)
if err!=nil{
return
}
defer dstFile.Close()
return io.Copy(dstFile,srcFile)
}
即使其中的Copy()函数抛出异常,Go仍然会保证dstFile和srcFile会被正常关闭。
3、panic()和recover()
Go 语言引入了两个内置函数panic()和recover()以报告和处理运行时错误和程序中错误场景:
2
func recover() interface{}
从panic()的参数类型interface{} 我们可以得知,该函数接受任意类型的数据,比如整型、字符串、对象等。调用方法很简单,下面是几个例子:
2
3
panic("network broken")
panic(Error("file not exists"))
下面是一个例子:
2
3
4
5
6
if r:=recover;r!=nil{
log.Printf("Runtime error caught:%v",r)
}
}()
foo()
7 、完整实例
现在我们用本章学到的知识来实现一个完整的程序。我们准备开发一个排序算法的比较程序,从命令行指定输入的数据文件和输出的数据文件,并指定对应的排序算法。该程序的用法如下所示:
USAGE:sorter -i <in> -o <out> -a <qsort|bubblesort>
一个具体的执行过程如下:
$./sorter -i in.dat -o out.dat -a qsort
The sorting process costs 10us to complete.
当然,如果输入不合法,应该给出对应的提示,接下来我们一步步实现这个程序。
1、程序结构
我们将该函数分成两类:主程序和排序算法函数。每个排序算法都包装成一个静态库,虽然现在看起来似乎有些多此一举,但这只是为了顺便演示包之间的依赖方法。
假设我们的程序根目录为~/goyard/sorter,因此需要再环境变量GOPATH中添加这个路径。根目录结构如下:
<sorter>
|--<src>
|--<sorter>
|--sorter.go
|--<algorithms>
|--<qsort>
|--qsort.go
|--qsort_test.go
|--<bublesort>
|--bublesort.go
|--bublesort_test.go
|--<pkg
|--<bin>
其中sorter.go是主程序,qsort.go用于实现快速排序,bubblesort.go用于实现冒泡排序。
下面我们先定义一下排序算法函数的函数原型:
func QuickSort(int [] int)[] int
func BubbleSort(int []int)[]int
2、主程序
我们的主程序需要做的工作包含一下几点:
1、获取并解析命令行输入
2、从对应文件中读取输入数据
3、调用对应的排序函数
4、将排序结果输出到对应的文件中
5、打印排序所花费的时间的信息
接下来我们一步步地编写程序。
1、命令行参数
Go语言标准库提供了用于快速解析命令行参数的flag包。对于本示例的参数需求,我们可以利用flag包进行实现,如下代码所示:
2
3
4
5
6
7
8
9
10
11
12
13
14
15
import "fmt"
import "flag"
var infile *string=flag.String("i","infile","File contains values for sorting")
var outfile *string=flag.String("o","outfile","File contains values sorted")
var algorithm *string=flag.String("a","algorithm","The algorithm to sort")
func main(){
flag.Parse()
if infile!=nil{
fmt.Println("the infile is ",*infile,", the outfile is ",*outfile,", the algorithm is ",*algorithm)
}
}
$ go build sorter.go
$ ./sorter -i unsorted.dat -o sorted.dat -a bubblesort
infile = unsorted.dat outfile = sorted.dat algorithm = bubblesort
可以看到,传入的各个命令行参数已经被正确读取到各个变量中。flag包使用起来非常方便,大大简化了C语言时代解析命令行参数的过程。
2、读取输入文件
我们需要先从一个文件中把包含内容读取到数组中,将该数组排好序后再写回到另一个文件中,因此还需要学习如何在Go语言中操作文件。
我们先设计输入文件的格式。输入文件是一个纯文本文件,每一行是一个需要被排序的数字。下面是一个示例unsorted.dat 文件内容:
123
3064
3
64
490
然后需要逐行从这个文件中读取内容,并解析为int类型的数据,再添加到一个int类型的数组切片中。接下来我们实现这部分功能,如下所示:
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
import "fmt"
import "flag"
import "bufio"
import "io"
import "os"
import "strconv"
var infile *string=flag.String("i","unsorted.dat","File contains values for sorting")
var outfile *string=flag.String("o","sorted.dat","File contains values sorted")
var algorithm *string=flag.String("a","qsort","The algorithm to sort")
func readValues(infile string)(values []int,err error){
file,err:=os.Open(infile)
if err!=nil{
fmt.Println("Failed to open the file:",infile)
return
}
defer file.Close()
br:=bufio.NewReader(file)
values=make([]int,0)
for{
line,isPrefix,err1:=br.ReadLine()
if err1!=nil{
if err1!=io.EOF{
err=err1
}
break
}
if isPrefix{
fmt.Println("A too long line is unexpected.")
return
}
str:=string(line)
value,err1:=strconv.Atoi(str)
if err1!=nil{
err=err1
return
}
values=append(values,value)
}
return
}
func main(){
flag.Parse()
if infile!=nil{
fmt.Println("the infile is ",*infile,", the outfile is ",*outfile,", the algorithm is ",*algorithm)
}
values,err:=readValues(*infile)
if err==nil{
fmt.Println("ReadValue:",values)
}else{
fmt.Println("Read Error:",err)
}
}
在实现readValues()函数的过程中,我们用到了os,io,bufio和strconv等Go语言标准包,用于读写和字符串处理。熟练掌握这些包的基本用法,将会大幅度提高使用Go语言的工作效率。
我们还示范了数组切片的使用,并使用defer关键字以确保关闭文件句柄。
3、写到输出文件
在数据处理结束后,我们需要将排序结果输出到另一个文本文件。这个过程比较简单,因此这里只列出writeValues()函数的实现,读者可以自行对照Go语言标准库以熟悉相关包的用法。