Go语言之函数,返回值,作用域,传参,匿名函数,高阶函数,闭包函数

本文详细介绍了Go语言中函数的声明、调用、参数传递、返回值,以及闭包的概念和应用。通过示例代码,阐述了函数如何接收和返回多个值、如何使用不定长参数、如何处理函数的局部作用域和变量生命周期。同时,重点讲解了闭包的特性,如数据隔离和计数器功能的实现,以及在装饰器模式中的应用。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

函数声明和调用

go语言是通过func关键字声明一个函数的,声明语法格式如下

func 函数名(形式参数) (返回值) {
        函数体
        return 返回值   // 函数终止语句
}

函数名:由字母、数字、下划线组成。但函数名的第一个字母不能是数字。在同一个包内,函数名也称不能重名(包的概念详见后文)。
形式参数:参数由参数变量和参数变量的类型组成,多个参数之间使用,分隔。
返回值:返回值由返回值变量和其变量类型组成,也可以只写返回值的类型,多个返回值必须用()包裹,并用,分隔。
函数体:实现指定功能的代码块。

func cal_sum100()  {
    // 计算1-100的和
    var s = 0
    for i := 1; i <= 100; i++ {
        s += i
    }
    fmt.Println(s)
}

声明一个函数并不会执行函数内代码,只是完成一个一个包裹的作用。真正运行函数内的代码还需要对声明的函数进行调用,一个函数可以在任意位置多次调用。调用一次,即执行一次该函数内的代码。

func cal_sum100()  {
    // 计算1-100的和
    var s = 0
    for i := 1; i <= 100; i++ {
        s += i
    }
    fmt.Println(s)
}
cal_sum100()  

函数参数

什么是参数,函数为什么需要参数呢?将上面的打印的两个菱形换乘打印一个6行的和一个8行的,如何实现呢?这就涉及到了函数参数。
再比如上面我们将计算1-100的和通过函数实现了,但是完成新的需求:
分别计算并在终端打印1-100的和,1-150的和以及1-200的和

package main

import "fmt"

func cal_sum100()  {
    // 计算1-100的和
    var s = 0
    for i := 1; i <= 100; i++ {
        s += i
    }
    fmt.Println(s)
}

func cal_sum150()  {
    // 计算1-100的和
    var s = 0
    for i := 1; i <= 150; i++ {
        s += i
    }
    fmt.Println(s)
}

func cal_sum200()  {
    // 计算1-100的和
    var s = 0
    for i := 1; i <= 200; i++ {
        s += i
    }
    fmt.Println(s)
}

func main() {
    cal_sum100()
    cal_sum150()
    cal_sum200()
}

这样当然可以实现,但是是不是依然有大量重复代码,一会发现三个函数出了一个变量值不同以外其他都是相同的,所以为了能够在函数调用的时候动态传入一些值给函数,就有了参数的概念。
参数从位置上区分分为形式参数和实际参数。

// 函数声明
func 函数名(形式参数1 参数1类型,形式参数2 参数2类型,...){
     函数体
}
// 调用函数
函数名(实际参数1,实际参数2,...)  

函数每次调用可以传入不同的实际参数,传参的过程其实就是变量赋值的过程,将实际参数按位置分别赋值给形参。
还是刚才的案例,用参数实现:

package main

import "fmt"

func cal_sum(n int)  {

    // 计算1-100的和
    var s = 0
    for i := 1; i <= n; i++ {
        s += i
    }
    fmt.Println(s)
}

func main() {
   cal_sum(100)
   cal_sum(101)
   cal_sum(200)
}

这样是不是就灵活很多了呢,所以基本上一个功能强大的函数都会有自己需要的参数,让整个业务实现更加灵活。

位置参数

位置参数,有时也称必备参数,指的是必须按照正确的顺序将实际参数传到函数中,换句话说,调用函数时传入实际参数的数量和位置都必须和定义函数时保持一致。

