1、Go语言函数
函数是基本的代码块,用于执行一个任务。
Go 语言最少有个 main() 函数。
你可以通过函数来划分不同功能,逻辑上每个函数执行的是指定的任务。
函数声明告诉了编译器函数的名称,返回类型,和参数。
Go 语言标准库提供了多种可动用的内置的函数。例如,len() 函数可以接受不同类型参数并返回该类型的长度。如
果我们传入的是字符串则返回字符串的长度,如果传入的是数组,则返回数组中包含的元素个数。
1.1 函数定义
Go 语言函数定义格式如下:
func function_name( [parameter list] ) [return_types] {
函数体
}
函数定义解析:
-
func:函数由 func 开始声明
-
function_name:函数名称,参数列表和返回值类型构成了函数签名。
-
parameter list:参数列表,参数就像一个占位符,当函数被调用时,你可以将值传递给参数,这个值被称为
实际参数。参数列表指定的是参数类型、顺序、及参数个数。参数是可选的,也就是说函数也可以不包含参
数。
-
return_types:返回类型,函数返回一列值。return_types 是该列值的数据类型。有些功能不需要返回值,这
种情况下 return_types 不是必须的。
-
函数体:函数定义的代码集合。
// 函数传入两个整型参数num1和num2,并返回这两个参数的最大值
// 函数返回两个数的最大值
func max(num1, num2 int) int {
/* 声明局部变量 */
var result int
if num1 > num2 {
result = num1
} else {
result = num2
}
return result
}
1.2 函数调用
当创建函数时,你定义了函数需要做什么,通过调用该函数来执行指定任务。
package main
import "fmt"
func main() {
//定义切片
var num []int
for i := 1; i < 10; i++ {
//将数据添加到切片中去
num = append(num, i)
}
PrintingMultiplicationFormulas(num)
}
// 打印九九乘法口诀
func PrintingMultiplicationFormulas(num [] int){
for i := 1; i < 10; i++ {
for j := 1; j < i+1; j++ {
value := num[j-1] * i
fmt.Printf("%d*%d=%d\t", j, i, value)
}
fmt.Println()
}
}
# 程序输出
1*1=1
1*2=2 2*2=4
1*3=3 2*3=6 3*3=9
1*4=4 2*4=8 3*4=12 4*4=16
1*5=5 2*5=10 3*5=15 4*5=20 5*5=25
1*6=6 2*6=12 3*6=18 4*6=24 5*6=30 6*6=36
1*7=7 2*7=14 3*7=21 4*7=28 5*7=35 6*7=42 7*7=49
1*8=8 2*8=16 3*8=24 4*8=32 5*8=40 6*8=48 7*8=56 8*8=64
1*9=9 2*9=18 3*9=27 4*9=36 5*9=45 6*9=54 7*9=63 8*9=72 9*9=81
package main
import "fmt"
// 函数传入两个整型参数num1和num2,并返回这两个参数的最大值
// 函数返回两个数的最大值
func max(num1, num2 int) int {
/* 声明局部变量 */
var result int
if num1 > num2 {
result = num1
} else {
result = num2
}
return result
}
func main() {
/* 定义局部变量 */
var a int = 100
var b int = 200
var ret int
/* 调用函数并返回最大值 */
ret = max(a, b)
// 最大值是 : 200
fmt.Printf("最大值是 : %d\n", ret)
}
1.3 函数返回多个值
package main
import "fmt"
func swap(x, y string) (string, string) {
return y, x
}
func main() {
a, b := swap("Google", "Baidu")
// Baidu Google
fmt.Println(a, b)
}
1.4 函数参数
函数如果使用参数,该变量可称为函数的形参。
形参就像定义在函数体内的局部变量。
调用函数,可以通过两种方式来传递参数:
传递类型 | 描述 |
---|---|
值传递 | 值传递是指在调用函数时将实际参数复制一份传递到函数中,这样在函数中如果对参数进行修改,将不会影响到实际参数。 |
引用传递 | 引用传递是指在调用函数时将实际参数的地址传递到函数中,那么在函数中对参数所进行的修改,将影响到实际参数。 |
默认情况下,Go 语言使用的是值传递,即在调用过程中不会影响到实际参数。
1.4.1 Go 语言函数值传递值
传递是指在调用函数时将实际参数复制一份传递到函数中,这样在函数中如果对参数进行修改,将不会影响到实际
参数。
默认情况下,Go 语言使用的是值传递,即在调用过程中不会影响到实际参数。
package main
import "fmt"
func main() {
/* 定义局部变量 */
var a int = 100
var b int = 200
/*
交换前 a 的值为 : 100
交换前 b 的值为 : 200
交换后 a 的值 : 100
交换后 b 的值 : 200
*/
fmt.Printf("交换前 a 的值为 : %d\n", a)
fmt.Printf("交换前 b 的值为 : %d\n", b)
/* 通过调用函数来交换值 */
swap(a, b)
fmt.Printf("交换后 a 的值 : %d\n", a)
fmt.Printf("交换后 b 的值 : %d\n", b)
}
/* 定义相互交换值的函数 */
func swap(x, y int) int {
var temp int
temp = x /* 保存 x 的值 */
x = y /* 将 y 值赋给 x */
y = temp /* 将 temp 值赋给 y*/
return temp
}
程序中使用的是值传递, 所以两个值并没有实现交互,我们可以使用引用传递来实现交换效果。
交换值可以这么写:
a := 100
b := 200
a, b = b, a
1.4.2 Go 语言函数引用传递值
引用传递是指在调用函数时将实际参数的地址传递到函数中,那么在函数中对参数所进行的修改,将影响到实际参
数。引用传递指针参数传递到函数内。
package main
import "fmt"
func main() {
/* 定义局部变量 */
var a int = 100
var b int = 200
/*
交换前,a 的值 : 100
交换前,b 的值 : 200
交换后,a 的值 : 200
交换后,b 的值 : 100
*/
fmt.Printf("交换前,a 的值 : %d\n", a)
fmt.Printf("交换前,b 的值 : %d\n", b)
/* 调用 swap() 函数
* &a 指向 a 指针,a 变量的地址
* &b 指向 b 指针,b 变量的地址
*/
swap(&a, &b)
fmt.Printf("交换后,a 的值 : %d\n", a)
fmt.Printf("交换后,b 的值 : %d\n", b)
}
func swap(x *int, y *int) {
var temp int
temp = *x /* 保存 x 地址上的值 */
*x = *y /* 将 y 值赋给 x */
*y = temp /* 将 temp 值赋给 y */
}
所有参数都是值传递,slice
,map
,channel
会有传引用的错觉,比如切片,它背后对应的是一个数组,切片
本身是一个数据结构,在这个数据结构中包含了指向了这个数组的指针。所以说,即便是在传值的情况下这个结构
被复制到函数里了,在通过指针去操作这个数组的值的时候,其实是操作的是同一块空间,实际上是结构被复制
了,但是结构里包含的指针指向的是同一个数组,所以才有这个错觉。
1.5 函数用法
函数用法 | 描述 |
---|---|
函数作为另外一个函数的实参 | 函数定义后可作为另外一个函数的实参数传入 |
闭包 | 闭包是匿名函数,可在动态编程中使用 |
方法 | 方法就是一个包含了接受者的函数 |
1.5.1 Go 语言函数作为实参
Go 语言可以很灵活的创建函数,并作为另外一个函数的实参。
package main
import (
"fmt"
"math"
)
func main() {
/* 声明函数变量 */
getSquareRoot := func(x float64) float64 {
return math.Sqrt(x)
}
/* 使用函数 */
// 3
fmt.Println(getSquareRoot(9))
}
函数作为参数传递,实现回调。
package main
import "fmt"
// 声明一个函数类型
type cb func(int) int
func main() {
/*
我是回调,x:1
我是回调,x:2
*/
testCallBack(1, callBack)
testCallBack(2, func(x int) int {
fmt.Printf("我是回调,x:%d\n", x)
return x
})
}
func testCallBack(x int, f cb) {
f(x)
}
func callBack(x int) int {
fmt.Printf("我是回调,x:%d\n", x)
return x
}
1.5.2 Go 语言函数闭包
Go 语言支持匿名函数,可作为闭包。匿名函数是一个"内联"语句或表达式。匿名函数的优越性在于可以直接使用
函数内的变量,不必申明。
实例1:
package main
import "fmt"
func getSequence() func() int {
i := 0
return func() int {
i += 1
return i
}
}
func main() {
/* nextNumber 为一个函数,函数 i 为 0 */
nextNumber := getSequence()
/* 调用 nextNumber 函数,i 变量自增 1 并返回 */
// 1
fmt.Println(nextNumber())
// 2
fmt.Println(nextNumber())
// 3
fmt.Println(nextNumber())
/* 创建新的函数 nextNumber1,并查看结果 */
nextNumber1 := getSequence()
// 1
fmt.Println(nextNumber1())
// 2
fmt.Println(nextNumber1())
}
实例2:带参数的闭包函数调用
package main
import "fmt"
func main() {
add_func := add(1, 2)
// 1 3
fmt.Println(add_func())
// 2 3
fmt.Println(add_func())
// 3 3
fmt.Println(add_func())
}
// 闭包使用方法
func add(x1, x2 int) func() (int, int) {
i := 0
return func() (int, int) {
i++
return i, x1 + x2
}
}
package main
import "fmt"
func main() {
add_func := add(1, 2)
// 1 3 2
fmt.Println(add_func(1, 1))
// 2 3 0
fmt.Println(add_func(0, 0))
// 3 3 4
fmt.Println(add_func(2, 2))
}
// 闭包使用方法
func add(x1, x2 int) func(x3 int, x4 int) (int, int, int) {
i := 0
return func(x3 int, x4 int) (int, int, int) {
i++
return i, x1 + x2, x3 + x4
}
}
/*
闭包使用方法,函数声明中的返回值(闭包函数)不用写具体的形参名称
func add(x1, x2 int) func(int, int) (int, int, int) {
i := 0
return func(x3, x4 int) (int, int, int) {
i += 1
return i, x1 + x2, x3 + x4
}
}
*/
1.5.3 Go 语言函数方法
Go 语言中同时有函数和方法。一个方法就是一个包含了接受者的函数,接受者可以是命名类型或者结构体类型的
一个值或者是一个指针。所有给定类型的方法属于该类型的方法集。语法格式如下:
func (variable_name variable_data_type) function_name() [return_type]{
/* 函数体*/
}
package main
import (
"fmt"
)
/* 定义结构体 */
type Circle struct {
radius float64
}
func main() {
var c1 Circle
c1.radius = 10.00
// 圆的面积 = 314
fmt.Println("圆的面积 = ", c1.getArea())
}
//该 method 属于 Circle 类型对象中的方法
func (c Circle) getArea() float64 {
//c.radius 即为 Circle 类型对象中的属性
return 3.14 * c.radius * c.radius
}
关于值和指针,如果想在方法中改变结构体类型的属性,需要对方法传递指针。
package main
import (
"fmt"
)
/* 定义结构体 */
type Circle struct {
radius float64
}
func main() {
var c Circle
// 0
fmt.Println(c.radius)
c.radius = 10.00
// 100
fmt.Println(c.getArea())
c.changeRadius(20)
// 20
fmt.Println(c.radius)
change(&c, 30)
// 30
fmt.Println(c.radius)
}
func (c Circle) getArea() float64 {
return c.radius * c.radius
}
// 注意如果想要更改成功c的值,这里需要传指针
func (c *Circle) changeRadius(radius float64) {
c.radius = radius
}
// 以下操作将不生效
/*
func (c Circle) changeRadius(radius float64) {
c.radius = radius
}
*/
// 引用类型要想改变值需要传指针
func change(c *Circle, radius float64) {
c.radius = radius
}
1.6 不定参数
Go函数支持不定数目的形式参数,不定参数声明使用param ...type
的语法格式。
函数的不定参数有如下几个特点:
(1)、所有的不定参数类型必须是相同的。
(2)、不定参数必须是函数的最后一个参数。
(3)、不定参数名在函数体内相当于切片,对切片的操作同样适合对不定参数的操作。
package main
import "fmt"
func sum(arr ...int) (sum int) {
// 此时arr就相当于切片,可以使用range访问
for _, v := range arr {
sum += v
}
return
}
func main() {
result := sum(1, 2, 3, 5, 6, 7)
// 24
fmt.Print(result)
}
(4)、切片可以作为参数传递给不定参数,切片名后要加上...
。
package main
import "fmt"
func sum(arr ...int) (sum int) {
for _, v := range arr {
sum += v
}
return
}
func main() {
// 数纽不可以作为实参传递给不定参数的函数
slice := []int{1, 2, 3, 4, 5}
result := sum(slice...)
// 15
fmt.Print(result)
}
(5)、形参为不定参数的函数和形参为切片的函数类型不相同。
package main
import "fmt"
func suma(arr ...int) (sum int) {
for v := range arr {
sum += v
}
return
}
func sumb(arr []int) (sum int) {
for v := range arr {
sum += v
}
return
}
func main() {
// func(...int) int
fmt.Printf("%T\n", suma)
// func([]int) int
fmt.Printf("%T\n", sumb)
}
1.7 匿名函数
package main
import "fmt"
// 匿名函数被直接赋值函数变量
var sum = func(a, b int) int {
return a + b
}
func doinput(f func(int, int) int, a, b int) int {
return f(a, b)
}
//匿名函数作为返回值
func wrap(op string) func(int, int) int {
switch op {
case "add":
return func(a, b int) int {
return a + b
}
case "sub":
return func(a, b int) int {
return a + b
}
default:
return nil
}
}
func main() {
// 匿名函数直接被调用
defer func() {
if err := recover(); err != nil {
fmt.Println(err)
}
}()
sum(1, 2)
doinput(func(x, y int) int {
return x + y
}, 1, 2)
opFunc := wrap("add")
re := opFunc(2, 3)
// 5
fmt.Printf("%d\n", re)
}
1.8 闭包
闭包是由函数及其相关引用环境组合而成的实体,一般通过在匿名函数中引用外部函数的局部变量或包全局变量构
成。闭包=函数+引用环境。
闭包对闭包外的环境引入是直接引用,编译器检测到闭包,会将闭包引用的外部变量分配到堆上。
如果函数返回的闭包引用了该函数的局部变量(参数或函数内部变量):
(1)、多次调用该函数,返回的多个闭包所引用的外部变量是多个副本,原因是每次调用函数都会为局部变量分配
内存。
(2)、用一个闭包函数多次,如果该闭包修改了其引用的外部变量,则每一次调用该闭包对该外部变量都有影响,
因为闭包函数共享外部引用。
package main
func fa(a int) func(i int) int {
return func(i int) int {
println(&a, a)
a = a + i
return a
}
}
func main() {
// f引用的外部的闭包环境包括本次函数调用的形参a的值1
f := fa(1)
// f引用的外部的闭包环境包括本次函数调用的形参a的值1
g := fa(1)
// 此时f,g引用的闭包环境中的a的值并不是同一个,而是两次函数调用产生的副本
// 0xc000047f60 1
// 2
println(f(1))
// 0xc000047f60 2
// 3
// 多次调用f引用的是同一个副本a
println(f(1))
// g中a的值仍然是1
// 0xc000047f58 1
// 2
println(g(1))
// 0xc000047f58 2
// 3
println(g(1))
}
package main
var (
a = 0
)
func fa() func(i int) int {
return func(i int) int {
println(&a, a)
a = a + i
return a
}
}
func main() {
// f引用的外部的闭包环境包括全局变量a
f := fa()
// f引用的外部的闭包环境包括全局变量a
g := fa()
// 此时f,g引用的闭包环境中的a的值是同一个
// 0x41cda0 0
// 1
println(f(1))
// 0x41cda0 1
// 2
println(f(1))
// 0x41cda0 2
// 3
println(f(1))
// 0x41cda0 3
// 4
println(g(1))
}
package main
func fa(base int) (func(int) int, func(int) int) {
println(&base, base)
add := func(i int) int {
base += i
println(&base, base)
return base
}
sub := func(i int) int {
base -= i
println(&base, base)
return base
}
return add, sub
}
func main() {
//f,g 闭包引用的base是同一个,是fa函数调用是的形参
// 0xc000047f18 0
f, g := fa(0)
// 0xc000047f10 0
//s,k 闭包引用的base是同一个,是fa函数调用的的形参
s, k := fa(0)
//f,g 和 s,k引用不同的闭包变量,这个是由于fa每次调用都要重新分配形参
// 0xc000047f18 1
// 0xc000047f18 -1
// 1 -1
println(f(1), g(2))
// 0xc000047f10 1
// 0xc000047f10 -1
// 1 -1
println(s(1), k(2))
}
闭包的价值:闭包最初的目的是减少全局变量,在函数调用的过程中隐式地传递共享变量,有其有用的一面;但是
这种隐秘的共享变量的方式带来的坏处是不够直接,不够清晰,除非是非常有价值的地方,一般不建议使用闭包。
对象是附有行为的数据,而闭包是附有数据的行为,类在定义时已经显式地集中定义了行为,但是闭包中的数据没
有显式地集中声明的地方,这种数据和行为耦合的模型不是一种推荐的编程模型,闭包仅仅是锦上添花的东西,不
是不可缺少的。
1.9 方法调用
本节主要讨论类型方法的调用方式、方法集、方法变量和方法表达式。
1.9.1 一般调用
类型方法的一般调用方式:
TypeInstanceName.MethodName(ParamList)
TypeInstanceName
:类型实例名或指向实例的指针变量名;MethodName
:类型方法名;ParamList
:方法参数;
package main
import "fmt"
type T struct {
a int
}
func (t T) Get() int {
return t.a
}
func (t *T) Set(i int) {
t.a = i
}
func main() {
// var t = &T{}
var t = T{}
// 普通方法调用
t.Set(2)
// 2
// 普通方法调用
fmt.Print(t.Get())
}
1.9.2 方法值
变量 x 的静态类型是 T,M 是类型 T 的一个方法,x.T 被称为方法值(method value)。x.T 是一个函数类型变
量,可以赋值给其他变量,并像普通的函数名一样使用。
f := x.M
f(args...)
等价于
x.M(args ...)
方法值(method value)其实是一个带闭包的函数变量,其底层实现原理和带有闭包的匿名函数类似,接收值被隐
式地绑定到方法值(method value)的闭包环境中,后续调用不需要再显式地传递接收者。
package main
import "fmt"
type T struct {
a int
}
func (t T) Get() int {
return t.a
}
func (t *T) Set(i int) {
t.a = i
}
func (t *T) Print() {
fmt.Printf("%p,%v,%d\n", t, t, t.a)
}
func main() {
// var t = &T{}
var t = T{}
// 方法值调用
f := t.Set
// 方法值调用
f(2)
t.Print()
// 方法值调用
f(3)
t.Print()
}
# 程序输出
0xc000016098,&{2},2
0xc000016098,&{3},3
1.9.3 方法表达式
方法表达式相当于提供一种语法将类型方法调用显式地转换为函数调用,接收者(receiver)必须显式地传递进去。
下面定义一个类型 T ,增加两个方法,方法Get的接收者为T,方法Set的接收者类型为 *T。
package main
import "fmt"
type T struct {
a int
}
func (t T) Get() int {
return t.a
}
func (t *T) Set(i int) {
t.a = i
}
func (t *T) Print() {
fmt.Printf("%p,%v,%d\n", t, t, t.a)
}
func main() {
// 如下方法表达式调用都是等价的
t := T{a: 1}
// 普通方法调用
result := t.Get()
// 1
fmt.Println(result)
// 方法表达式调用
result1 := T.Get(t)
// result1 := (T).Get(t)
// 1
fmt.Println(result1)
// 方法表达式调用
f1 := t.Get
// f1 := (t).Get
result2 := f1()
// 1
fmt.Println(result2)
// 方法表达式调用
f2 := T.Get
// f2 := (T).Get
result3 := f2(t)
// 1
fmt.Println(result3)
// 方法表达式调用
f3 := (*T).Get
result4 := f3(&t)
// 1
fmt.Println(result4)
// 方法表达式调用
t.Set(10)
// 10
fmt.Println(t.Get())
// 方法表达式调用
f4 := t.Set
f4(20)
// 20
fmt.Println(t.Get())
// 方法表达式调用
f5 := (*T).Set
f5(&t, 30)
// 30
fmt.Println(t.Get())
// 0xc0000120a8,&{30},30
t.Print()
}
# 程序输出
1
1
1
1
1
10
20
30
0xc0000120a8,&{30},30
表达式 T.Get 和 (*T).Set 被称为方法表达式(method expression),方法表达式可以看作函数名,只不过这个函
数的首个参数是接收者的实例或指针。T.Get 的函数签名是 func(t T) int,(*T).Set 的函数签名是
func(t *T,i int)
。注意:这里的 T.Get 不能写成 (*T).Get
,(*T).Set
也不能写成 T.Set,在方法表达式中编
译器不会做自动转换。
1.9.4 方法集
命名类型方法接收者有两种类型,一个是值类型,另一个是指针类型,这个和函数是一样的,前者的形参是值类
型,后者的形参是指针类型。无论接收者是什么类型,方法和函数的实参传递都是值拷贝。如果接收者是值类型,
则传递的是值的副本;如果接收者是指针类型,则传递的是指针的副本。
package main
import "fmt"
type Int int
func (a Int) Max(b Int) Int {
if a >= b {
return a
} else {
return b
}
}
func (i *Int) Set(a Int) {
*i = a
}
func (i Int) Print() {
fmt.Printf("value=%d\n", i)
}
func main() {
var a Int = 10
var b Int = 50
c := a.Max(b)
// value=50
c.Print()
// value=50
// 内部被编译器转换为c.Print()
(&c).Print()
// 内部被编译器转化为(&a).Set(20)
a.Set(20)
// value=20
a.Print()
(&a).Set(30)
// value=30
a.Print()
}
# 程序输出
value=50
value=50
value=20
value=30
上面示例定义了一个新类型 Int,新类型的底层类型是 int,Int 虽然不能继承 int 的方法,但底层类型支持的操作
(算术运算和赋值运算)可以被上层类型继承,这是Go类型系统的一个特点。
接收者是 Int 类型的方法集合(method set):
func (i Int) Print()
func (a Int) Max(b Int) Int
接收者是 *Int 类型的方法集合(method set):
func (i *Int) Set(a Int)
为了简化描述,将接收者(receiver)为值类型 T 的方法的集合记录为 S,将接收者(receiver)为指针类型 *T 的方法
的集合统称为 *S。类型的方法集总结如下:
(1)、T 类型的方法集是 S。
(2)、*T 类型的方法集是 S 和 *S。
从上面的示例可以看出,在直接使用类型实例调用类型的方法时,无论值类型变量还是指针类型变量,都可以调用
类型的所有方法,原因是编译器在编译期间能够识别出这种调用关系,做了自动的转换。比如 a.Set (使用值类型
实例调用指针接收者方法,编译器会自动将其转换为 (&a).Set() ,(&a).Print() 使用指针类型实例调用值类型接收者
方法,编译器自动将其转化为 a.Print()。
前面讲到的另外两种调用方法:方法的值和方法表达式,编译器对这两种方法的调用处理也不相同,具体见接下来
的介绍。
1.9.5 值调用和表达式调用的方法集
前面介绍方法集时我们知道,具体类型实例变量直接调用其方法时,编译器会所调用方法进行自动转换,即使接收
者是指针的方法,仍然可以使用值类型变量进行调用。下面讨论在以下两种情况下编译器是否会进行方法的自动转
换。
(1)、通过类型字面量显式地进行值调用和表达式调用,可以看到在这种情况下编译器不会做自动转换,会进行严
格的方法集检查。
type Data struct{}
func (Data) TestValue() {}
func (*Data) TestPointer() {}
// 这种字面量显式调用,无论值调用,还是表达式调用
// 编译器都不会进行方法集的自动转换,编译器会严格校验方法集
// *Data方法集是TestPointer和TestValue
// Data方法集只有TestValue
(*Data) (&struct{}{}).TestPointer() // 显式的调用
(*Data) (&struct{}{}).TestValue() // 显式的调用
(Data) (struct{}{}).TestValue() // method value
Data.TestValue(struct{}{}) // method expression
// 如下调用因为方法集不匹配而失败
// type Data has no method TestPoiter
Data.TestPoiter(struct{}{})
// cannot call pointer method on Data(struct{}{})
(Data)(struct{}{}).TestPointer()
(2)、通过类型变量进行值调用和表达式调用,在这种情况下,使用值调用(method value)方式调用时编译器会进
行自动转换,使用表达式调用(method expression)方式调用时编译器不会进行转换,会进行严格的方法集检查。
package main
type Data struct{}
func (Data) TestValue() {}
func (*Data) TestPointer() {}
func main() {
// 声明一个类型变量a
var a Data = struct{}{}
// 表达式调用编译器不会进行自动转换
Data.TestValue(a)
// Data.TestValue(&a)
(*Data).TestPointer(&a)
// type Data has no method TestPointer
// Data.TestPointer(&a)
// 值调用编译器会进行自动转换
f := a.TestValue
f()
// 编译器帮助转换a.TestValue
y := (&a).TestValue
y()
// 会转换为(&a).TestPointer
g := a.TestPointer
g()
x := (&a).TestPointer
x()
}
package main
type Data struct{}
func (Data) TestValue() {}
func (*Data) TestPointer() {}
func main() {
// p的类型是*Data,但是p的值是nil,p指向的是一块未初始化的类型实例
var p *Data = nil
// 指针方法集的调用是可以使用nil传递进去的,只要方法内部不去访问nil指向的实例,不会引发panic
// 直接调用
// method value
m := p.TestPointer
m()
// method value等价p.TestPointer()
m = (*Data)(nil).TestPointer
m()
// p为nil,无法通过*p访问p指向的实例,所以此调用会导致panic
// (*p).TestValue()
// cannot convert nil to type Data
// (Data)(nil).TestValue()
// invalid memory address or nil pointer dereference
// p.TestValue()
// cannot use nil as type Data in function argument
// Data.TestValue(nil)
// method expression
(*Data).TestPointer(nil)
(*Data).TestPointer(p)
// 等价上面调用
s := (*Data).TestPointer
s(nil)
s(p)
}
1.10 函数类型
package main
import "fmt"
// 有名函数定义,函数名是add
// add类型是函数字面量类型 func (int,int) int
func add(a, b int) int {
return a + b
}
// 函数声明语句,用于go代码调用汇编代码
// func add(int,int) int
// add函数的签名,实际上就是add的字面量类型
// func (int,int) int
// 新定义函数类型ADD
// ADD底层类型是函数字面量类型func (int,int) int
type ADD func(int, int) int
// add和ADD的底层类型相同,并且add是字面量类型
// 所以add可直接赋值给ADD类型的变量f
var g ADD = add
func main() {
// 匿名函数
// 其不能独立存在,常常作为函数参数、返回值或者赋值给某个变量
// 匿名函数可以看作是函数字面量类型直接显式初始化
// 匿名函数的类型也是函数字面量类型func (int,int) int
f := func(a, b int) int {
return a + b
}
g(1, 2)
f(1, 2)
// f和add的函数签名相同
// func(int, int) int
fmt.Printf("%T\n", f)
// func(int, int) int
fmt.Printf("%T\n", add)
}
1.11 递归函数
递归,就是在运行的过程中调用自己。
// 语法格式如下
func recursion() {
/* 函数调用自身 */
recursion()
}
func main() {
recursion()
}
Go 语言支持递归。但我们在使用递归时,开发者需要设置退出条件,否则递归将陷入无限循环中。
递归函数对于解决数学上的问题是非常有用的,就像计算阶乘,生成斐波那契数列等。
// 递归函数实例阶乘
package main
import "fmt"
func Factorial(n uint64) (result uint64) {
if n > 0 {
result = n * Factorial(n-1)
return result
}
return 1
}
func main() {
var i int = 15
// 15 的阶乘是 1307674368000
fmt.Printf("%d 的阶乘是 %d\n", i, Factorial(uint64(i)))
}
// 递归函数实现斐波那契数列
package main
import "fmt"
func fibonacci(n int) int {
if n < 2 {
return n
}
return fibonacci(n-2) + fibonacci(n-1)
}
func main() {
var i int
// 0 1 1 2 3 5 8 13 21 34
for i = 0; i < 10; i++ {
fmt.Printf("%d\t", fibonacci(i))
}
}
// 更好的一种fibonacci实现,用到多返回值特性,降低复杂度
package main
import "fmt"
func fibonacci2(n int) (int, int) {
if n < 2 {
return 0, n
}
a, b := fibonacci2(n - 1)
return b, a + b
}
func fibonacci(n int) int {
_, b := fibonacci2(n)
return b
}
func main() {
var i int
// 0 1 1 2 3 5 8 13 21 34
for i = 0; i < 10; i++ {
fmt.Printf("%d\t", fibonacci(i))
}
}
求平方根原理:计算机通常使用循环来计算 x 的平方根。从某个猜测的值 z 开始,我们可以根据 z² 与 x 的近似度
来调整 z,产生一个更好的猜测:
z -= (z*z - x) / (2*z)
package main
import "fmt"
func sqrt(x float64, i float64) (float64, float64) {
remain := (i*i - x) / (2 * i)
i = i - remain
if remain > 0 {
return sqrt(x, i)
} else {
return i, remain
}
}
func get_sqrt(x float64) float64 {
i, _ := sqrt(x, x)
return i
}
func main() {
// 1.4142135623730951
fmt.Println(get_sqrt(2))
// 1.7320508075688774
fmt.Println(get_sqrt(3))
}
// 求平方根算法复杂度改成O(1)
package main
import "fmt"
import "unsafe"
func get_sqrt(x float32) float32 {
xhalf := 0.5 * x
var i int32 = *(*int32)(unsafe.Pointer(&x))
i = 0x5f375a86 - (i >> 1)
x = *(*float32)(unsafe.Pointer(&i))
x = x * (1.5 - xhalf*x*x)
x = x * (1.5 - xhalf*x*x)
x = x * (1.5 - xhalf*x*x)
return 1 / x
}
func main() {
// 2.0000002
fmt.Println(get_sqrt(4))
}