函数是一块执行特定任务的代码。一个函数是在输入源基础上,通过执行一系列的算法,生成预期的输出。
Go 里面有三种类型的函数:
func functionName(parameter_list) (return_value_list) {
…
}
//parameter_list 的形式为 (param1 type1, param2 type2, …)
//return_value_list 的形式为 (ret1 type1, ret2 type2, …)
函数的声明以关键词 func
开始,后面紧跟自定义的函数名 functionname (函数名)
。函数能够接收参数供自己使用,也可以返回零个或多个值(我们通常把返回多个值称为返回一组值)。
多值返回是 Go 的一大特性,为我们判断一个函数是否正常执行提供了方便。
func vals() (int, error) {
return 3, nil
}
func main() {
// 获取函数的两个返回值
a, err := vals()
if err != nil {
fmt.Println(a)
}
}
空白符用来匹配一些不需要的值,然后丢弃掉。
func vals() (int, error) {
return 3, nil
}
func main() {
// 如果你只对多个返回值里面的几个感兴趣
// 可以使用下划线(_)来忽略其他的返回值
c, _ := vals()
fmt.Println(c) //3
}
如果函数的最后一个参数是采用 ...type
的形式,那么这个函数就可以处理一个变长的参数,这时函数可以接受任意个 type
类型参数作为最后一个参数。需要注意只有函数的最后一个参数才允许是可变的。
func find(num int, nums ...int) {
fmt.Printf("type of nums is %T\n", nums)
found := false
for i, v := range nums {
if v == num {
fmt.Println(num, "found at index", i, "in", nums)
found = true
}
}
if !found {
fmt.Println(num, "not found in ", nums)
}
fmt.Printf("\n")
}
func main() {
find(89, 89, 90, 95) //type of nums is []int 89 found at index 0 in [89 90 95]
find(78, 38, 56, 98) //type of nums is []int 78 not found in [38 56 98]
find(87) //type of nums is []int 87 not found in []
}
可变参数函数的工作原理是把可变参数转换为一个新的切片。
func find(num int, nums ...int) {
fmt.Printf("type of nums is %T\n", nums)
found := false
for i, v := range nums {
if v == num {
fmt.Println(num, "found at index", i, "in", nums)
found = true
}
}
if !found {
fmt.Println(num, "not found in ", nums)
}
fmt.Printf("\n")
}
func main() {
nums := []int{89, 90, 95}
find(89, nums)
}
上面的例子中我们将一个切片传给一个可变参数函数。这种情况下无法通过编译,编译器报出错误 cannot use nums (type []int) as type int in argument to find
。原因是在这里 nums
已经是一个 int 类型切片,编译器试图在 nums
基础上再创建一个切片,所以失败。
有一个可以直接将切片传入可变参数函数的语法糖,你可以在在切片后加上 ...
后缀。如果这样做,切片将直接传入函数,不再创建新的切片。
func main() {
nums := []int{89, 90, 95}
find(89, nums...) //type of nums is []int 89 found at index 0 in [89 90 95]
}
如果一个变长参数的类型没有被指定,则可以使用默认的空接口 interface{}
,这样就可以接受任何类型的参数。该方法不仅可以用于长度未知的参数,还可以用于任何不确定类型的参数。一般而言我们会使用一个 for-range 循环以及 switch 结构对每个参数的类型进行判断:
func typecheck(..,..,values ...interface{}) {
for _, value := range values {
switch v := value.(type) {
case int: …
case float64: …
case string: …
case bool: …
default: …
}
}
}
函数重载(function overloading)指的是可以编写多个同名函数,只要它们拥有不同的形参与/或者不同的返回值,在 Go 里面函数重载是不被允许的。这将导致一个编译错误:
funcName redeclared in this book, previous declaration at lineno
Go语言中函数的传参都是值传递(传值),都是一个副本,一个拷贝。因为拷贝的内容有时候是非引用类型(int、string、struct等这些),这样就在函数中就无法修改原内容数据;有的是引用类型(指针、map、slice、chan等这些),这样就可以修改原内容数据。
func main() {
var args int64= 1
fmt.Printf("实际参数的地址 %p\n", &args) //实际参数的地址 0xc00006e090
modifiedNumber(args) // args就是实际参数
fmt.Printf("改动后的值是 %d\n",args) //改动后的值是 1
}
func modifiedNumber(args int64) { //这里定义的args就是形式参数
fmt.Printf("形参地址 %p \n",&args) //形参地址 0xc00006e098
args = 10
}
func main() {
var args = []int64{1,2,3}
fmt.Printf("切片args的地址: %p \n",args) //切片args的地址: 0xc00006c120
fmt.Printf("切片args第一个元素的地址: %p \n",&args[0]) //切片args第一个元素的地址: 0xc00006c120
fmt.Printf("直接对切片args取地址:%p \n",&args) //直接对切片args取地址:0xc000064440
modifiedNumber(args)
fmt.Println(args) //[10 2 3]
}
func modifiedNumber(args []int64) {
fmt.Printf("形参切片的地址 %p \n",args) //形参切片的地址 0xc00006c120
fmt.Printf("形参切片args第一个元素的地址: %p \n",&args[0]) //形参切片args第一个元素的地址: 0xc00006c120
fmt.Printf("直接对形参切片args取地址:%p \n",&args) //直接对形参切片args取地址:0xc0000644a0
args[0] = 10
}
关键字 defer 允许我们推迟到函数返回之前(或任意位置执行 return
语句之后)一刻才执行某个语句或函数。
func a() {
i := 0
defer fmt.Println(i)
i++
return
}
func main() {
a() //0
}
当一个函数内多次调用 defer
时,Go 会把 defer
调用放入到一个栈中,随后按照后进先出(Last In First Out, LIFO)的顺序执行。
func f() {
for i := 0; i < 5; i++ {
defer fmt.Printf("%d ", i)
}
}
func main() {
f() //4 3 2 1 0
}
defer的作用:
简化资源回收:在函数中,经常需要创建资源(比如:数据库连接、文件句柄、锁等) ,为了在函数执行完毕后,及时的释放资源;
panic异常的捕获
对返回值进行操作
1.关闭文件流
// open a file
defer file.Close()
2.解锁一个加锁的资源
mu.Lock()
defer mu.Unlock()
3.关闭数据库链接
// open a database connection
defer disconnectFromDB()
4.panic异常捕获
func f() {
defer func() {
if r := recover(); r != nil {
fmt.Println("Recovered in f", r)
}
}()
fmt.Println("Calling g.")
g()
fmt.Println("Returned normally from g.")
}
5.对函数return的返回值进行操作
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") //2021/04/13 15:15:24 func1("Go") = 7, EOF
}
init()函数会在每个包完成初始化后自动执行,并且执行优先级比main函数高。init 函数通常被用来:
package main
import "fmt"
var _ int64=s()
func init(){
fmt.Println("init function --->")
}
func s() int64{
fmt.Println("function s() --->")
return 1
}
func main(){
fmt.Println("main --->")
}
执行结果如下:
function s() --->
init function --->
main --->
init()函数特性:
func init(){
fmt.Println("init 1")
}
func init(){
fmt.Println("init2")
}
func main(){
fmt.Println("main")
}
/*执行结果:
init1
init2
main */
Go 语言拥有一些不需要进行导入操作就可以使用的内置函数。
名称 | 说明 |
---|---|
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 进行更多的工作 |
copy、append | 用于复制和连接切片 |
panic、recover | 两者均用于错误处理机制 |
print、println | 底层打印函数,在部署环境中建议使用 fmt 包 |
complex、real 、imag | 用于创建和操作复数 |
当我们不希望给函数起名字的时候,可以使用匿名函数。匿名函数即没有名称的函数。Go语言支持匿名函数是因为Go 语言支持头等函数的机制。支持头等函数(First Class Function)的编程语言,可以把函数赋值给变量,也可以把函数作为其它函数的参数或者返回值。
func main() {
a := func() {
fmt.Println("hello world first class function")
}
a()
fmt.Printf("%T", a)
}
在上面的程序中,我们将一个函数赋值给了变量 a
。这是把函数赋值给变量的语法。可以看到赋值给 a
的函数没有名称,由于没有名称,这类函数称为匿名函数(Anonymous Function)。
要调用一个匿名函数,可以不用赋值给变量。就像其它函数一样,还可以向匿名函数传递参数。
func main() {
func(n string) {
fmt.Println("Welcome", n) //Welcome Gophers
}("Gophers")
}
正如我们定义自己的结构体类型一样,我们可以定义自己的函数类型。
type add func(a int, b int) int
func main() {
var a add = func(a int, b int) int {
return a + b
}
s := a(5, 6)
fmt.Println("Sum", s) //Sum 11
}
以上代码片段创建了一个新的函数类型 add
,它接收两个整型参数,并返回一个整型。
然后向它赋值了一个符合 add
类型签名的函数。
函数可以作为其它函数的参数进行传递,然后在其它函数内调用执行
func simple(a func(a, b int) int) {
fmt.Println(a(60, 7))
}
func main() {
f := func(a, b int) int {
return a + b
}
simple(f) //67
}
函数可以作为其它函数的返回值进行传递
func simple() func(a, b int) int {
f := func(a, b int) int {
return a + b
}
return f
}
func main() {
s := simple()
fmt.Println(s(60, 7)) //67
}
在上面程序中, simple
函数返回了一个函数,并接受两个 int
参数,返回一个 int
。
我们调用了 simple
函数,并把 simple
的返回值赋值给了 s
。现在 s
包含了 simple
函数返回的函数。我们调用了 s
,并向它传递了两个 int 参数,程序输出 67。
闭包是由函数及其相关的引用环境组合而成的实体(即:闭包=函数+引用环境)。在函数式语言中,当内嵌函数体内引用到体外的变量时,将会把定义时涉及到的引用环境和函数体打包成一个整体(闭包)。
在Go语言中,匿名函数就是一个闭包,它可以直接引用外部函数的局部变量。
func main() {
a := 5
func() {
fmt.Println("a =", a)
}()
}
在上面的程序中,匿名函数访问了变量 a
,而 a
存在于函数体的外部。因此这个匿名函数就是闭包。
每一个闭包都会绑定它自己的外围变量(Surrounding Variable)。
func appendStr() func(string) string {
t := "Hello"
c := func(b string) string {
t = t + " " + b
return t
}
return c
}
func main() {
a := appendStr()
b := appendStr()
fmt.Println(a("World")) //Hello World
fmt.Println(b("Everyone")) //Hello Everyone
fmt.Println(a("Gopher")) //Hello World Gopher
fmt.Println(b("!")) //Hello Everyone !
}
在上面程序中,函数 appendStr
返回了一个闭包。这个闭包绑定了变量 t
。
我们首先用参数 World
调用了 a
。现在 a
中 t
值变为了 Hello World
。然后我们又用参数 Everyone
调用了 b
。由于 b
绑定了自己的变量 t
,因此 b
中的 t
还是等于初始值 Hello
。于是该函数调用之后,b
中的 t
变为了 Hello Everyone
。
闭包的使用
package main
import (
"fmt"
)
type student struct {
firstName string
lastName string
grade string
country string
}
func filter(s []student, f func(student) bool) []student {
var r []student
for _, v := range s {
if f(v) == true {
r = append(r, v)
}
}
return r
}
func main() {
s1 := student{
firstName: "Naveen",
lastName: "Ramanathan",
grade: "A",
country: "India",
}
s2 := student{
firstName: "Samuel",
lastName: "Johnson",
grade: "B",
country: "USA",
}
s := []student{s1, s2}
f := filter(s, func(s student) bool {
if s.grade == "B" {
return true
}
return false
})
fmt.Println(f) //[{Samuel Johnson B USA}]
}
在上面的代码中,filter
的第二个参数是一个函数。这个函数接收 student
参数,返回一个 bool
值。这个函数计算了某一学生是否满足筛选条件。我们遍历了 student
切片,将每个学生作为参数传递给了函数 f
。如果该函数返回 true
,就表示该学生通过了筛选条件,接着将该学生添加到了结果切片 r
中。
在 main
函数中,我们首先创建了两个学生 s1
和 s2
,并将他们添加到了切片 s
。现在假设我们想要查询所有成绩为 B
的学生。为了实现这样的功能,我们传递了一个检查学生成绩是否为 B
的函数,如果是,该函数会返回 true
。我们把这个函数作为参数传递给了 filter
函数。上述程序会输出:
[{Samuel Johnson B USA}]
假设我们想要查找所有来自印度的学生。通过修改传递给 filter
的函数参数,就很容易地实现了。
c := filter(s, func(s student) bool {
if s.country == "India" {
return true
}
return false
})
fmt.Println(c) //[{Naveen Ramanathan A India}]
在 func
这个关键字和方法名中间加入了一个特殊的接收器类型。接收器可以是结构体类型或者是非结构体类型。接收器是可以在方法的内部访问的。
func (t Type) methodName(parameter_list) (return_value_list) {
}
package main
import (
"fmt"
)
type Employee struct {
name string
salary int
currency string
}
/*
displaySalary() 方法将 Employee 做为接收器类型
*/
func (e Employee) displaySalary() {
fmt.Printf("Salary of %s is %s%d \n", e.name, e.currency, e.salary)
}
/*
displaySalary()方法被转化为一个函数,把 Employee 当做参数传入。
*/
func displaySalary(e Employee) {
fmt.Printf("Salary of %s is %s%d \n", e.name, e.currency, e.salary)
}
func main() {
emp1 := Employee {
name: "Sam Adolf",
salary: 5000,
currency: "$",
}
emp1.displaySalary() //调用方法 Salary of Sam Adolf is $5000
displaySalary(emp1) //调用函数 Salary of Sam Adolf is $5000
}
Go 不是纯粹的面向对象编程语言,而且Go不支持类。因此,基于类型的方法是一种实现和类相似行为的途径。
相同的名字的方法可以定义在不同的类型上,而相同名字的函数是不被允许的。
package main
import (
"fmt"
"math"
)
type Rectangle struct {
length int
width int
}
type Circle struct {
radius float64
}
func (r Rectangle) Area() int {
return r.length * r.width
}
func (c Circle) Area() float64 {
return math.Pi * c.radius * c.radius
}
func main() {
r := Rectangle{
length: 10,
width: 5,
}
fmt.Printf("Area of rectangle %d\n", r.Area())
c := Circle{
radius: 12,
}
fmt.Printf("Area of circle %f", c.Area())
}
package main
import (
"fmt"
)
type Employee struct {
name string
age int
}
/*
使用值接收器的方法。
*/
func (e Employee) changeName(newName string) {
e.name = newName
}
/*
使用指针接收器的方法。
*/
func (e *Employee) changeAge(newAge int) {
e.age = newAge
}
func main() {
e := Employee{
name: "Mark Andrew",
age: 50,
}
fmt.Printf("Employee name before change: %s", e.name) //Employee name before change: Mark Andrew
e.changeName("Michael Andrew")
fmt.Printf("\nEmployee name after change: %s", e.name) //Employee name after change: Mark Andrew
fmt.Printf("\n\nEmployee age before change: %d", e.age) //Employee age before change: 50
(&e).changeAge(51)
fmt.Printf("\nEmployee age after change: %d", e.age) //Employee age after change: 51
e.changeAge(52)
fmt.Printf("\nEmployee age after change: %d", e.age) //Employee age after change: 52
}
在上面的程序中,changeName
方法有一个值接收器 (e Employee)
,而 changeAge
方法有一个指针接收器 (e *Employee)
。在 changeName
方法中对 Employee
结构体的字段 name
所做的改变对调用者是不可见的,因此程序在调用 e.changeName("Michael Andrew")
这个方法的前后打印出相同的名字。由于 changeAge
方法是使用指针 (e *Employee)
接收器的,所以在调用 (&e).changeAge(51)
方法对 age
字段做出的改变对调用者将是可见的。我们使用 (&e).changeAge(51)
来调用 changeAge
方法。由于 changeAge
方法有一个指针接收器,所以我们使用 (&e)
来调用这个方法。其实没有这个必要,Go语言让我们可以直接使用 e.changeAge(51)
。e.changeAge(51)
会自动被Go语言解释为 (&e).changeAge(51)
。
一般来说,指针接收器可以使用在:
对方法内部的接收器所做的改变应该对调用者可见时。
当拷贝一个结构体的代价过于昂贵时。考虑下一个结构体有很多的字段。在方法内使用这个结构体做为值接收器需要拷贝整个结构体,这是很昂贵的。在这种情况下使用指针接收器,结构体不会被拷贝,只会传递一个指针到方法内部使用。
为了在一个类型上定义一个方法,方法的接收器类型定义和方法的定义应该在同一个包中。
package main
func (a int) add(b int) {
}
func main() {
}
在上面程序中,我们尝试把一个 add
方法添加到内置的类型 int
。这是不允许的,因为 add
方法的定义和 int
类型的定义不在同一个包中。该程序会抛出编译错误 cannot define new methods on non-local type int
。
我们可以为内置类型 int 创建一个类型别名,然后创建一个以该类型别名为接收器的方法。
package main
import "fmt"
type myInt int
func (a myInt) add(b myInt) myInt {
return a + b
}
func main() {
num1 := myInt(5)
num2 := myInt(10)
sum := num1.add(num2)
fmt.Println("Sum is", sum)
}