// 函数声明 两个形参:x,y
func add_cal(x int,y int){
    fmt.Println(x+y)
}

func main() {
    // 函数调用,按顺序传参
    // add_cal(2) // 报错
    // add_cal(232,123,12) // 报错
    add_cal(100,12)
}

不定长参数

如果想要一个函数能接收任意多个参数,或者这个函数的参数个数你无法确认,就可以使用不定长参数,也叫可变长参数。Go语言中的可变参数通过在参数名后加…来标识。

package main

import "fmt"

func sum(nums ...int) { //变参函数
    fmt.Println("nums",nums)
    total := 0
    for _, num := range nums {
        total += num
    }
    fmt.Println(total)
}

func main() {

    sum(12,23)
    sum(1,2,3,4)

}

注意:可变参数通常要作为函数的最后一个参数。

package main

import "fmt"

func sum(base int, nums ...int) int {
    fmt.Println(base, nums)
    sum := base
    for _, v := range nums {
        sum = sum + v
    }
    return sum
}

func main() {
    ret := sum(10,2,3,4)
    fmt.Println(ret)

}

go的函数强调显示表达的设计哲学,没有默认参数

函数返回值

函数的返回值是指函数被调用之后,执行函数体中的代码所得到的结果,这个结果通过 return 语句返回。return 语句将被调函数中的一个确定的值带回到主调函数中,供主调函数使用。函数的返回值类型是在定义函数时指定的。return 语句中表达式的类型应与定义函数时指定的返回值类型必须一致。

func 函数名(形参 形参类型)(返回值类型){
    //  函数体
    return 返回值
}

变量 = 函数(实参)    // return 返回的值赋值给某个变量,程序就可以使用这个返回值了。

同样是设计一个加法计算函数:

func add_cal(x,y int) int{
    return x+y
}

func main() {
    ret := add_cal(1,2)
    fmt.Println(ret)
}

无返回值,声明函数时没有定义返回值,函数调用的结果不能作为值使用

func foo(){
    fmt.Printf("hi,yuan!")
    return  // 不写return默认return空
}

func main() {
    // ret := foo() // 报错:无返回值不能将调用的结果作为值使用
    foo()
    
}

返回多个值,函数可以返回多个值

func get_name_age() (string, int) {
    return "yuan",18
}

func main() {
    a, b := get_name_age()
    fmt.Println(a, b)
}

返回值命名

函数定义时可以给返回值命名,并在函数体中直接使用这些变量,最后通过return关键字返回。

func calc(x, y int) (sum, sub int) {
    sum = x + y
    sub = x - y
    return     //  return sum sub
}

作用域

所谓变量作用域,即变量可以作用的范围。
作用域(scope)通常来说,程序中的标识符并不是在任何位置都是有效可用的,而限定这个标识符的可用性的范围就是这个名字的作用域。
变量根据所在位置的不同可以划分为全局变量和局部变量
局部变量 :写在{}中或者函数中或者函数的形参, 都是局部变量

1、局部变量的作用域是从定义的那一行开始, 直到遇到 } 结束或者遇到return为止
2、局部变量, 只有执行了才会分配存储空间, 只要离开作用域就会自动释放
3、局部变量存储在栈区
4、局部变量如果没有使用, 编译会报错。全局变量如果没有使用, 编译不会报错
5、:=只能用于局部变量, 不能用于全局变量

全局变量 :函数外面的就是全局变量

1、全局变量的作用域是从定义的那一行开始, 直到文件末尾为止
2、全局变量, 只要程序一启动就会分配存储空间, 只有程序关闭才会释放存储空间,
3、全局变量存储在静态区(数据区)

func foo()  {
    // var x =10
    x = 10
    fmt.Println(x)
}

var x = 30   // 全局变量

func main() {

    // var x = 20
    foo()
    fmt.Println(x)
}

注意,if,for语句具备独立开辟作用域的能力:

// if的局部空间
if true{
    x:=10
    fmt.Println(x)
}

fmt.Println(x)

