Go
语言最少有个 main() 函数。函数声明告诉了编译器函数的名称,返回类型和参数。
func funcName(parameter_list)(result_list) {
function_body
}
函数定义解析:
func:定义函数关键字;
funcName:函数名遵循标识符的命名规则,首字母大写其它包可见,首字母小写只能本包可见;
parameter_list:参数列表,使用 () 包裹,参数就像一个占位符,当函数被调用时,你可以将值传递给参数,这个值被称为实际参数。参数列表指定的是参数类型、顺序、及参数个数。参数是可选的,也就是说函数也可以不包含参数;
result_list:返回类型,函数返回一列值。result_list 是该列值的数据类型。有些功能不需要返回值,这种情况下 result_list不是必须的;
function_body:函数体使用 {}
包裹, {
必须位于函数定义行的行尾;
func A() {
// do something
...
}
// or
func A() (int) {
// do something
...
return 1
}
func add(a int, b int) {
...
}
简写为:
func add(a, b int) {
...
}
以下两个声明是等价的:
func f(i, j, k int, s, t string) { /* ... */ }
func f(i int, j int, k int, s string, t string) { /* ... */ }
命名返回参数可看做与形参类似的局部变量,最后由 return 隐式返回。
// sum 相当于函数内的局部变量,被初始化为 0
func add(a, b int) (sum int) {
sum = a + b
return // return sum 的简写模式
// sum := a + b 则相当于新声明一个 sum 变量名,原有的 sum 变量被覆盖
// return sum 需要显式地调用 return sum
}
每一次函数调用都必须按照声明顺序为所有参数提供实参(参数值)。在函数调用时, Go 语言没有默认参数值,也没有任何方法可以通过参数名指定形参,因此形参和返回值的变量名对于函数调用者而言没有意义。
不支持函数重载;
函数作为值赋值给变量;
package main
import (
"fmt"
)
func fire() {
fmt.Println("fire")
}
func main() {
var f func()// 将变量 f 声明为 func() 类型,此时 f 就被俗称为“回调函数”,此时 f 的值为 nil。
f = fire // 将 fire() 函数作为值,赋给函数变量 f,此时 f 的值为 fire() 函数
f() // 使用函数变量 f 进行函数调用,实际调用的是 fire() 函数。
}
func add(a , b int) (sum int) {
anonymous:= func(x , y int) int {
return x + y
}
return anonymous(a , b)
}
package main
import "fmt"
func test(fn func() int) int {
return fn()
}
// FormatFunc 定义函数类型。
type FormatFunc func(s string, x, y int) string
func format(fn FormatFunc, s string, x, y int) string {
return fn(s, x, y)
}
func main() {
s1 := test(func() int { return 100 }) // 直接将匿名函数当参数。
s2 := format(func(s string, x, y int) string {
return fmt.Sprintf(s, x, y)
}, "%d, %d", 10, 20)
println(s1, s2)
}
package main
func add(x, y int) (z int) {
defer func() {
z += 100
}()
z = x + y
return
}
func main() {
println(add(1, 2)) // 输出: 103
}
package main
func add(x, y int) (z int) {
defer func() {
println(z) // 输出: 203
}()
z = x + y
return z + 200 // 执行顺序: (z = z + 200) -> (call defer) -> (ret)
}
func main() {
println(add(1, 2)) // 输出: 203
}
slice
、 map
、 function
、 channel
等类型,实参可能会由于函数的间接引用被修改。Go
支持多值返回,定义多值返回的返回参数列表时要使用 ()
包含,支持命名参数的返回。
如果返回值是同一种类型,则用括号将多个返回值类型括起来,用逗号分隔每个返回值的类型。使用 return
语句返回时,值列表的顺序需要与函数声明的返回值类型一致,示例代码如下:
func swap(a, b int) (int, int) {
return b, a
}
如果多值返回有错误类型,一般将错误类型作为最后一个返回值。
不能用容器对象接收多返回值。只能用多个变量或 “_” 忽略。
package main
func test() (int, int) {
return 1, 2
}
func main() {
// s := make([]int, 2)
// s = test() // Error: multiple-value test() in single-value context
x, _ := test()
println(x)
}
多返回值可直接作为其他函数调用实参。
package main
func test() (int, int) {
return 1, 2
}
func add(x, y int) int {
return x + y
}
func sum(n ...int) int { // 不定参数
var x int
for _, i := range n {
x += i
}
return x
}
func main() {
println(add(test()))
println(sum(test()))
}
Go
语言支持对返回值进行命名,这样返回值就和参数一样拥有参数变量名和类型。
命名的返回值变量的默认值为类型的默认值,即数值为 0,字符串为空字符串,布尔为 false、指针为 nil 等。
下面代码中的函数拥有两个整型返回值,函数声明时将返回值命名为 a 和 b,因此可以在函数体中直接对函数返回值进行赋值,在命名的返回值方式的函数体中,在函数结束前需要显式地使用 return 语句进行返回,代码如下:
package main
import (
"fmt"
)
func main() {
x, y := namedRetValues()
fmt.Println(x, y) // "5"
}
func namedRetValues() (a, b int) { // 对两个整型返回值进行命名,分别为 a 和 b
a = 1 // 命名返回值的变量与这个函数的局部变量的效果一致,可以对返回值进行赋值和值获取。
b = 2
// 当函数使用命名返回值时,可以在 return 中不填写返回值列表,如果填写也是可行的,
return // 等价 return a, b
}
Go
函数实参到形参的传递永远是值传递,除非参数传递的是指针值得拷贝,实参是一个指针变量,传递给形参的是这个指针变量的副本,二者指向同一地址, 本质上参数传递仍是值拷贝。
package main
import "fmt"
func main() {
a := 10
addOne(a)
fmt.Println("main a is ", a)
fmt.Println("main a address is ", &a)
addPointer(&a) // 实参给形参传递时仍然是值拷贝,传递的是 a 的地址
fmt.Println("main a is ", a)
}
func addOne(a int) int {
a = a + 1
fmt.Println("addOne a is ", a)
return a
}
func addPointer(a *int) {
fmt.Println("addPointer a address is ", a)
*a = *a + 1
fmt.Println("addPointer a address is ", a)
fmt.Println("addPointer a is ", *a)
return
}
输出结果:
addOne a is 11
main a is 10
main a address is 0xc000016068
addPointer a address is 0xc000016068
addPointer a address is 0xc000016068
addPointer a is 11
main a is 11
不定参数也叫作可变参数,是指函数传入的参数个数是可变的,Go
函数支持不定数目的形式参数,不定参数声明使用 param ...type
的语法格式。
为了做到这点,首先需要将函数定义为可以接受可变参数的类型:
``
package main
import (
"fmt"
)
// 函数 myfunc() 接受不定数量的参数,这些参数的类型全部是 int,
func myfunc(args ...int) {
for _, arg := range args {
fmt.Println(arg)
}
}
func main() {
myfunc(1, 2, 3)
myfunc(10, 20, 30)
}
从内部实现机理上来说,类型...type
本质上是一个数组切片,也就是[]type
,这也是为什么上面的参数 args 可以用 for 循环来获得每个传入的参数。
假如没有...type
这样的语法糖,开发者将不得不这么写:
func myfunc2(args []int) {
for _, arg := range args {
fmt.Println(arg)
}
}
从函数的实现角度来看,这没有任何影响,该怎么写就怎么写,但从调用方来说,情形则完全不同:
myfunc2([]int{1, 3, 7, 13})
大家会发现,我们不得不加上[]int{}
来构造一个数组切片实例,但是有了...type
这个语法糖,我们就不用自己来处理了。
函数的不定参数特点:
所有的不定参数类型必须是相同的;
不定参数必须是函数的最后一个参数;
不定参数在函数体内相当于切片,对切片的操作同样适合对不定参数的操作;
切片可以作为参数传递给不定参数,切片名后要加上 ...
;
形参为不定参数的函数和形参为切片的函数类型是不相同的;
可变参数变量是一个包含所有参数的切片,如果要将这个含有可变参数的变量传递给下一个可变参数函数,可以在传递时给可变参数变量后面添加...
,这样就可以将切片中的元素进行传递,而不是传递可变参数变量本身。
package main
import "fmt"
func main() {
n := []int{1, 2, 3, 4, 5}
result := sumOne(n...) // 切片元素作为参数传递给函数的不定参数,需要在切片名后加上 ...
fmt.Println("result is ", result)
ret := sumTwo(n) // 传递切片自身
fmt.Println("ret is ", ret)
fmt.Printf("sumOne type is %T\n", sumOne) // sumOne type is func(...int) int
fmt.Printf("sumTwo type is %T\n", sumTwo) // sumTwo type is func([]int) int
}
func sumOne(a ...int) (ret int) {
for _, v := range a { // 不定参数相当于切片,可以使用 range 访问
ret += v
}
return
}
func sumTwo(a []int) (ret int) {
for _, v := range a {
ret += v
}
return
}
用 interface{}
传递任意类型数据是 Go
语言的惯例用法,使用 interface{}
仍然是类型安全的,下面通过示例来了解一下如何分配传入 interface{}
类型的数据。
package main
import "fmt"
func MyPrintf(args ...interface{}) {
for _, arg := range args {
switch arg.(type) {
case int:
fmt.Println(arg, "is an int value.")
case string:
fmt.Println(arg, "is a string value.")
case int64:
fmt.Println(arg, "is an int64 value.")
default:
fmt.Println(arg, "is an unknown type.")
}
}
}
func main() {
var v1 int = 1
var v2 int64 = 234
var v3 string = "hello"
var v4 float32 = 1.234
MyPrintf(v1, v2, v3, v4)
}
输出结果:
1 is an int value.
234 is an int64 value.
hello is a string value.
1.234 is an unknown type.
函数在定义后,可以通过调用的方式,让当前代码跳转到被调用的函数中进行执行,调用前的函数局部变量都会被保存起来不会丢失,被调用的函数运行结束后,恢复到调用函数的下一行继续执行代码,之前的局部变量也能继续访问。
函数内的局部变量只能在函数体中使用,函数调用结束后,这些局部变量都会被释放并且失效。
Go语言的函数调用格式如下:
返回值变量列表 = 函数名(参数列表)
下面是对各个部分的说明:
实参通过值的方式传递,因此函数的形参是实参的拷贝。对形参进行修改不会影响实参。但是,如果实参包括引用类型,如指针, slice
(切片)、 map
、 function
、 channel
等类型,实参可能会由于函数的间接引用被修改。