1. 介绍
每一个程序都包含很多的函数:函数是基本的代码块。
Go是编译型语言,所以函数编写的顺序是无关紧要的;鉴于可读性的需求,最好把main()
函数写在文件的前面,其他函数按照一定逻辑顺序进行编写(例如函数被调用的顺序)。
编写多个函数的主要目的是将一个需要很多行代码的复杂问题分解为一系列简单的任务(那就是函数)来解决。而且,同一个任务(函数)可以被调用多次,有助于代码重用。
(事实上,好的程序是非常注意DRY原则的,即不要重复你自己(Don’t Repeat Yourself),意思是执行特定任务的代码只能在程序里面出现一次。)
2. 定义
函数包含函数名、行参列表、函数体和返回值列表,使用func进行声明,函数无参数或返回值时则形参列表和返回值列表省略
func name(parameters) returns {
...
}
形参列表需要描述参数名及参数类型,所有形参为函数块局部变量。返回值需要描述返回值类型。
作为提醒,提前介绍一个语法:
不正确的Go代码:
func g()
{
}
它必须是这样的:
func g() {
}
举例:
无参&无返回值
go func sayHello() { fmt.Println("Hello!") }
有参&无返回值
go func sayHi(name string) { fmt.Printf("Hi, %s\n", name) }
有参&有返回值
func add(ni int, n2 int) int { return n1 + n2 }
函数重载(function overloading) 指的是可以编写多个同名函数,只要它们拥有不同的形参或者不同的返回值,在Go里面函数重载是不被允许的。
Go语言不支持这项特性的主要原因是函数重载需要进行多余的类型匹配影响性能;没有重载意味着只是一个简单的函数调度。所以需要给不同的函数使用不同的名字,我们通常会根据函数的特征对函数进行命名。
3. 调用
函数通过函数名(实参列表),在调用过程中实参的每个数据会赋值给形参中的每个变量,因此实参列表类型和数量需要函数定义的形参一一对应。针对函数返回值可通过变量赋值的方式接收。
// 调用无参无返回值函数
sayHello()
// 调用有参无返回值函数
sayHi("123")
// 调用有参有返回值函数
n1, n2 := 1, 2
fmt.Printf("%d + %d = %d\n", n1, n2, add(n1, n2))
n3 := add(4, 5)
fmt.Println(n3)
// 忽略函数返回值
add(3, 4)
4. 函数参数和返回值
函数能够接收参数供自己使用,也可以返回零个或多个值。
我们通过 return
关键字返回一组值。事实上,任何一个有返回值(单个或多个)的函数都必须以 return
和 panic
结尾。
在函数块里面,return
之后的语句都不会执行,如果一个函数需要返回值,那么这个函数里面的每一个代码分支(code-path)都要有return
语句。
4.1 按值传递和按引用传递
Go默认使用按值传递来传递参数,也就是传递参数的副本。函数接收参数副本之后,在使用变量的过程中可能对副本的值进行更改,但不会影响到原来的变量。
如果希望函数可以直接修改参数的值,而不是对参数的副本进行操作,需要将参数的地址传递给函数,这就是按引用传递,比如Function(&arg1)
,此时传递给函数的是一个指针。如果传递给函数的是一个指针,指针的值(一个地址)会被复制,但指针的值所指向的地址上的值不会被复制;可以通过这个指针的值来修改这个值所指向的地址上的值。
几乎在任何情况下,传递指针(一个32位或64位的值)的消耗都比传递副本来得少。
在函数调用时,像切片(slice)、字典(map)、接口(interface)、通道(channel)这样的引用类型都是默认使用引用传递(即使没有显式的指出指针)。
4.1.1 类型合并
在声明函数中若存在多个连续形参类型系统并且可以只保留最后一个参数类型名
func mergeFuncArgs(n1, n2 int, s1, s2, s3 string, b1 bool){
fmt.Println(n1, n2, s1, s2, s3, b1)
}
4.1.2 可变参数
某些情况下函数需要处理形参数量可变,可以在函数的最后一个参数是采用...type
的形式,可变参数则被初始化为对应类型的切片。
func myFunc(a, b, arg ...int){}
示例:
func Greeting(prefix string, who ...string)
Greeting("hi:", "test1", "test2", "test3")
在Greeting函数中,变量 who 的值为[]string{“test1”, “test2”, “test3”}。
如果参数被存储在一个 slice 类型的变量 slice1 中,则可以通过 slice1… 的形式来传递参数,调用变参函数。
4.2 多返回值
Go语言支持函数有多个返回值,在声明函数时使用括号包含所有返回值类型,并使用return返回对应数量的用逗号分割数据。
func calc(n1, n2 int)(int, int, int, int) {
return n1 + n2, n1 - n2, n1 * n2, n1 / n2
}
4.3 命名返回值
在函数返回值列表中可指定变量名,变量在调用时会根据类型使用零值进行初始化,在函数体中进行赋值,同时在调用return时不需要参加返回值,go语言自动将变量的最终结果进行返回。
在使用命名返回值时,当声明函数中存在若多个连续返回值类型相同可只保留最后一个返回值类型名。
multiple_return.go:
package main
import "fmt"
var num int = 10
var numx2, numx3 int
func main() {
numx2, numx3 = getX2AndX3(num)
PrintValues()
numx2, numx3 = getX2AndX3_2(num)
PrintValues()
}
func PrintValues() {
fmt.Printf("num = %d, 2x num = %d, 3x num = %d\n", num, numx2, numx3)
}
func getX2AndX3(input int) (int, int) {
return 2 * input, 3 * input
}
func getX2AndX3_2(input int) (x2 int, x3 int) {
x2 = 2 * input
x3 = 3 * input
// return x2, x3
return
}
输出结果:
num = 10, 2x num = 20, 3x num = 30
num = 10, 2x num = 20, 3x num = 30
警告:
- return 或 return var 都是可以的。
- 不过
return var = expression
(表达式)会引发一个编译错误:syntax error: unexpected =, expecting semicolon or newline or }
。
即使函数使用了命名返回值,依旧可以无视它而返回明确的值。
尽量使用命名返回值:会使代码更清晰、更简短,通过更加容易读懂。
4.4 空白符
空白符用来匹配一些不需要的值,然后丢弃掉。
blank_identifier.go:
package main
import "fmt"
func main() {
var i1 int
var f1 float32
i1, _, f1 = ThreeValues()
fmt.Printf("The int: %d, the float: %f \n", i1, f1)
}
func ThreeValues() (int, int, float32) {
return 5, 6, 7.5
}
输出结果:
The int: 5, the float: 7.500000
ThreeValues 是拥有三个返回值的不需要任何参数的函数,我们将第一个与第三个返回值赋给了 i1 与 f1。第二个返回值赋给了空白符 _ ,然后自动丢弃掉。
4.5 改变外部变量
传递指针函数不但可以节省内存(因为没有复制变量的值),而且赋予了函数直接修改外部变量的能力,所以被修改的变量不再需要使用 return 返回。
如下的例子,reply 是一个指向 int 变量的指针,通过这个指针,我们在函数内修改了这个 int 变量的数值。
side_effect.go:
package main
import "fmt"
func main() {
n := 0
reply := &n
Multiply(10, 5, reply)
fmt.Println("Multiply:", *reply)
}
func Multiply(a, b int, reply *int){
*reply = a * b
}
5. 内置函数
Go语言拥有一些不需要进行导入操作就可以使用的内置函数。它们有时可以针对不同的类型进行操作,例如:len、cap和append,或必须用于系统级的操作,例如: panic。因此,它们需要直接获得编译器的支持。
一下是一个简单的列表:
名称 | 说明 |
---|---|
close | 用于管道通信 |
len、cap | len 用于返回某个类型的长度或数量(字符串、数组、切片、map 和管道);cap 是容量的意思,用于返回某个类型的最大容量(只能用于切片和 map) |
new、make | new 和 make 均是用于分配内存:new 用于值类型和用户定义的类型,如自定义结构,make 用于内置引用类型(切片、map 和管道)。它们的用法就像是函数,但是将类型作为参数:new(type)、make(type)。new(T) 分配类型 T 的零值并返回其地址,也就是指向类型 T 的指针。它也可以被用于基本类型:v := new(int)。make(T) 返回类型 T 的初始化之后的值,因此它比 new 进行更多的工作new() 是一个函数,不要忘记它的括号 |
copy、append | 用于复制和连接切片 |
panic、recover | 两者均用于错误处理机制 |
print、println | 底层打印函数,在部署环境中建议使用fmt包 |
complex、real imag | 用于创建和操作复数 |
6. 递归函数
当一个函数在其函数体内调用自身,则称之为递归。
计算n的阶乘:
func factorial(n int) int {
if n < 0 {
return -1
} else if n == 0 {
return 1
} else {
return n *factorial(n - 1)
}
}
汉罗塔游戏:
// 将所有a柱上的圆盘借助柱移动到c柱,在移动过程中保证每个柱子的上面圆盘比下面圆盘小
package main
import "fmt"
func tower(a,b,c string,layer int){
if layer == 1{
fmt.Println(a,"->",c)
return
}
tower(a,c,b,layer-1)
fmt.Println(a,"->",c)
tower(b,a,c,layer-1)
}
func main(){
tower("A","B","C",3)
}
7. 将函数作为参数
函数可以赋值给变量,存储在数组、切片、映射中,也可作为参数传递给函数或作为函数返回值进行返回
function_parameter.go:
package main
import "fmt"
func main() {
callback(1, Add)
}
func Add(a, b int){
fmt.Printf("the sum of %d and %d is: %d\n", a, b, a+b)
}
func callback(y int, f func(int, int)) {
f(y, 2)
}
输出结果:
the sum of 1 and 2 is: 3
8. 匿名函数和闭包
8.1 匿名函数
不需要定义名字的函数叫做匿名函数,常用做帮助函数在局部代码块中使用或作为其他函数的参数。
当我们不希望给函数起名字的时候,可以使用匿名函数,例如:func(x, y int) int {return x + y }
。
这样的一个函数不能够独立存在(编译器会返回错误:not-declaration statement outside function body
),但可以被赋值与某个变量,即保存函数的地址到变量中:fplus := func(x, y int) int {return x + y }
,然后通过变量名对函数进行调用:fplus(3, 4)
。
当然,也可以直接对匿名函数进行调用:func(x, y int) int {return x + y } (3, 4)
。
hi := func(name string) { // 定义匿名函数并赋值给hi
fmt.Printf("Hi, %s\n", name)
}
hi("zky") // 调用匿名函数hi
func() { // 定义匿名函数并进行调用
fmt.Println("我是匿名函数")
}()
// 使用匿名函数作为printResult的参数
printResult(func(list ...string) {
for i, v := range list {
fmt.Printf("%d: %s\n", i, v)
}
}, names...)
8.2 闭包
匿名函数同样被称之为闭包:它们被允许调用定义在其它环境下的变量。闭包可使得某个函数捕捉到一些外部状态,例如:函数被创建时的状态。另一种表示方式为:一个闭包继承了函数所声明时的作用域。作用域内的变量都被共享到闭包的环境中,因此这些变量可以在闭包中被操作,直到被销毁。
function_return.go:
package main
import "fmt"
func main() {
// make an Add2 function, give it a name p2, and call it;
p2 := Add2()
fmt.Printf("Call Add2 for 3 gives: %v\n", p2(3))
// make a special Adder function, a gets value 2;
TwoAdder := Adder(2)
fmt.Printf("The result is: %v\n", TwoAdder(3))
}
func Add2() func(b int) int {
return func(b int) int {
return b +2
}
}
func Adder(a int) func(b int) int {
return func(b int) int {
return a + b
}
}
输出结果:
Call Add2 for 3 gives: 5
The result is: 5
在程序function_return.go
中我们看到函数Add2和Adder均会返回签名为func(b int) int
函数;
func Add2() (func(b int) int)
func Adder(a int) (func(b int) int)
函数Add2不接受任何参数,但函数Adder接受一个int类型的整数作为参数。
学习并理解以下程序的工作原理:
一个返回值为另一个函数的函数可以称之为工厂函数,这在需要创建一系列相似的函数的时候非常有用:书写一个工厂函数而不是针对每种情况都书写一个函数。下面的函数演示了如何动态返回追加后缀的函数:
func MakeAddSuffix(suffix string) func(string) string {
return func(name string) string {
if !string.HasSuffix(name, suffix) {
return name + suffix
}
return name
}
}
生成如下函数:
addBmp := MakeAddSuffix(".bmp")
addJpeg := MakeAddSuffix(".jpeg")
然后调用它们:
addBmp("file") // return: file.bmp
addJpeg("file") // return: file.jpeg
8.3 使用闭包调试
当在分析和调试复杂的程序时,无数个函数在不同的代码文件中相互调用,如果这时候能够准确地知道哪个文件中的具体哪个函数正在执行,对于调试是十分有帮助的。可以使用 runtime
或 log
包中的特殊函数来实现这样的功能。包 runtime
中的函数 Caller()
提供了相应的信息,因此可以在需要的时候实现一个 where()
闭包函数来打印函数执行的位置:
where := func() {
_, file, line, _ := runtime.Caller(1)
log.Printf("%s:%d", file, line)
}
where()
// some code
where()
// some more code
where()
也可以设置 log
包中的 flag
参数来实现:
log.SetFlags(log.Llongfile)
log.Print("")
或使用一个更加简短版本的 where
函数:
var where = log.Print
func func1() {
where()
... some code
where()
... some code
where()
}
9. 错误处理
9.1 error接口
Go有一个预先定义的error接口类型
type error interface {
Error() string
}
错误值用来表示异常状态;errors包中有一个errorString结构体实现了error接口。当程序处于错误状态时可以使用os.Exit(1)来中止运行。
9.1.1 定义错误
任何时候当你需要一个新的错误类型,都可以使用 errors
包的 errors.New
函数接收合适的错误信息来创建,像下边这样:
err := errors.New("math - square root of negative number")
errors.go:
package main
import (
"errors"
"fmt"
)
var errNotFount error = errors.New("Not fount error")
func main() {
fmt.Printf("error: %v\n", errNotFount)
}
可以把它用于计算平方根函数的参数测试:
func Sqrt(f float64) (float64, error) {
if f <0 {
return 0, errors.New("math - square root of negative number")
}
}
调用Sqrt函数:
if f, err := Sqrt(-1); err != nil {
fmt.Printf("Error: %s\n", err)
}
9.1.2 用fmt创建错误对象
想要返回包含错误参数的更多信息量的字符串,例如:可以使用 fmt.Errorf()
来实现:它和fmt.Printf()完全一样,接收一个或多个格式占位符的格式化字符串和相应数量的占位变量。和打印信息不同的是它用信息生成错误对象。
比如在前面的平方根例子中使用:
if f < 0 {
return 0, fmt.Errorf("math: square root of negative number %g", f)
}
9.2 defer
defer 关键字用户声明函数,不论函数是否发生错误都在函数执行最后执行(return之前),若使用defer声明多个函数,则按照声明的顺序,先声明后执行,常用来做资源释放,记录日志等工作。
func main() {
defer func() {
fmt.Println("defer 01")
}()
defer func() {
fmt.Println("defer 02")
}()
defer func() {
fmt.Println("defer 03")
}()
}
9.3 panic和recover函数
9.3.1 panic
当发生数组下标越界或类型断言失败这样的运行错误时,Go运行时会触发运行时panic,伴随着程序的崩溃抛出一个 runtime.Error
接口类型的值。这个错误值有个RuntimeError()
方法用于区别普通错误。
panic
可以直接从代码初始化:当错误条件(我们所测试的代码)很严苛且不可恢复,程序不能继续运行时,可以使用panic
函数产生一个终止程序的运行时错误。
panic
接收一个做任意类型的参数,通常是字符串,在程序死亡时打印出来。Go运行时负责中止程序并给出调试信息。
panic.go:
package main
import "fmt"
func main() {
fmt.Println("Starting the program")
panic("A severe error occurred: stopping the program!")
fmt.Println("Ending the program")
}
在多层嵌套的函数调用中调用Panic,可以马上中止当前函数的执行,所有的defer语句都会保证执行并把控制权交还给接收到panic的函数调用者。这样向上冒泡知道最顶层,并执行(每层)defer,在栈顶处程序崩溃,并在命令行中用传给panic的值报告错误情况:这个终止过程就是panicking。
9.3.2 从panic中恢复(Recover)
Recover内建函数被用于从panic或错误场景中恢复:让程序可以从panicking重新获得控制权,停止终止过程进而恢复正常执行。
recover
只能在defer修饰的函数中使用:用于取得panic调用中传递过来的错误值,如果是正常执行,调用recover
会返回nil,且没有其它效果。
总结:panic会导致栈被展开直到defer修饰的recover()被调用或者程序中止。
下面例子中的protect函数调用函数参数g来保护调用者防止从g中抛出的运行时panic,并展示panic中的信息:
func protect(g func()) {
defer func() {
log.Println("done")
// Println executes normally even if there is a panic
if err := recover(); err != nil {
log.Printf("run time panic: %v", err)
}
}()
log.Println("start")
g() // possible runtime-error
}
下面展示panic,defer和recover怎么结合使用的完整例子:
panic_recover.go:
package main
import "fmt"
func badCall() {
panic("bad end")
}
func test() {
defer func () {
if e := recover(); e != nil {
fmt.Printf("Panicing %s\n", e)
}
}()
badCall()
fmt.Printf("After bad call\n")
}
func main() {
fmt.Printf("Calling test\n")
test()
fmt.Printf("Test completed\n")
}
输出结果:
Calling test
Panicing bad end
Test completed
10. 自定义包中的错误处理和panicking
这是所有自定义包实现者应该遵守的最佳实践:
- 在包内部,总是应该从panic中recover:不允许显式的超出包范围的panic()。
- 向包的调用者返回错误值(而不是panic)。
在包内部,特别是在非导出函数中有很深层次的嵌套调用时,将panic转换成error来告诉调用方为何出错,是很实用的(且提高了代码可读性)。
示例:
一个简单的parse包用来把输入的字符串解析为整数切片;这个包有自己特殊的ParseError。
当没有东西需要转换或者转换成整数失败时,这个包会panic。但是可导出的Parse函数会从panic中recover并用所有这些信息返回一个错误给调用者。在panic_recover.go中调用了parse包;不可解析的字符串会导致错误并被打印出来。
parse.go:
package parse
import (
"fmt"
"strings"
"strconv"
)
// A ParseError indicates an error in converting a word into an integer
type ParseError struct {
Index int // The index into the space-separated list of words.
Word string // The word that generated the parse error.
Err error // The raw error that precipitated this error, if any
}
// String returns a human-readable error message
func (e *ParseError) String() string {
return fmt.Sprintf("pkg parse: error parsing %q as int", e.Word)
}
// Parse parses the space-separated words in input as integres.
func Parse(input string) (numbers []int, err error) {
defer func() {
if r := recover(); r != nil {
var ok bool
err, ok = r.(error)
if !ok {
err = fmt.Errorf("pkg: %v", r)
}
}
}()
fields := strings.Fields(input)
numbers = fields2numbers(fields)
return
}
func fields2numbers(fields []string) (numbers []int) {
if len(fields) == 0 {
panic("no words to parse")
}
for idx, field := range fields {
num, err := strconv.Atoi(field)
if err != nil {
panic(&ParseError{idx, field, err})
}
numbers = append(numbers, num)
}
return
}
panic_package.go:
package main
import (
"fmt"
"testparse/parse"
)
func main() {
var examples = []string {
"1 2 3 4 5",
"100 50 25 12.5 6.25",
"2 + 2 = 4",
"1st class",
"",
}
for _, ex := range examples {
fmt.Printf("Parsing %q:\n ", ex)
nums, err := parse.Parse(ex)
if err != nil {
fmt.Println(err)
continue
}
fmt.Println(nums)
}
}
输出结果:
Parsing "1 2 3 4 5":
[1 2 3 4 5]
Parsing "100 50 25 12.5 6.25":
pkg: pkg parse: error parsing "12.5" as int
Parsing "2 + 2 = 4":
pkg: pkg parse: error parsing "+" as int
Parsing "1st class":
pkg: pkg parse: error parsing "1st" as int
Parsing "":
pkg: no words to parse
11. 计算函数执行时间
能够知道一个计算执行消耗的时间是非常有意义的,尤其是在对比和基准测试中。最简单的一个办法就是在计算开始之前设置一个起始时候,再由计算结束时的结束时间,最后取出它们的差值,就是这个计算所消耗的时间。想要实现这样的做法,可以使用 time
包中的 Now()
和 Sub
函数:
start := time.Now()
longCalculation()
end := time.Now()
delta := end.Sub(start)
fmt.Printf("longCalculation took this amount of time: %s\n", delta)