// for的局部空间
for i:=0;i<10 ;i++  {

}
fmt.Println(i)

值传递

// 案例1
var x = 10
fmt.Printf("x的地址%p\n", &x)
y := x
fmt.Printf("y的地址%p\n", &y)
x = 100
fmt.Println(y)

// 案例2
var a = []int{1, 2, 3}
b := a
a[0] = 100
fmt.Println(b)

// 案例3
var m1 = map[string]string{"name":"yuan"}
var m2 = m1
m2["age"] = "22"
fmt.Println(m1)

函数传参

package main

import "fmt"

func swap(a int, b int) {
    c := a
    a = b
    b = c
    fmt.Println("a", a)
    fmt.Println("b", b)
}

func main() {
    var x = 10
    var y = 20
    swap(x, y)
    fmt.Println("x", x)
    fmt.Println("y", y)
}
package main

import "fmt"

func func01(x int) {
    x = 100
}

func func02(s []int) {
    fmt.Printf("func02的s的地址:%p\n",&s)
    s[0] = 100
    // s = append(s, 1000)
}

func func03(p *int)  {
    *p = 100
}
func main() {

    // 案例1
    var x = 10
    func01(x)
    fmt.Println(x)

    // 案例2
    var s = []int{1, 2, 3}
    fmt.Printf("main的s的地址:%p\n",&s)
    func02(s)
    fmt.Println(s)

    //案例3
    var a = 10
    var p *int = &a
    func03(p)
    fmt.Println(a)

}

思考之前的scan函数为什么一定传参&
Go语言中所有的传参都是值传递(传值),都是一个副本,一个拷贝。因为拷贝的内容有时候是非引用类型(int、string、struct等这些),这样就在函数中就无法修改原内容数据;有的是引用类型(指针、map、slice、chan等这些),这样就可以修改原内容数据。

func makemap(t *maptype, hint int64, h *hmap, bucket unsafe.Pointer) *hmap {
}

make函数创建的map就是一个指针类型,工作原理类似于案例3,所以map数据和切片数据一样虽然值拷贝但依然可以修改原值。

匿名函数

匿名函数,顾名思义,没有函数名的函数。
匿名函数的定义格式如下:

func(参数列表)(返回参数列表){
    函数体
}

匿名函数可以在使用函数的时候再声明调用

//(1)
(func() {
    fmt.Println("yuan")
})()
//(2)
var z =(func(x,y int) int {
        return x + y
    })(1,2)

fmt.Println(z)

也可以将匿名函数作为一个func类型数据赋值给变量

var f  = func() {
    fmt.Println("yuan")
}

fmt.Println(reflect.TypeOf(f))  // func

f() // 赋值调用调用

Go语言不支持在函数内部声明普通函数,只能声明匿名函数。

func foo()  {
    fmt.Println("foo功能")
    f := func(){
        fmt.Println("bar功能")
    }
    fmt.Println(f)
}

高阶函数

一个高阶函数应该具备下面至少一个特点:
将一个或者多个函数作为形参
返回一个函数作为其结果
首先明确一件事情:函数名亦是一个变量

package main

import (
    "fmt"
    "reflect"
)

func addCal(x int, y int)int{
    return x + y
}

func main() {

    var a = addCal
    a(2, 3)
    fmt.Println(a)
    fmt.Println(addCal)
    fmt.Println(reflect.TypeOf(addCal))  // func(int, int) int

}

结论:函数参数是一个变量,所以,函数名当然可以作为一个参数传入函数体,也可以作为一个返回值。

函数参数

package main

import (
    "fmt"
    "time"
)

func timer(f func()){
    timeBefore := time.Now().Unix()
    f()
    timeAfter := time.Now().Unix()
    fmt.Println("运行时间:", timeAfter - timeBefore)

}

func foo() {
    fmt.Println("foo function... start")
    time.Sleep(time.Second * 2)
    fmt.Println("foo function... end")
}

