go函数使用
介绍
每一个程序都包含很多的函数:函数是基本的代码块。
Go是编译型语言,所以函数编写的顺序是无关紧要的;鉴于可读性的需求,最好把 main() 函数写在文件的前面,其他函数按照一定逻辑顺序进行编写(例如函数被调用的顺序)。
Go 里面拥三种类型的函数:
- 普通的带有名字的函数
- 匿名函数或者lambda函数
- 方法
所有类型的函数都可以有参数与返回值。函数参数、返回值以及它们的类型被统称为函数签名。
注意
go语言方法后面要紧跟大括号
这样是不正确的 Go 代码:
func g()
{
}
它必须是这样的:
func g() {
}
函数被调用的基本格式如下:
pack1.Function(arg1, arg2, …, argn)
Function 是 pack1 包里面的一个函数,括号里的是被调用函数的实参(argument):这些值被传递给被调用函数的形参(parameter)。
一个简单的函数调用其他函数的例子:
package main
func main() {
println("In main before calling greeting")
greeting()
println("In main after calling greeting")
}
func greeting() {
println("In greeting: Hi!!!!!")
}
代码输出:
In main before calling greeting
In greeting: Hi!!!
In main after calling greeting
注意
函数可以将其他函数调用作为它的参数,只要这个被调用函数的返回值个数、返回值类型和返回值的顺序与调用函数所需求的实参是一致的,例如:
假设 f1 需要 3 个参数 f1(a, b, c int) ,同时 f2 返回 3 个参数 f2(a, b int) (int, int, int) ,就可以这样调用 f1: f1(f2(a, b)) 。
注意在 Go 里面函数重载是不被允许的。这将导致一个编译错误:funcName redeclared in this book, previous declaration at lineno
Go 语言不支持这项特性的主要原因是函数重载需要进行多余的类型匹配影响性能;没有重载意味着只是一个简单的函数调度。所以你需要给不同的函数使用不同的名字,我们通常会根据函数的特征对函数进行命名。如果需要申明一个在外部定义的函数,你只需要给出函数名与函数签名,不需要给出函数体:
func flushICache(begin, end uintptr) // implemented externally
函数也可以以申明的方式被使用,作为一个函数类型,就像:type binOp func(int, int) int在这里,不需要函数体 {} 。
函数参数与返回值
- 按值传递(call by value) 按引用传递(call by reference)
- 命名的返回值(named return variables)
- 空白符(blank identifier)
- 改变外部变量(outside variable)
函数能够接收参数供自己使用,也可以返回零个或多个值(我们通常把返回多个值称为返回一组值)。多值返回是 Go 的一大特性,为我们判断一个函数是否正常执行提供了方便。
我们通过 return 关键字返回一组值。事实上,任何一个有返回值(单个或多个)的函数都必须以return 或 panic 结尾。
在函数块里面, return 之后的语句都不会执行。如果一个函数需要返回值,那么这个函数里面的每一个代码分支(code-path)都要有 return 语句。
按值传递 按引用传递
Go 默认使用按值传递来传递参数,也就是传递参数的副本。函数接收参数副本之后,在使用变量的过程中可能对副本的值进行更改,但不会影响到原来的变量,比如 Function(arg1) 。
如果你希望函数可以直接修改参数的值,而不是对参数的副本进行操作,你需要将参数的地址(变量名前面添加&符号,比如 &variable)传递给函数,这就是按引用传递,比如 Function(&arg1) ,此时传递给函数的是一个指针。如果传递给函数的是一个指针,指针的值(一个地址)会被复制,但指针的值所指向的地址上的值不会被复制;我们可以通过这个指针的值来修改这个值所指向的地址上的值。
几乎在任何情况下,传递指针(一个32位或者64位的值)的消耗都比传递副本来得少。
在函数调用时,像切片(slice)、字典(map)、接口(interface)、通道(channel)这样的引用类型都是默认使用引用传递(即使没有显示的指出指针)。
package main
import "fmt"
func main() {
fmt.Printf("Multiply 2 * 5 * 6 = %d\n", MultiPly3Nums(2, 5, 6))
// var i1 int = MultiPly3Nums(2, 5, 6)
// fmt.Printf("MultiPly 2 * 5 * 6 = %d\n", i1)
}
func MultiPly3Nums(a int, b int, c int) int {
// var product int = a * b * c
// return product
return a * b * c
}
输出显示:
Multiply 2 * 5 * 6 = 60
如果一个函数需要返回四到五个值,我们可以传递一个切片给函数(如果返回值具有相同类型)或者是传递一个结构体(如果返回值具有不同的类型)。因为传递一个指针允许直接修改变量的值,消耗也更少。
命名的返回值(named return variables)
函数带有一个 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
}
输出结果:
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 } 。
空白符(blank identifier)
空白符用来匹配一些不需要的值,然后丢弃掉,下面的 blank_identifier.go 就是很好的例子。ThreeValues 是拥有三个返回值的不需要任何参数的函数,在下面的例子中,我们将第一个与第三个返回值赋给了 i1 与 f1 。第二个返回值赋给了空白符 _ ,然后自动丢弃掉。
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
改变外部变量
传递指针给函数不但可以节省内存,而且赋予了函数直接修改外部变量的能力,所以被修改的变量不再需要使用 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,这样的函数称为变参函数。
func myFunc(a, b, arg ...int) {}
示例函数和调用:
func Greeting(prefix string, who ...string)
Greeting("hello:", "Joe", "Anna", "Eileen")
在 Greeting 函数中,变量 who 的值为 []string{“Joe”, “Anna”, “Eileen”} 。
如果参数被存储在一个数组 arr 中,则可以通过 arr… 的形式来传递参数调用变参函数。
package main
import "fmt"
func main() {
x := Min(1, 3, 2, 0)
fmt.Printf("The minimum is: %d\n", x)
arr := []int{7,9,3,5,1}
x = Min(arr...)
fmt.Printf("The minimum in the array arr is: %d", x)
}
func Min(a ...int) int {
if len(a)==0 {
return 0
}
min := a[0]
for _, v := range a {
if v < min {
min = v
}
}
return min
}
输出:
The minimum is: 0
The minimum in the array arr is: 1
- 使用结构:
定义一个结构类型,假设它叫 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 语句之后)一刻才执行某个语句或函数
关键字 defer 的用法类似于面向对象编程语言 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!")
}
输出:
In Function1 at the top
In Function1 at the bottom!
Function2: Deferred until the end of the calling function!
请将 defer 关键字去掉并对比输出结果。
使用 defer 的语句同样可以接受参数,下面这个例子就会在执行 defer 语句时打印 0 :
func a() {
i := 0
defer fmt.Println(i)
i++
return
}
当有多个 defer 行为被注册时,它们会以逆序执行(类似栈,即后进先出):
func f() {
for i := 0; i < 5; i++ {
defer fmt.Printf(“%d “, i)
}
}
上面的代码将会输出: 4 3 2 1 0 。
关键字 defer 允许我们进行一些函数执行完成后的收尾工作,例如:
- 关闭文件流:
// open a file defer file.Close() - 解锁一个加锁的资源
mu.Lock() defer mu.Unlock() - 打印最终报告
printHeader() defer printFooter() - 关闭数据库链接
// open a database connection defer disconnectFromDB()
合理使用 defer 语句能够使得代码更加简洁。
以下代码模拟了上面描述的第 4 种情况:
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
}
输出:
ok, connected to db
Defering the database disconnect.
Doing some DB operations …
Oops! some crash or network error …
Returning from function here!
ok, disconnected from db
使用 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) { fmt.Println("entering:", s) }
func untrace(s string) { fmt.Println("leaving:", s) }
func a() {
trace("a")
defer untrace("a")
fmt.Println("in a")
}
func b() {
trace("b")
defer untrace("b")
fmt.Println("in b")
a()
}
func main() {
b()
}
输出:
entering: b
in b
entering: a
win a
leaving: a
leaving: b
6.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 的指针(详见第 10.1 节)。它也可以被用于基本类型:
v := new(int) 。make(T) 返回类型 T 的初始化之后的值,因此它比 new 进行更多的
工作(详见第 7.2.3/4 节、第 8.1.1 节和第 14.2.1 节)new() 是一个函数,不要忘记它
的括号
Go入门指南
本文档使用 看云 构建 - 118 -
copy、
append
用于复制和连接切片
panic、
recover
两者均用于错误处理机制
print、
println
底层打印函数(详见第 4.2 节),在部署环境中建议使用 fmt 包
complex、
real imag
用于创建和操作复数(详见第 4.5.2.2 节)
名称 说明
递归函数
当一个函数在其函数体内调用自身,则称之为递归。最经典的例子便是计算斐波那契数列,即每个数均为前两个数之和。
数列如下所示:
1, 1, 2, 3, 5, 8, 13, 21, 34, 55, 89, 144, 233, 377, 610, 987, 1597, 2584, 4181, 6765, 10946, …
下面的程序可用于生成该数列(示例 6.13 fibonacci.go):
package main
import "fmt"
func main() {
result := 0
for i := 0; i <= 10; i++ {
result = fibonacci(i)
fmt.Printf("fibonacci(%d) is: %d\n", i, result)
}
}
func fibonacci(n int) (res int) {
if n <= 1 {
res = 1
} else {
res = fibonacci(n-1) + fibonacci(n-2)
}
return
}
输出:
fibonacci(0) is: 1
fibonacci(1) is: 1
fibonacci(2) is: 2
fibonacci(3) is: 3
fibonacci(4) is: 5
fibonacci(5) is: 8
fibonacci(6) is: 13
fibonacci(7) is: 21
fibonacci(8) is: 34
fibonacci(9) is: 55
fibonacci(10) is: 89
在使用递归函数时经常会遇到的一个重要问题就是栈溢出:一般出现在大量的递归调用导致的程序栈内存分配耗尽。这个问题可以通过一个名为懒惰求值的技术解决,在 Go 语言中,我们可以使用管道(channel)和 goroutine来实现。
Go 语言中也可以使用相互调用的递归函数:多个函数之间相互调用形成闭环。因为 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) // this becomes Add(1, 2)
}
输出:
The sum of 1 and 2 is: 3
闭包
当我们不希望给函数起名字的时候,可以使用匿名函数,例如: func(x, y int) int { return x + y } 。这样的一个函数不能够独立存在(编译器会返回错误:non-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) 。下面是一个计算从 1 到 1 百万整数的总和的匿名函数:
func() {
sum = 0.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)
}
}
输出:
0 - g is of type func(int) and has value 0x681a80
1 - g is of type func(int) and has value 0x681b00
2 - g is of type func(int) and has value 0x681ac0
3 - g is of type func(int) and has value 0x681400
应用闭包:将函数作为返回值
在程序 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 类型的整数作为参数。
我们也可以将 Adder 返回的函数存到变量中(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 3:
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
计算函数执行时间
有时候,能够知道一个计算执行消耗的时间是非常有意义的,尤其是在对比和基准测试中。最简单的一个办法就是在计算开始之前设置一个起始时候,再由计算结束时的结束时间,最后取出它们的差值,就是这个计算所消耗的时间。想要实现这样的做法,可以使用 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)
通过内存缓存来提升性能
当在进行大量的计算时,提升性能最直接有效的一种方式就是避免重复计算。通过在内存中缓存和重复利用相同计算的结果,称之为内存缓存。
普通写法:4.730270 秒
内存缓存:0.001000 秒
内存缓存的优势显而易见,而且您还可以将它应用到其它类型的计算中,例如使用 map而不是数组或切片(Listing 6.21 - fibonacci_memoization.go):
package main
import (
"fmt"
"time"
)
const LIM = 41
var fibs [LIM]uint64
func main() {
var result uint64 = 0
start := time.Now()
for i := 0; i < LIM; i++ {
result = fibonacci(i)
fmt.Printf("fibonacci(%d) is: %d\n", i, result)
}
end := time.Now()
delta := end.Sub(start)
fmt.Printf("longCalculation took this amount of time: %s\n", delta)
}
func fibonacci(n int) (res uint64) {
// memoization: check if fibonacci(n) is already known in array:
if fibs[n] != 0 {
res = fibs[n]
return
}
if n <= 1 {
res = 1
} else {
res = fibonacci(n-1) + fibonacci(n-2)
}
fibs[n] = res
return
}
内存缓存的技术在使用计算成本相对昂贵的函数时非常有用(不仅限于例子中的递归),譬如大量进行相同参数的运算。这种技术还可以应用于纯函数中,即相同输入必定获得相同输出的函数。
- 点赞
- 收藏
- 关注作者
评论(0)