函数定义与调用:函数的基本定义,参数传递,返回值。
golang 函数特点如下:
函数声明包含一个函数名,参数列表, 返回值列表和函数体。如果函数没有返回值,则返回列表可以省略。函数从第一条语句开始执行,直到执行return语句或者执行函数的最后一条语句。
使用关键字 func 定义函数,左大括号依旧不能另起一行。
有以下三种函数定义的情况:
// 无参数无返回值
func sayHello() {
fmt.Println("Hello, Go!")
}
// 带参数无返回值
func greet(name string) {
fmt.Printf("Hello, %s!\n", name)
}
// 带参数和返回值
func add(a, b int) int {
return a + b
}
// 函数调用示例
func main() {
sayHello() // Hello, Go!
greet("Alice") // Hello, Alice!
result := add(5, 7) // 12
}
函数是第一类对象,可作为参数传递。建议将复杂签名定义为函数类型,以便于阅读。
func test(fn func() int) int {
return fn()
}
// 定义函数类型。
type FormatFunc func(s string, x, y int) string
func format(fn FormatFunc, s string, x, y int) string {
return fn(s, x, y)
}
有返回值的函数,必须有明确的终止语句,否则会引发编译错误。
函数定义时指出,函数定义时有参数,该变量可称为函数的形参。形参就像定义在函数体内的局部变量。
但当调用函数,传递过来的变量就是函数的实参,函数可以通过两种方式来传递参数:
但是 Go 语言只有值传递,但通过指针可以实现类似引用传递的效果:
// 值传递(操作副本)
func doubleValue(n int) {
n *= 2
}
// 指针传递(操作原值)
func doublePointer(n *int) {
*n *= 2
}
func main() {
x := 5
doubleValue(x) // x 不变(仍为5)
doublePointer(&x) // x 变为10
}
注意:
Go 语言的特色功能 多返回值机制,特别适合错误处理
如下是 多返回值 常见的两个例子:
// 返回商和余数
func div(a, b int) (int, int) {
return a / b, a % b
}
// 返回值命名
func calc(a, b int) (sum int, diff int) {
sum = a + b
diff = a - b
return // 隐式返回命名返回值
}
func main() {
q, r := div(10, 3) // q=3, r=1
s, d := calc(8, 5) // s=13, d=3
x, _ := calc(1, 3)
}
上面我们用到了 带名字的返回值,这个又是什么呢?
Go 的返回值可被命名,它们会被视作定义在函数顶部的变量。
return
语句会直接返回已命名的返回值,也就是「裸」返回值。
注意:Golang返回值不能用容器对象接收多返回值。只能用多个变量,或 "_"
忽略。
错误处理模式 的应用,举个例子:
func readFile(filename string) ([]byte, error) {
data, err := os.ReadFile(filename)
if err != nil {
return nil, fmt.Errorf("读取文件失败: %w", err)
}
return data, nil
}
func main() {
content, err := readFile("config.yaml")
if err != nil {
log.Fatal(err)
}
fmt.Println(string(content))
}
定义:
举个最简单的例子:
func main() {
// 1. 最简单的匿名函数
func() {
fmt.Println("这是一个匿名函数")
}() // 立即执行 -- IIFE
// 2. 带参数的匿名函数
func(name string) {
fmt.Printf("Hello, %s!\n", name)
}("张三")
// 3. 带返回值的匿名函数 -- 赋值给变量后调用
result := func(a, b int) int {
return a + b
}(3, 5)
fmt.Printf("3 + 5 = %d\n", result)
}
同样,我们也可以把一个函数当做 变量/参数传递 一样的操作。
func main() {
// 1. 将匿名函数赋值给变量
add := func(a, b int) int {
return a + b
}
fmt.Printf("add(10, 20) = %d\n", add(10, 20))
// 2. 匿名函数作为参数传递
operate := func(fn func(int, int) int, x, y int) int {
return fn(x, y)
}
multiply := func(a, b int) int {
return a * b
}
result2 := operate(multiply, 4, 6)
fmt.Printf("operate(multiply, 4, 6) = %d\n", result2)
// 3. 作为结构体
fns := [](func(x int) int){
func(x int) int { return x + 1 },
func(x int) int { return x + 2 },
}
println(fns[0](100)) // 101
}
还可以作为回调函数(和上面匿名函数作为参数传递其实大体相似),如下:
func main(){
processNumbers([]int{1, 2, 3}, func(n int) {
fmt.Println(n * 2)
})
}
func processNumbers(nums []int, callback func(int)) {
for _, n := range nums {
callback(n)
}
}
闭包的应该都听过,但到底什么是闭包呢?
看着上面的描述,会发现闭包和匿名函数似乎有些像。可是可能还是有些云里雾里的。因为跳过闭包的创建过程直接理解闭包的定义是非常困难的。 目前在JavaScript、Go、PHP、Scala、Scheme、Common Lisp、Smalltalk、Groovy、Ruby、 Python、Lua、objective c、Swift 以及Java8以上等语言中都能找到对闭包不同程度的支持。通过支持闭包的语法可以发现一个特点,他们都有垃圾回收(GC)机制。
javascript应该是普及度比较高的编程语言了,通过这个来举例应该好理解写。看下面的代码,只要关注script里方法的定义和调用就可以了。
<!DOCTYPE html>
<html lang="zh">
<head>
<title></title>
</head>
<body>
</body>
</html>
<script src="http://ajax.googleapis.com/ajax/libs/jquery/1.2.6/jquery.min.js" type="text/javascript"></script>
<script>
function a(){
var i=0;
function b(){
console.log(++i);
document.write("<h1>"+i+"</h1>");
}
return b;
}
$(function(){
var c1=a();
c1();
c1();
c1();
//a(); //不会有信息输出
document.write("<h1>=============</h1>");
var c2=a();
c2();
c2();
});
</script>
这段代码有两个特点:
var c1=a()
后,变量c实际上是指向了函数b(),再执行函数 c1()
后就会显示i的值,第一次为1,第二次为2,第三次为3,以此类推。 其实,这段代码就创建了一个闭包。
c1
引用了函数a()内的函数b(),就是说:
a()
的内部函数 b()
被函数a()外的一个变量引用的时候,就创建了一个闭包。 在上面的例子中,由于闭包的存在使得函数a()返回后,a中的i始终存在,这样每次执行 c1()
,i都是自加1后的值。 从上面可以看出闭包的作用就是在 a()
执行完并返回后,闭包使得Javascript的垃圾回收机制GC不会收回a()所占用的资源,因为a()的内部函数b()的执行需要依赖 a()
中的变量i。
a()
返回的不是函数 b()
,情况就完全不同了。因为 a()
执行完后,b()没有被返回给 a()
的外界,只是被 a()
所引用,而此时 a()
也只会被 b()
引 用,因此函数a()和b()互相引用但又不被外界打扰(被外界引用),函数a和b就会被GC回收。所以直接调用 a()
;是页面并没有信息输出。
下面来说闭包的另一要素引用环境。c1()
跟c2()引用的是不同的环境,在调用i++时修改的不是同一个i,因此两次的输出都是1。函数a()每进入一次,就形成了一个新的环境,对应的闭包中,函数都是同一个函数,环境却是引用不同的环境。这和 c1()
和 c2()
的调用顺序都是无关的。
下面我来将之前的JavaScript的闭包例子用Go来实现,代码示例:
func counter() func() int {
count := 0
return func() int {
count++
return count
}
}
func main() {
// 1. 简单闭包示例 -- 变量捕获
x := 10
closure := func() {
fmt.Printf("闭包访问外部变量 x = %d\n", x) // 访问外部变量
}
closure()
// // 2. 闭包捕获变量 -- 匿名函数形式
// counter := func() func() int {
// count := 0 // 外部变量
// return func() int {
// count++ // 修改外部变量
// return count
// }
// }
// 创建两个独立的计数器
c1 := counter()
c2 := counter()
fmt.Println("计数器1:")
fmt.Println(c1()) // 1
fmt.Println(c1()) // 2
fmt.Println("计数器2:")
fmt.Println(c2()) // 1(独立作用域)
fmt.Println("再次调用计数器1:")
fmt.Println(c1()) // 4
}
可以发现,输出和之前的JavaScript的代码是一致的。具体的原因和上面的也是一样的,这说明Go语言是支持闭包的。
闭包复制的是原对象指针,这就很容易解释延迟引用现象。
func test() func() {
x := 100
fmt.Printf("x (%p) = %d\n", &x, x)
return func() {
fmt.Printf("x (%p) = %d\n", &x, x)
}
}
func main() {
f := test()
f()
}
// 输出
x (0xc42007c008) = 100
x (0xc42007c008) = 100
在汇编层 ,test 实际返回的是 FuncVal 对象,其中包含了匿名函数地址、闭包对象指针。当调 匿名函数时,只需以某个寄存器传递该对象即可。
FuncVal { func_address, closure_var_pointer ... }
① 外部引用函数参数局部变量
func add(base int) func(int) int {
return func(i int) int {
base += i
return base
}
}
func main() {
tmp1 := add(10)
fmt.Println(tmp1(1), tmp1(2))
// 此时tmp1和tmp2不是一个实体了
tmp2 := add(100)
fmt.Println(tmp2(1), tmp2(2))
}
② 变量共享
func main() {
count := 0
counter := func() func() {
return func() {
count++
fmt.Printf("share: %d\n", count)
return
}
}
c1 := counter()
c2 := counter()
c1()
c1()
c2()
}
③ 返回2个闭包
// 返回2个函数类型的返回值
func test(base int) (func(int) int, func(int) int) {
// 定义2个函数,并返回
// 相加
add := func(i int) int {
base += i
return base
}
// 相减
sub := func(i int) int {
base -= i
return base
}
// 返回
return add, sub
}
func main() {
f1, f2 := test(10)
// base一直是没有消
fmt.Println(f1(1), f2(2))
// 此时base是9
fmt.Println(f1(3), f2(4))
}
递归,就是在运行的过程中调用自己。 一个函数调用自己,就叫做递归函数。
构成递归需具备的条件:
场景一:数字阶乘
func factorial(i int) int {
if i <= 1 {
return 1
}
return i * factorial(i-1)
}
func main() {
var i int = 7
fmt.Printf("Factorial of %d is %d\n", i, factorial(i))
}
场景二:斐波那契数列
func fibonaci(i int) int {
if i == 0 {
return 0
}
if i == 1 {
return 1
}
return fibonaci(i-1) + fibonaci(i-2)
}
func main() {
var i int
for i = 0; i < 10; i++ {
fmt.Printf("%d ", fibonaci(i))
}
}
特点如下:
用途:关闭文件句柄、锁资源释放、数据库连接释放
举个最简单的例子,如下:
func main() {
defer fmt.Println("world")
fmt.Println("hello")
}
// 输出
hello
world
defer 栈:推迟调用的函数调用会被压入一个栈中。 当外层函数返回时,被推迟的调用会按照 后进先出 的顺序调用
func main() {
fmt.Println("counting")
for i := 0; i < 10; i++ {
defer fmt.Printf("%d ", i)
}
fmt.Println("done")
}
// 输出
counting
done
9 8 7 6 5 4 3 2 1 0
defer 闭包
func main() {
var whatever [5]struct{}
for i := range whatever {
defer func() { fmt.Print("%d ", i) }() // 4 3 2 1 0
}
}
但是在 之前版本的 Go 中,输出的 i 可能全是 4,因为 闭包用到的变量 i 在执行的时候已经变成4
defer f.Close
type Test struct {
name string
}
func (t *Test) Close() {
fmt.Println(t.name, " closed")
}
func main() {
ts := []Test{{"a"}, {"b"}, {"c"}}
for _, t := range ts {
// 在之前版本可能会出现调用的全是 c closed 的情况, 因为闭包执行的时候最后刚好是 c
// 之前的解决办法
// t2 := t
// defer t2.Close()
defer t.Close() // 现在不用直接这样写就行
}
}
不定参数传值 就是函数的参数不是固定的,后面的类型是固定的。(可变参数)
…
即可。func myfunc(args ...int) { //0个或多个参数
}
func add(a int, args…int) int { //1个或多个参数
}
func add(a int, b int, args…int) int { //2个或多个参数
}
注意:其中args是一个slice,我们可以通过 arg[index]
依次访问所有参数,通过 len(arg)
来判断传递参数的个数.
interface{}
传递任意类型数据是Go语言的惯例用法,而且 interface{}
是 类型安全 的。func myfunc(args ...interface{}) {
}
代码
func sum(numbers ...int) int {
total := 0
for _, num := range numbers {
total += num
}
return total
}
func main() {
fmt.Println(sum(1, 2, 3)) // 6
fmt.Println(sum([]int{4, 5, 6}...)) // 15 slice... 展开slice
}
注意:使用 slice 对象做变参时,必须展开。(slice...)
多返回值可直接作为其他函数调用实参。
type Operation func(int, int) int
func calculate(a, b int, op Operation) int {
return op(a, b)
}
func getOperator(op string) Operation {
switch op {
case "add":
return func(a, b int) int { return a + b }
case "multiply":
return func(a, b int) int { return a * b }
default:
return nil
}
}
func main() {
add := getOperator("add")
fmt.Println(calculate(3, 4, add)) // 7
}
命名返回参数
命名返回参数可看做与形参类似的局部变量,最后由 return 隐式返回
func add(x, y int) (z int) {
z = x + y
return
}
func main() {
println(add(1, 2)) // 3
}
注意:命名返回参数可被同名局部变量遮蔽,此时需要显式返回。如下:
func add(x, y int) (z int) {
{ // 不能在一个级别,引发 "z redeclared in this block" 错误。
var z = x + y
// return // Error: z is shadowed during return
return z // 必须显式返回。
}
}
同样,命名返回参数允许 defer 延迟调用通过闭包读取和修改
func add(x, y int) (z int) {
defer func() {
z += 100
}()
z = x + y
return
}
func main() {
println(add(1, 2))
}
注意:显式 return 返回前,会先修改命名返回参数
func add(x, y int) (z int) {
defer func() {
println(z) // 输出: 203
}()
z = x + y
return z + 200 // 执行顺序: (z = z + 200) -> (call defer) -> (return)
}
func main() {
println(add(1, 2)) // 输出: 203
}
var fn func()
func main() {
if fn == nil {
fmt.Println("函数未初始化")
}
fn() // panic: call of nil function
}