func bar() {
    fmt.Println("bar function... start")
    time.Sleep(time.Second * 3)
    fmt.Println("bar function... end")
}

func main() {

    timer(foo)
    timer(bar)
}

注意如果函数参数也有参数该怎么写呢?

package main

import "fmt"

func add(x,y int ) int {
    return x+y
}

func mul(x,y int ) int {
    return x*y
}

// 双值计算器
func cal(f func(x,y int) int,x,y int,) int{

    return f(x,y)

}

func main() {

    ret1 := cal(add,12,3,)
    fmt.Println(ret1)
    ret2 := cal(mul,12,3,)
    fmt.Println(ret2)

}

函数返回值

package main

import (
    "fmt"
)

func foo() func(){

    inner := func() {
        fmt.Printf("函数inner执行")
    }

    return inner
}

func main() {

    foo()()

}

闭包函数

闭包并不只是一个go中的概念,在函数式编程语言中应用较为广泛。

首先看一下维基上对闭包的解释:

在计算机科学中,闭包(英语:Closure),又称词法闭包(Lexical Closure)或函数闭包(function closures),是引用了自由变量(外部非全局)的函数。

简单来说就是一个函数定义中引用了函数外定义的变量,并且该函数可以在其定义环境外被执行。这样的一个函数我们称之为闭包函数。

闭包就是当函数可以记住并访问所在的词法作用域时,就产生了闭包,即使函数是在当前词法作用域之 外执行。
需要注意的是,自由变量不一定是在局部环境中定义的,也有可能是以参数的形式传进局部环境;另外在 Go 中,函数也可以作为参数传递,因此函数也可能是自由变量。
闭包中,自由变量的生命周期等同于闭包函数的生命周期,和局部环境的周期无关。
闭包在运行时可以有多个实例,不同的引用环境和相同的函数组合可以产生不同的实例。
实现一个计数器函数,不考虑闭包的情况下,最简单的方式就是声明全局变量:

package main

import "fmt"

var i = 0

func counter() {
    i++
    fmt.Println(i)
}

func main() {
    counter()
    counter()
    counter()
}

这种方法的一个缺点是全局变量容易被修改,安全性较差。闭包可以解决这个问题,从而实现数据隔离。

package main

import "fmt"

func getCounter() func() {   
    var i = 0
    return func() {
        i++
        fmt.Println(i)
    }

}

func main() {
    counter := getCounter()
    counter()
    counter()
    counter()

    counter2 := getCounter()
    counter2()
    counter2()
    counter2()
}

getCounter完成了对变量i以及counter函数的封装,然后重新赋值给counter变量,counter函数和上面案例的counter函数的区别就是将需要操作的自由变量转化为闭包环境。

闭包函数应用案例

在go语言中可以使用闭包函数来实现装饰器

案例1:计算函数调用次数


package main

import (
    "fmt"
    "reflect"
    "runtime"
)

// 函数计数器
func getCounter(f func()) func() {
    calledNum := 0 // 数据隔离
    return func() {
        f()
        calledNum += 1
        // 获取函数名
        fn := runtime.FuncForPC(reflect.ValueOf(f).Pointer()).Name()
        fmt.Printf("函数%s第%d次被调用\n", fn, calledNum)
    }
}

// 测试的调用函数
func foo() {
    fmt.Println("foo function执行")
}

func bar() {
    fmt.Println("bar function执行")
}

func main() {
    /*fooAndCounter := getCounter(foo)   // 针对foo的计数器
    fooAndCounter()
    fooAndCounter()
    fooAndCounter()

    barAndCounter := getCounter(bar)
    barAndCounter()
    barAndCounter()
    barAndCounter()*/

    foo := getCounter(foo) // 开放原则
    foo()
    foo()
    foo()

    bar := getCounter(bar)
    bar()
    bar()
}

案例2:计算函数运行时间

package main

import (
    "fmt"
    "time"
)

