函数的作用
函数的分类
func 函数名(参数列表) [(返回值列表)]{
函数体 (代码块)
[return 返回值]
}
这里[]表示其中的内容可有可无
函数名就是标识符,命名要求一样
定义中的参数列表称为形式参数,只是一种符号表达(标识符),简称形参
返回值列表可有可无,需要return语句配合,表示一个功能函数执行完返回的结果
函数名(参数列表) [(返回值列表)] 这部分称为函数签名
Go语言中形参也被称为入参,返回值也被称为出参
函数定义,只是声明了一个函数,它不能被执行,需要调用执行
调用的方式,就是函数名后加上小括号,如有必要在括号内填写上参数
调用时写的参数是实际参数,是实实在在传入的值,简称实参,这个过程称为传实参,简称传参
如果定义了返回值列表,就需要配合使用return来返回这些值
package main
import "fmt"
// 函数定义
// x、y是形式参数,result是返回值
func add(x, y int) int {
result := x + y // 函数体
return result // 返回值
}
func main() {
out := add(4, 5) // 函数调用,可能有返回值,使用变量接收这个返回值
fmt.Println(out) // 对于Println函数来说,这也是调用,传入了实参out
out = add(10, 11) // 请问,这次函数调用和上次有没有关系?
fmt.Println(out) // 请问,函数定义了几次?调用了几次?可以调用几次?
}
9
21
上面代码解释:
定义一个函数add,函数名是add,能接受2个整型参数
该函数计算的结果,通过return语句返回"返回值"实现
调用时,通过函数名add后加2个参数,返回值可使用变量接收
函数名也是标识符
返回值也是值
一般习惯上函数定义需要在调用之前,也就是说调用时,函数已经被定义过了。请在书写代码时,也尽量这样做,便于阅读代码
特别注意,函数定义只是告诉你有一个函数可以用,但这不是函数调用执行其代码。至于函数什么时候被调用,不知道。一定要分清楚定义和调用的区别。
函数调用相当于运行一次函数定义好的代码,函数本来就是为了复用,试想你可以用加法函数,我也可以用加法函数,你加你的,我加我的,应该互不干扰的使用函数。为了实现这个目标,函数调用的一般实现,都是把函数压栈(LIFO),每一个函数调用都会在栈中分配专用的栈帧,局部变量、实参、返回值等数据都保存在这里。
package main
import "fmt"
func fn1() {}
func fn2(i int) int { return 100 }
func fn3(j int) (r int) { return 200 }
func main() {
fmt.Printf("%T\n", fn1)
fmt.Printf("%T\n", fn2)
fmt.Printf("%T\n", fn3)
}
输出如下
func()
func(int) int
func(int) int
可以看出同一种签名的函数是同一种类型
返回值变量是局部变量
1、无返回值函数
在Go语言中仅仅一个return并不一定表示无返回值,只能说在一个无返回值的函数中,return表示无返回值函数返回。
// 无返回值函数,可以不使用return,或在必要时使用return
func fn1() {
fmt.Println("无返回值函数")
return // return可有可无,如有需要,在必要的时候使用return来返回
}
t := fn1() // 错误,无返回值函数无返回值可用
fmt.Println(fn1()) // 错误,无返回值函数无返回值可打印
2、返回一个值
在函数体中,必须显式执行return
// 返回一个值,没有变量名只有类型。匿名返回值
func fn2() int {
a := 100
return a + 1 // return后面只要类型匹配就行
}
fmt.Println(fn2()) // 返回101
t := fn2() // 返回101
3、 返回多值
Go语言是运行函数返回多个值
package main
import "fmt"
// 返回多个值
func fn4() (int, bool) {
a, b := 100, true
return a, b
}
func main() {
fmt.Println(fn4())
}
100 true
package main
import "fmt"
// 返回多个值
func fn4() (i int, b bool) {
return
}
func main() {
fmt.Println(fn4())
}
0 false
这种写法对的 调用fn4函数时,也会被传入实参值。
返回值
可以无形参,也可以多个形参
不支持形式参数的默认值
形参是局部变量
func fn1() {} // 无形参
func fn2(int) {} // 有一个int形参,但是没法用它,不推荐
func fn3(x int) {} // 单参函数
func fn4(x int, y int) {} // 多参函数
func fn5(x, y int, z string) {} // 相邻形参类型相同,可以写到一起
fn1()
fn2(5)
fn3(10)
fn4(4, 5)
fn5(7, 8, "ok")
可变参数variadic。其他语言也有类似的被称为剩余参数,但Go语言有所不同
package main
import "fmt"
func fn6(nums ...int) { // 可变形参
fmt.Printf("%T %[1]v, %d, %d\n", nums, len(nums), cap(nums))
}
func main() {
fn6(1)
fn6(3, 5)
fn6(7, 8, 9)
}
[]int [1], 1, 1
[]int [3 5], 2, 2
[]int [7 8 9], 3, 3
可变参数收集实参到一个切片中
如果有可变参数,那它必须位于参数列表中最后。 func fn7(x, y int, nums …int, z string){} 这是错误的
unc fn7(x, y int, nums ...int) {
fmt.Printf("%d %d; %T %[3]v, %d, %d\n", x, y, nums, len(nums), cap(nums))
}
fn7(1, 2) // 1 2; []int [], 0, 0
fn7(1, 2, 3) // 1 2; []int [3], 1, 1
fn7(1, 2, 3, 4) // 1 2; []int [3 4], 2, 2
可以看出有剩下的实参才留给剩余参数。
func fn4(x int, y int) {} // 多参函数
p := []int{4, 5}
fn4(p...) // 错误,这在Go中不行,不能用在非可变参数non-variadic上
package main
import "fmt"
func fn6(nums ...int) { // 可变形参
fmt.Printf("%p, %p, %v\n", &nums, &nums[0], nums)
}
var p = []int{1, 3, 5}
func main() {
fmt.Printf("%p, %p, %v\n", &p, &p[0], p)
fn6(p...)
}
0xb411c0, 0xb30950, [1 3 5]
0xc000008090, 0xb30950, [1 3 5]
可以看到,这种方式并不是把p这个切片分解了,然后传递给fn6函数,在封装成一个新的切片nums。而是相当于切片header的复制。
重点: 切片… 只能为可变参数传参 也就是说 只能为(nums …int) 这种传参,不能为x.y传参
func fn7(x, y int, nums ...int) {
fmt.Printf("%d %d; %T %[3]v, %d, %d\n", x, y, nums, len(nums),cap(nums))
}
p := []int{4, 5}
fn7(p...) // 错误,不能用在普通参数上
fn7(1, p...) // 错误,不能用在普通参数上
fn7(1, 2, 3, p...) // 错误,不能用2种方式为可变参数传参,不能混用
// fn7(1, 2, p..., 9, 10) // 语法错误
// fn7(1, 2, []int{4, 5}..., []int{6, 7}...) // 语法错误,不能连续使用p...,只能一次
// 正确的如下
fn7(1, 2, []int{4, 5}...)
fn7(1, 2, p...)
fn7(1, 2, 3, 4, 5)
可以看出,可变参数限制较多
函数会开辟一个局部作用域,其中定义的标识符仅能在函数之中使用,也称为标识符在函数中的可见范围。
这种对标识符约束的可见范围,称为作用域。
if、for、switch等语句中使用短格式定义的变量,可以认为就是该语句块的变量,作用域仅在该语句块中。
s := []int{1, 3, 5}
for i, v := range s {
fmt.Println(i, v) // i和v在for块中可见
}
fmt.Println(i, v) // 错误,在for外不可见
if f, err := os.Open("o:/t.txt"); err != nil {
fmt.Println(f, err) // 可见
}
fmt.Println(f, err) // 错误,不可见
swith、select语句中的每个子句都被视为一个隐式的代码块。
func main() {
{
// 块作用域
const a = 100
var b = 200
c := 300
fmt.Println(a, b, c) // 可见
}
fmt.Println(a, b, c) // 错误,不可见
}
一个package包含该包所有源文件,形成的作用域。有时在包中顶层代码定义标识符,也称为全局标识符。
所有包内定义全局标识符,包内可见。包的顶层代码中标识符首字母大写则导出,从而包外可见,使用时也要加上包名。例如 fmt.Prinf() 。
函数声明的时候使用了花括号,所以整个函数体就是一个显式代码块。这个函数就是一个块作用域。
标识符作用域
标识符对外不可见,在标识符定义所在作用域外是看不到标识符的
使用标识符,自己这一层定义的标识符优先,如果没有,就向外层找同名标识符——自己优先,由近及远
标识符对内可见,在内部的局部作用域中,可以使用外部定义的标识符——向内穿透
包级标识符
标识符对外不可见,在标识符定义所在作用域外是看不到标识符的
使用标识符,自己这一层定义的标识符优先,如果没有,就向外层找同名标识符——自己优先,由近及远
标识符对内可见,在内部的局部作用域中,可以使用外部定义的标识符——向内穿透包级标识符
在所在包内,都可见
跨包访问,包级标识符必须大写开头,才能导出到包外,可以在包外使用 xx包名.VarName 方
式访问。例如 fmt.Print()