函数
文章目录
参考书:《the way to go》
Go 里面有三种类型的函数:
- 普通的带有名字的函数
- 匿名函数或者lambda函数
- 方法
没有参数的函数通常被称为 niladic 函数 (niladic function)
函数可以将其他函数调用作为它的参数,只要这个被调用函数的返回值个数、返回值类型和返回值的顺序与调用函数所需求的实参是一致的
假设 f1
需要 3 个参数 f1(a, b, c int)
,同时 f2
返回 3 个参数 f2(a, b int) (int, int, int)
,就可以这样调用 f1
:f1(f2(a, b))
Go不允许函数重载,因为多余的类型匹配会影响性能
就是以函数作为返回类型。这种类型的声明要写在函数名和可选的参数列表之后,例如:
func FunctionName (a typea, b typeb) typeFunc
可以定义没有形参名的函数
func f(int, int, float64)
你可以在函数体中的某处返回使用类型为 typeFunc
的变量 var
:
return var
可拥有多种返回值
返回类型之间需要使用逗号分割,并使用小括号 ()
将它们括起来,如:
func FunctionName (a typea, b typeb) (t1 type1, t2 type2)
返回的形式:
return var1, var2
这种多返回值一般用于判断某个函数是否执行成功 (true/false) 或与其它返回值一同返回错误消息
函数也可以以申明的方式被使用,作为一个函数类型,就像:
type binOp func(int, int) int
在这里,不需要函数体 {}
。
函数是一等值 (first-class value):它们可以赋值给变量,就像 add := binOp
一样。
这个变量知道自己指向的函数的签名,所以给它赋一个具有不同签名的函数值是不可能的。
函数值 (functions value) 之间可以相互比较:如果它们引用的是相同的函数或者都是 nil
的话,则认为它们是相同的函数。函数不能在其它函数里面声明(不能嵌套),不过我们可以通过使用匿名函数(参考)来破除这个限制。
目前 Go 没有泛型 (generic) 的概念,也就是说它不支持那种支持多种类型的函数。不过在大部分情况下可以通过接口 (interface),特别是空接口与类型选择(type switch)与/或者通过使用反射(reflection)来实现相似的功能。使用这些技术将导致代码更为复杂、性能更为低下,所以在非常注意性能的的场合,最好是为每一个类型单独创建一个函数,而且代码可读性更强
多返回值函数
多值返回是 Go 的一大特性,为我们判断一个函数是否正常执行提供了方便。
我们通过 return
关键字返回一组值。事实上,任何一个有返回值(单个或多个)的函数都必须以 return
或 panic
结尾
如果一个函数需要返回四到五个值,我们可以传递一个切片给函数(如果返回值具有相同类型)或者是传递一个结构体(如果返回值具有不同的类型)。因为传递一个指针允许直接修改变量的值,消耗也更少
实例:
如下里的函数带有一个 int
参数,返回两个 int
值;其中一个函数的返回值在函数调用时就已经被赋予了一个初始零值。
getX2AndX3
与 getX2AndX3_2
两个函数演示了如何使用非命名返回值与命名返回值的特性。当需要返回多个非命名返回值时,需要使用 ()
把它们括起来,比如 (int, int)
。
命名返回值作为结果形参 (result parameters) 被初始化为相应类型的零值,当需要返回的时候,我们只需要一条简单的不带参数的 return
语句。需要注意的是,即使只有一个命名返回值,也需要使用 ()
括起来
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
}
输出结果:
按值传递、按引用传递
Go 默认使用按值传递来传递参数,也就是传递参数的副本。函数接收参数副本之后,在使用变量的过程中可能对副本的值进行更改,但不会影响到原来的变量,比如 Function(arg1)
几乎在任何情况下,传递指针(一个32位或者64位的值)的消耗都比传递副本来得少。
在函数调用时,像切片 (slice)、字典 (map)、接口 (interface)、通道 (channel) 这样的引用类型都是默认使用引用传递(即使没有显式的指出指针)。
有些函数只是完成一个任务,并没有返回值。我们仅仅是利用了这种函数的副作用 (side-effect),就像输出文本到终端,发送一个邮件或者是记录一个错误等。
但是绝大部分的函数还是带有返回值的。
类成员函数
package main
import "fmt"
type user struct {
name string
password string
}
func (u *user) check2(password string) bool {
return u.password == password
}
func (u *user) reset(password string) { //从一个普通函数变成类成员函数
u.password = password
}
func main() {
a := user{name: "wang", password: "1024"}
a.reset("2048")
fmt.Println(a.check2("2048"))
}
输出
true
改变外部变量
传递指针给函数不但可以节省内存(因为没有复制变量的值),而且赋予了函数直接修改外部变量的能力,所以被修改的变量不再需要使用 return
返回。
如下的例子,reply
是一个指向 int
变量的指针,通过这个指针,我们在函数内修改了这个 int
变量的数值:
package main
import (
"fmt"
)
// this function changes reply:
func Multiply(a, b int, reply *int) {
*reply = a * b
}
func main() {
n := 0
reply := &n
Multiply(10, 5, reply)
fmt.Println("Multiply:", *reply) // Multiply: 50
}
当需要在函数内改变一个占用内存比较大的变量时,性能优势就更加明显了。然而,如果不小心使用的话,传递一个指针很容易引发一些不确定的事,所以,我们要十分小心那些可以改变外部变量的函数,在必要时,需要添加注释以便其他人能够更加清楚的知道函数里面到底发生了什么。
变参函数
定义:如果一个函数最后的参数传递的是...type
类型,则可以处理变长的参数(长度可以为0)
这样的函数接受一个类似于slice
的参数,再使用for loop
//函数声明
func myFunc(a, b, arg ...int) {}
如果参数为 slice
类型,则可以通过 slice...
来传递参数
package main
import "fmt"
func main() {
x := min(1, 3, 2, 0)
fmt.Printf("The minimum is: %d\n", x)
slice := []int{7,9,3,5,1}
x = min(slice...)
fmt.Printf("The minimum in the slice is: %d", x)
}
func min(s ...int) int {
if len(s)==0 {
return 0
}
min := s[0]
for _, v := range s {
if v < min {
min = v
}
}
return min
}
输出
The minimum is: 0
The minimum in the slice is: 1
一个接受变长参数的函数可以将这个参数作为其它函数的参数进行传递:
func F1(s ...string) {
F2(s...)
F3(s)
}
func F2(s ...string) { }
func F3(s []string) { }
变长参数可以作为对应类型的 slice 进行二次传递。
但是如果变长参数的类型并不是都相同的呢?使用 5 个参数来进行传递并不是很明智的选择,有 2 种方案可以解决这个问题:
-
使用结构:
定义一个结构类型,假设它叫
Options
,用以存储所有可能的参数:type Options struct { par1 type1, par2 type2, ... }
函数
F1()
可以使用正常的参数a
和b
,以及一个没有任何初始化的Options
结构:F1(a, b, Options {})
。如果需要对选项进行初始化,则可以使用F1(a, b, Options {par1:val1, par2:val2})
。 -
使用空接口:
如果一个变长参数的类型没有被指定,则可以使用默认的空接口
interface{}
,这样就可以接受任何类型的参数。该方案不仅可以用于长度未知的参数,还可以用于任何不确定类型的参数。一般而言我们会使用一个 for-range 循环以及switch
结构对每个参数的类型进行判断:func typecheck(..,..,values … interface{}) { for _, value := range values { switch v := value.(type) { case int: … case float: … case string: … case bool: … default: … } } }
defer和追踪
说明
关键字defer
允许推迟到函数的任意位置执行return
语句之后的一刻,才执行某个语句或函数;一般用于释放某些已分配的资源
用法:类似于面向对象编程语言 Java 和 C# 的 finally 语句块,它一般用于释放某些已分配的资源
package main
import "fmt"
func main() {
function1()
}
func function1() {
fmt.Printf("In function1 at the top\n")
defer function2() //被延后
fmt.Printf("In function1 at the bottom!\n")
}
func function2() {
fmt.Printf("Function2: Deferred until the end of the calling function!")
}
输出
使用 defer
的语句同样可以接受参数
//只是推迟输出,并未推迟执行
func a() {
i := 0
defer fmt.Println(i)
i++
fmt.Println(i)
return
}
//最后输出为0
当有多个 defer
行为被注册时,它们会以逆序执行(类似栈,即后进先出):
//最后输出为 4 3 2 1 0
func f() {
for i := 0; i < 5; i++ {
defer fmt.Printf("%d ", i)
}
}
一些常见操作
//关闭文件流
defer file.Close()
//解锁一个加锁资源
muu.Lock()
defer mu.Unlock()
//打印最终报告
printHeader()
defer printFooter()
//关闭数据库连接
defer disconnectFromDB()
实现
package main
import "fmt"
func main() {
doDBOperations()
}
func connectToDB() {
fmt.Println("ok, connected to db")
}
func disconnectFromDB() {
fmt.Println("ok, disconnected from db")
}
func doDBOperations() {
connectToDB()
fmt.Println("Defering the database disconnect.")
defer disconnectFromDB() //function called here with defer
fmt.Println("Doing some DB operations ...")
fmt.Println("Oops! some crash or network error ...")
fmt.Println("Returning from function here!")
return //terminate the program
// deferred function executed here just before actually returning, even if
// there is a return or abnormal termination before
}
输出
使用defer
实现代码追踪
一个基础但十分实用的实现代码执行追踪的方案就是在进入和离开某个函数打印相关的消息,即可以提炼为下面两个函数:
func trace(s string) { fmt.Println("entering:", s) }
func untrace(s string) { fmt.Println("leaving:", s) }
以下代码展示了何时调用这两个函数:
package main
import "fmt"
func trace(s string) string {
fmt.Println("entering:", s)
return s
}
func un(s string) {
fmt.Println("leaving:", s)
}
func a() {
defer un(trace("a"))
fmt.Println("in a")
}
func b() {
defer un(trace("b"))
fmt.Println("in b")
a()
}
func main() {
b()
}
输出
记录函数的参数和返回值
package main
import (
"io"
"log"
)
func func1(s string) (n int, err error) {
defer func() {
log.Printf("func1(%q) = %d, %v", s, n, err)
}()
return 7, io.EOF
}
func main() {
func1("Go")
}
输出
Output: 2011/10/04 10:46:11 func1("Go") = 7, EOF
常见的内置函数
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 的指针(详见[第 10.1 节](file:///D:/self/资料/go/the-way-to-go/eBook/10.1.md))。它也可以被用于基本类型:v := new(int) 。make(T) 返回类型 T 的初始化之后的值,因此它比 new() 进行更多的工作(详见[第 7.2.3/4 节](file:///D:/self/资料/go/the-way-to-go/eBook/07.2.md)、[第 8.1.1 节](file:///D:/self/资料/go/the-way-to-go/eBook/08.1.md)和[第 14.2.1 节](file:///D:/self/资料/go/the-way-to-go/eBook/14.2.md))。new() 是一个函数,不要忘记它的括号。 |
copy() 、append() | 用于复制和连接切片 |
panic() 、recover() | 两者均用于错误处理机制 |
print() 、println() | 底层打印函数(详见[第 4.2 节](file:///D:/self/资料/go/the-way-to-go/eBook/04.2.md)),在部署环境中建议使用 fmt 包 |
complex() 、real () 、imag() | 用于创建和操作复数(详见[第 4.5.2.2 节](file:///D:/self/资料/go/the-way-to-go/eBook/04.5.md)) |
将函数作为参数
回调:函数可以作为其它函数的参数进行传递,然后在其它函数内调用执行
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) // this becomes Add(1, 2)
}
输出
The sum of 1 and 2 is: 3
将函数作为参数的最好的例子是函数 strings.IndexFunc()
:
该函数的签名是 func IndexFunc(s string, f func(c rune) bool) int
,它的返回值是字符串 s 中第一个使函数 f(c)
返回 true
的 Unicode 字符的索引值。如果找不到,则返回 -1
例如 strings.IndexFunc(line, unicode.IsSpace)
就会返回 line
中第一个空白字符的索引值。当然,您也可以书写自己的函数:
func IsAscii(c int) bool {
if c > 255 {
return false
}
return true
}
闭包
不想给函数起名字则可以使用匿名函数
此函数不能独立存在,但可以被赋值于某个变量当中(保存函数的地址),再通过变量名对函数进行调用,也可以直接对匿名函数进行调用
//保存函数地址,并进行使用
fplus:=func(x,y int) int {return x +y }
fplus(3,4)
//直接调用
func(x,y int) int {return x + y} (3,4)
//计算1到100万整数的总和
func() {
sum := 0
for i := 1; i <= 1e6; i++ {
sum += i
}
}()
表示参数列表的第一对括号必须紧挨着关键字 func
,因为匿名函数没有名称。花括号 {}
涵盖着函数体,最后的一对括号表示对该匿名函数的调用。
实例
package main
import "fmt"
func main() {
f()
}
func f() {
for i := 0; i < 4; i++ {
g := func(i int) { fmt.Printf("%d ", i) }
g(i)
fmt.Printf(" - g is of type %T and has value %v\n", g, g)
}
}
我们可以看到变量 g
代表的是 func(int)
,变量的值是一个内存地址
关键字 defer
经常配合匿名函数使用,它可以用于改变函数的命名返回值。
匿名函数还可以配合 go
关键字来作为 goroutine 使用
匿名函数同样被称之为闭包(函数式语言的术语):它们被允许调用定义在其它环境下的变量。闭包可使得某个函数捕捉到一些外部状态,例如:函数被创建时的状态。另一种表示方式为:一个闭包继承了函数所声明时的作用域。这种状态(作用域内的变量)都被共享到闭包的环境中,因此这些变量可以在闭包中被操作,直到被销毁
闭包经常被用作包装函数:它们会预先定义好 1 个或多个参数以用于包装,详见下一节中的示例。另一个不错的应用就是使用闭包来完成更加简洁的错误检查
闭包将函数作为返回值
函数 Adder()
现在被赋值到变量 f
中(类型为 func(int) int
)
package main
import "fmt"
func main() {
var f = Adder()
fmt.Print(f(1), " - ")
fmt.Print(f(20), " - ")
fmt.Print(f(300))
}
func Adder() func(int) int {
var x int
return func(delta int) int {
x += delta
return x
}
}
输出
在多次调用中,变量 x
的值是被保留的,即 0 + 1 = 1
,然后 1 + 20 = 21
,最后 21 + 300 = 321
:闭包函数保存并积累其中的变量的值,不管外部函数退出与否,它都能够继续操作外部函数中的局部变量。
这些局部变量同样可以是参数
这样闭包函数就能够被应用到整个集合的元素上,并修改它们的值。然后这些变量就可以用于表示或计算全局或平均值。
一个返回值为另一个函数的函数可以被称之为工厂函数,这在您需要创建一系列相似的函数的时候非常有用:书写一个工厂函数而不是针对每种情况都书写一个函数。下面的函数演示了如何动态返回追加后缀的函数:
func MakeAddSuffix(suffix string) func(string) string {
return func(name string) string {
if !strings.HasSuffix(name, suffix) {
return name + suffix
}
return name
}
}
现在,我们可以生成如下函数:
addBmp := MakeAddSuffix(".bmp")
addJpeg := MakeAddSuffix(".jpeg")
然后调用它们:
addBmp("file") // returns: file.bmp
addJpeg("file") // returns: file.jpeg
可以返回其它函数的函数和接受其它函数作为参数的函数均被称之为高阶函数,是函数式语言的特点。我们已经在中得知函数也是一种值,因此很显然 Go 语言具有一些函数式语言的特性。闭包在 Go 语言中非常常见,常用于 goroutine 和管道操作。
我们将会看到 Go 语言中的函数在处理混合对象时的强大能力。
计算函数执行时间
有时候,能够知道一个计算执行消耗的时间是非常有意义的,尤其是在对比和基准测试中。最简单的一个办法就是在计算开始之前设置一个起始时间,再记录计算结束时的结束时间,最后计算它们的差值,就是这个计算所消耗的时间。想要实现这样的做法,可以使用 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)
使用内存缓存来提升性能
类似于前缀和
内存缓存的技术在使用计算成本相对昂贵的函数时非常有用(不仅限于例子中的递归),譬如大量进行相同参数的运算。这种技术还可以应用于纯函数中,即相同输入必定获得相同输出的函数。