func GetTimer(f func(t time.Duration)) func(duration time.Duration) {

    return func(t time.Duration) {
        t1 := time.Now().Unix()
        f(t)
        t2 := time.Now().Unix()
        fmt.Println("运行时间:", t2-t1)
    }

}

func foo(t time.Duration) {
    fmt.Println("foo功能开始")
    time.Sleep(time.Second * t)
    fmt.Println("foo功能结束")
}

func bar(t time.Duration) {
    fmt.Println("bar功能开始")
    time.Sleep(time.Second * t)
    fmt.Println("bar功能结束")
}

func main() {

    var foo = GetTimer(foo)
    foo(3)
    var bar = GetTimer(bar)
    bar(2)

}

关键点:将一个功能函数作为自由变量与一个装饰函数封装成一个整体作为返回值,赋值给一个新的函数变量,这个新的函数变量在调用的时候,既可以完成原本的功能函数又可以实现装饰功能。

练习

package main
import "fmt"
func add(x int) int {
	x = 100
	return x
}
func main() {
	var x = 1
	x = add(x)
	fmt.Println(x) //输出:1
}
//==============================================================================
package main
import (
	"fmt"
	"reflect"
)
func setAge(age *int) { //声明一个整型指针类型
	fmt.Println(age)
	*age++ //赋值
}
func main() {
	var age = 20
	fmt.Println(&age, reflect.TypeOf(&age)) //&age是取址
	setAge(&age)
	fmt.Println(age) //输出结果是21
}
//==============================================================================
package main
import "fmt"
// 返回值命名
// 作用域
// 全局变量
var x = 100
//y := 100 ,全局变量不支持这样的简写形式
func main() {
	fmt.Println(x)
}
//==============================================================================
package main
import "fmt"
func 切片测试(s []int) {
	s[1] = 200
	//二倍扩容后将会生成新的数组
	s = append(s, 4)
	s[0] = 100
	fmt.Println(s) //[100 200 3 4]
}
func main() {
	//切片,底层有一个数组,
	var s = []int{1, 2, 3}
	切片测试(s)
	fmt.Println(s) //[1 200 3]
}
//==============================================================================
package main
import (
	"fmt"
	"time"
)
func 这是一个耗时的func() {
	time.Sleep(time.Second * 3)
	fmt.Println("该功能非常耗时,看看他运行了多久")
}
func1_函数可以作为形参(f func()) {
	fmt.Println(f)
	f()
}
func2_测算运行时间的方法(f func()) {
	// 时间戳
	start := time.Now().Unix()
	f()
	end := time.Now().Unix()
	fmt.Println("运行时间:", end-start)
}
func main() {
	//第1个_函数可以作为形参(这是一个耗时的func)2_测算运行时间的方法(这是一个耗时的func)
}
//==============================================================================
package main
import "fmt"
//高阶函数之函数作为返回值
func 声明一个匿名函数() func() {
	var inner = func() {
		fmt.Println("一个新的函数")
	}
	return inner
}
func main() {
	f := 声明一个匿名函数()
	f()
}
//==============================================================================
//编码?
//ASCII表
//字节是计算机的基本存储单位。
//一个字节(byte)包括8个比特位(bit),能够表示出256个二进制数字。
func byte字节类型() {
	var x uint8
	x = 25
	fmt.Println(x)
	//y := 'a' //双引号标识字符串,单引号标识字符
	var y byte
	y = 'a'
	fmt.Println(y, reflect.TypeOf(y))
	fmt.Printf("%c\n", y) //输出字符
	fmt.Printf("%d\n", y) //输出十进制
	fmt.Printf("%b\n", y) //输出二进制
	fmt.Printf("%x\n", y) //输出十六进制
}
//==============================================================================
package main
import "fmt"
func unicode与utf8的转换() {
	// rune类型
	var z rune
	z = '元'
	fmt.Println(z, string(z))
}
func unicode与utf8的转换1() {
	//unicode要占4个字节
	var a rune //四个字节
	a = '元'
	fmt.Printf("字符元unicode的10进制:%d\n", a)
	fmt.Printf("字符元unicode的16进制:%x\n", a)
	fmt.Printf("字符元unicode的2进制:%b\n", a)
}
func unicode与utf8的转换2() {
	var s = "苑abc"
	//汉字占3个字节
	fmt.Println(s, len(s)) //len字节
	for i := 0; i < len(s); i++ {
		fmt.Println(s[i])
	}
	//对字符串遍历用range方式,utf8可伸缩,他是针对unicode的编码方式
	for i, v := range s {
		fmt.Println(i, v)
	}
}
func main() {
	unicode与utf8的转换2()
	//go语⾔的string是⼀种数据类型,这个数据类型占⽤16字节空间,
	//前8字节是⼀个指针,指向字符串值的地址,后⼋个字节是⼀个整数,标识字 符串的长度;
}
//==============================================================================
func 字符串转字节串() {
	var msg = "abc苑"
	fmt.Println([]byte(msg)) //用于网络传输soket
	//==============================
	fmt.Println([]rune(msg))
	//求字符的长度
	fmt.Println(len([]rune(msg)))
}
func 字节串转字符串() {
	bytes := []byte{97, 98, 99, 232, 139, 145}
	fmt.Println(string(bytes))
}
//==============================================================================


闭包函数

package main

import (
	"fmt"
	"reflect"
)

func aoo1() {
	aoo := func() {
		return
	}
	fmt.Println(reflect.TypeOf(aoo))
}

// 为啥这种写法不好?
var x = 0
func 为啥存在闭包1() {
	x++
	fmt.Println(x)
}

// 闭包针对于作用域的,是引用了自由变量(外部非全局)的函数
// 使用场景:计数器
// 闭包的好处:功能和数据打包成一个整体使用!!
func 闭包2() func() {
	//返回谁就会保存谁,不会被GC垃圾回收器回收
	//i会随着返回的func一起消亡,所以起到计数器的效果,i不会被初始化,会累加
	var i = 0
	counter1 := func() {
		i++
		fmt.Println(i)
	}
	return counter1
}

func main() {
	闭包2()()
	闭包2()()
	闭包2()()
}

package main

import "fmt"

func 闭包案例1(start int) func() {
	//var i = 0
	var i = start
	cot := func() {
		i++
		fmt.Println(i)
	}
	return cot
}

func main() {
	f1 := 闭包案例1(10)
	f1()
	f1()

	f2 := 闭包案例1(5)
	f2()
	f2()
}

package main

import (
	"fmt"
	"reflect"
	"runtime"
)

//封闭原则,不要动原来的代码

func too() {
	fmt.Println("too功能")
}

func boo() {
	fmt.Println("boo功能")
}

// too函数每执行一次就累加1
func callNum(f func()) func() {
	f()
	var i = 0
	cot := func() {
		i++
		//获取函数的名字
		fname := runtime.FuncForPC(reflect.ValueOf(f).Pointer()).Name()
		fmt.Printf("%s函数被调用了第%d次\n", fname, i)
	}
	return cot
}

func main() {
	//闭包整合了函数功能和相关的自由变量
	boo := callNum(boo)
	boo()
	boo()

	too := callNum(too)
	too()
	too()
}

package main

import (
	"fmt"
	"time"
)

func fooo() {
	fmt.Println("fooo功能开始")
	time.Sleep(time.Second * 3)
	fmt.Println("fooo功能结束")
}

func booo() {
	fmt.Println("booo功能开始")
	time.Sleep(time.Second * 3)
	fmt.Println("booo功能结束")
}

// 用于计算方法的执行时间
func getTime(f func()) func() {
	return func() {
		start := time.Now().Unix()
		f()
		end := time.Now().Unix()
		fmt.Println("cost timer:", end-start)
	}
}

func main() {
	fooo := getTime(fooo)
	fooo()

	booo := getTime(booo)
	booo()
}

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值