GO的函数方法你真的理解了吗 | 豆包MarsCode AI刷题

237 阅读11分钟

前言

在Go语言的编程战场上,函数就是我们的LPL战队,每个函数都是一位冠军选手,拥有独特的技能和战术。就像T1在S14英雄联盟LPL中夺冠一样,go就是成为你赢得编程胜利的关键力量。

image.png

下面开始知识点讲解和案例学习,愿你超越faker,成为go的峡谷真神。

定义函数语法:


func 函数名(参数列表) (返回值列表){
	//函数体
  
    return //返回值
}

//例
func test(x, y int, s string) (int, string) {
    // 类型相同的相邻参数,参数类型可合并。 多返回值必须用括号。
    n := x + y          
    return n, fmt.Sprintf(s, n)
}

1.声明函数

🎭 函数的魔法:在Go的王国里,有两种函数巫师:普通巫师和匿名巫师。普通巫师有名有姓,而匿名巫师则神秘莫测,随时准备执行任务。(函数可分为普通函数和匿名函数两种)。

package main

import (
    "fmt"
    "strconv"
)

// 普通函数,接收两个整数参数,返回它们的和与差
func calculate(num1, num2 int) (int, int) {
    return num1 + num2, num1 - num2
}

func anotherFunc() {
    // 调用普通函数并获取返回值
    sum, diff := calculate(10, 5)
    fmt.Println("两数之和:", sum)
    fmt.Println("两数之差:", diff)

    // 声明匿名函数并赋值给变量
    greetPerson := func(name string, age int) (string, int) {
        return name+",你好呀!", age
    }
    personName, personAge := greetPerson("小明", 25)
    fmt.Println(personName, personAge)

    // 函数作为参数和返回值的示例
    myFunction := func(f func(int, int) int) func(int, int) string {
        return func(a, b int) string {
            result := f(a, b)
            return "传入函数的计算结果是: " + strconv.Itoa(result)
        }
    }

    // 定义一个具体的函数作为参数传入
    multiply := func(x, y int) int {
        return x * y
    }

    finalResultFn := myFunction(multiply)
    fmt.Println(finalResultFn(3, 4))
}

func main() {
    anotherFunc()
}

2.参数

🔄 参数的舞蹈:当你召唤一个函数时,你得给它一些参数作为舞伴。这些舞伴可以是实实在在的值,也可以是地址,它们决定了舞蹈的风格——是优雅的值传递华尔兹,还是狂野的引用传递探戈。

函数定义时指出,函数定义时有参数,该变量可称为函数的形参。形参就像定义在函数体内的局部变量。但当调用函数,传递过来的变量就是函数的实参,函数可以通过值传递和引用传递两种方式来传递参数。
A   值传递:指在调用函数时将实际参数复制一份传递到函数中,这样在函数中如果对参数进行修改,将不会影响到实际参数,在go函数中默认是值传递。
B   引用传递:是指在调用函数时将实际参数的地址传递到函数中,那么在函数中对参数所进行的修改,将影响到实际参数。map、slice、chan、指针、interface默认以引用的方式传递。

无论是值传递,还是引用传递,传递给函数的都是变量的副本,不过,值传递是值的拷贝。引用传递是地址的拷贝,一般来说,地址拷贝更为高效。而值拷贝取决于拷贝的对象大小,对象越大,则性能越低。

值传递与引用传递的区别:
1.值传递:指在调用函数时将实际参数复制一份传递到函数中,这样在函数中如果对参数进行修改,将不会影响到实际参数,在go函数中默认是值传递。
2.引用传递:是指在调用函数时将实际参数的地址传递到函数中,那么在函数中对参数所进行的修改,将影响到实际参数。map、slice、chan、指针、interface 默认以引用的方式传递。
3.无论是值传递,还是引用传递,传递给函数的都是变量的副本,不过,值传递是值的拷贝.引用传递是地址的拷贝,一般来说,地址拷贝更为高效。而值拷贝取决于拷贝的对象大小,对象越大,则性能越低。
package main

import "fmt"
func test01(){
	x,y:=0,0
	swap01:= func(x int) { //值传递
		x=10
	}
	swap01(x)
	fmt.Println(x) // 0 值传递并不会影响到原对象

	swap02:= func(x *int) { //引用传递
		*x=10
	}
	swap02(&y)
	fmt.Println(y) // 10 引用传递会影响到原对象
}

func main(){
    test01()
}

🌟 可变参数的奥秘:想象一下,你有一个魔法袋,可以装下无限多的糖果(参数)。这就是Go中的可变参数,它们其实是切片的化身,你可以随意地取出糖果,也可以知道袋子里有多少糖果。

package main

import "fmt"

func func01(){
	// 函数可变参数,可变参数是一个slice且必须是最后一个
	variableParamsFn01:= func(args ...int) { //可变参数
		fmt.Println("可变参数的length:",len(args)) // 可变参数的length: 3
		for _, v := range args {
			fmt.Print(v," ") // 1 2 3
		}
	}
	variableParamsFn01(1,2,3)

	fmt.Println()

    variableParamsFn02:= func(name string,age int,args ...int) { //多个参数+可变参数
		fmt.Println("可变参数的length:",len(args)) // 可变参数的length: 2
		for _, v := range args {
			fmt.Print(v," ") // 10 20 
		}
	}
	variableParamsFn02("z乘风",18,10,20)
    
    // 注意:使用 slice 对象做变参时,必须展开(slice...)。
	variableParamsFn03:= func(s string,args ...int) string{
		x:=0
		for _,v := range args {
			x+=v
		}
		return fmt.Sprintf(s,x)
	}
	res:=variableParamsFn03("sum: %d",[]int{1,2,3}...) //使用...展开slice
	fmt.Println(res) // sum: 6
}

有时候我们并明确参数参数的类型,可以将参数设置为interface{},interface{}表示可以传递任意类型,而且interface{}是类型安全的,它很像Java中的Object,由于目前Go还不支持泛型特性,所以无法明确的约束参数的类型。

package main
import "fmt"
func main(){
	//interface{}表示任意类型,且类型安全,类似Java的Object
	dream:= func(s interface{}) {
		fmt.Println(s)
	}
	dream(1) // 1
	dream("张三") // 张三
	dream([]int{1,2,3}) // [1 2 3]
}

image.png

3.匿名函数

🤹 匿名函数的戏法:匿名函数就像舞台上的魔术师,它们可以突然出现,执行一个神奇的戏法,然后消失不见。它们可以被藏在变量的帽子里,或者在通道中被传递。Golang匿名函数可赋值给变量,做为结构字段,或者在 channel 里传送。

image.png

4.闭包与递归

🔗 闭包的纽带:闭包是函数和它们环境的紧密结合,就像一对舞伴,即使音乐停止,它们也记得彼此的舞步。

其实很多函数式语言都提供了闭包特性,例如js、java8、go等等,闭包是由函数及其相关引用环境组合而成的实体(即:闭包=函数+引用环境)。

"官方"的解释是"所谓"闭包",指的是一个拥有许多变量和绑定了这些变量的环境的表达式(通常是一个函数),因而这些变量也是该表达式的一部分。

5.延迟调用(defer)

🛡 defer的守护咒语:defer是Go中的守护咒语,它会在函数即将结束时施展。它可以保护你的资源,就像一个守护者,确保所有的清理工作都得到妥善处理。

defer语句的作用是:含有 defer 语句的函数,会在该函数将要return之前,调用另一个函数。

defer的特点:

(1).关键字 defer 用于注册延迟调用,这些调用将在return前被执行,因此,可以用来做资源清理。
(2).多个defer语句,按先进后出的方式执行。
(3).defer语句中的变量,在defer声明时就决定了。
(4).注意不要在循环中使用defer,这样可能会导致内存泄漏和性能问题。

defer的使用场景:

(1).关闭文件句柄。
(2).锁资源释放。
(3).数据库连接释。
(4).延迟调用。

验证defer语句是先进后出:

func main(){
	/* 验证defer先进后出的例子 */
	var whatever [5]struct{}
	for i := 0; i < len(whatever) ; i++ {
		//在循环中使用defer可能会导致内存泄漏
		defer fmt.Print(i," ") // 4 3 2 1 0   因为defer遵循先进后出
	}
}

defer与闭包的结合:

func main(){
	var whatever01 [5]struct{}
	for i := 0; i < len(whatever01); i++ {
		defer func() {fmt.Print(i," ")}() // 5 5 5 5 5 函数正常执行,由于闭包用到的变量i在执行的时候已经变成4,所以输出全都是4
	}
}

6.错误处理

🧙‍♂️ 错误处理:编程魔法中的防护咒语

在编程的奇幻旅程中,我们总会遇到一些不期而至的“怪兽”——错误。在Go语言的魔法世界里,我们有一套强大的防护咒语来应对这些挑战。让我们一起来看看这些神奇的咒语是如何施展的吧!

📜 error:错误之书

在Go的世界里,所有的错误都被记录在一本名为error的魔法书中。这本书只包含一个强大的咒语——Error() string。当一个错误发生时,你可以念出这个咒语,它就会告诉你错误的具体内容。

🔍 探索错误之书的秘法

  1. 断言探秘法:有时候,错误之书的某一页(比如*PathError)不仅仅告诉你错误的内容,还记录了错误的来源和类型。通过使用断言探秘法,我们可以查看这一页的更多细节。
// 案例:探索文件打开错误
func explorePathError() {
    _, err := os.Open("神秘文件.txt")
    if pathErr, ok := err.(*os.PathError); ok {
        fmt.Println("操作类型:", pathErr.Op)
        fmt.Println("文件路径:", pathErr.Path)
        fmt.Println("错误详情:", pathErr.Err)
    }
}
  1. 方法探秘法:有些错误页面(比如*DNSError)不仅有Error() string咒语,还有其他特殊的咒语,比如Timeout() boolTemporary() bool,这些咒语可以告诉我们错误的更多特性。
// 案例:探索DNS查询错误
func exploreDNSError() {
    _, err := net.LookupHost("魔法森林.com")
    if dnsErr, ok := err.(*net.DNSError); ok {
        if dnsErr.Timeout() {
            fmt.Println("操作超时,再试一次吧!")
        } else if dnsErr.Temporary() {
            fmt.Println("临时错误,可能是魔法波动造成的。")
        } else {
            fmt.Println("通用错误:", dnsErr)
        }
    }
}
  1. 直接比较法:有时候,我们可以通过直接比较错误的内容来识别特定的错误模式。
// 案例:识别错误的模式
func recognizePatternError() {
    files, err := filepath.Glob("[")
    if err != nil && err == filepath.ErrBadPattern {
        fmt.Println("模式错误:", err)
    }
    fmt.Println("匹配的文件:", files)
}

🎨 自定义error:绘制自己的错误之书

  1. New咒语:使用errors.New咒语,你可以创建自己的错误页面。
// 案例:创建自定义错误
func createCustomError() {
    customErr := errors.New("自定义错误:魔法能量不足")
    fmt.Println(customErr)
}
  1. Errorf咒语fmt.Errorf咒语允许你在错误页面上添加更多的细节。
// 案例:添加错误细节
func addErrorDetails() {
    err := fmt.Errorf("计算错误:%d + %d 超出预期", 5, 5)
    fmt.Println(err)
}
  1. 结构体魔法:通过创建实现error接口的结构体,你可以绘制一个包含多个咒语的错误页面。
// 案例:使用结构体绘制错误页面
type CustomError struct {
    Message string
    Code    int
}

func (e *CustomError) Error() string {
    return fmt.Sprintf("错误代码 %d: %s", e.Code, e.Message)
}

func drawErrorPage() {
    err := &CustomError{"魔法失败", 404}
    fmt.Println(err)
}
  1. 方法描述法:给你的错误页面添加更多的方法,以描述不同的错误。
// 案例:描述不同的错误
type AreaError struct {
    Length, Width float64
}

func (e *AreaError) Error() string {
    return "面积计算错误"
}

func (e *AreaError) InvalidDimensions() bool {
    return e.Length <= 0 || e.Width <= 0
}

func describeError() {
    err := &AreaError{-1, -1}
    if err.InvalidDimensions() {
        fmt.Println("无效的尺寸:", err)
    }
}

通过这些风趣的案例和咒语,你是不是对Go语言中的错误处理有了更深的理解呢?现在,拿起你的魔杖(键盘),去编程的世界里施展这些强大的防护咒语吧!🧙‍♂️✨

小练习 :走迷宫游戏

image.png

游戏的目标是通过一个迷宫,玩家需要找到出口。迷宫将使用二维数组表示,其中 '#' 表示墙壁,. 表示通路,'S' 表示起点,'E' 表示终点。玩家可以通过输入 wasd 来控制角色在迷宫中的移动。

以下是游戏的代码实现:

package main

import (
    "fmt"
    "math/rand"
    "time"
)

const (
    mazeSize = 10 // 迷宫大小
)

var (
    maze [mazeSize][mazeSize]byte // 迷宫地图
    playerX, playerY int           // 玩家当前位置
)

// 初始化迷宫
func initMaze() {
    rand.Seed(time.Now().UnixNano())

    // 初始化迷宫为全墙
    for i := 0; i < mazeSize; i++ {
        for j := 0; j < mazeSize; j++ {
            maze[i][j] = '#'
        }
    }

    // 随机生成起点和终点
    startX, startY := rand.Intn(mazeSize), rand.Intn(mazeSize)
    endX, endY := rand.Intn(mazeSize), rand.Intn(mazeSize)
    maze[startX][startY] = 'S'
    maze[endX][endY] = 'E'

    // 生成一些通路
    for i := 0; i < mazeSize*2; i++ {
        x, y := rand.Intn(mazeSize), rand.Intn(mazeSize)
        maze[x][y] = '.'
    }

    // 初始化玩家位置为起点
    playerX, playerY = startX, startY
}

// 打印迷宫
func printMaze() {
    for i := 0; i < mazeSize; i++ {
        for j := 0; j < mazeSize; j++ {
            if i == playerX && j == playerY {
                fmt.Print("P") // 玩家位置
            } else {
                fmt.Print(string(maze[i][j]))
            }
        }
        fmt.Println()
    }
}

// 移动玩家
func movePlayer(dx, dy int) {
    newX, newY := playerX+dx, playerY+dy
    if newX >= 0 && newX < mazeSize && newY >= 0 && newY < mazeSize && maze[newX][newY]!= '#' {
        playerX, playerY = newX, newY
    }
}

// 游戏主循环
func gameLoop() {
    for {
        printMaze()
        fmt.Print("请输入移动方向(w:上,a:左,s:下,d:右):")

        var input string
        fmt.Scanln(&input)

        switch input {
        case "w":
            movePlayer(-1, 0)
        case "a":
            movePlayer(0, -1)
        case "s":
            movePlayer(1, 0)
        case "d":
            movePlayer(0, 1)
        default:
            fmt.Println("无效输入,请重新输入。")
            continue
        }

        if playerX == mazeSize-1 && playerY == mazeSize-1 {
            fmt.Println("恭喜你,你找到了出口!")
            break
        }
    }
}

func main() {
    initMaze()
    gameLoop()
}

在这个游戏中,我们使用了函数来初始化迷宫、打印迷宫、移动玩家和运行游戏主循环。这些函数通过参数传递和返回值来实现不同的功能。同时,游戏主循环中使用了一个无限循环和 switch 语句来处理玩家的输入,这体现了函数调用和控制流的概念。此外,我们还使用了闭包来确保玩家的位置在每次移动后得到更新,并且递归并没有在这个游戏中直接使用,但可以通过修改 movePlayer 函数来实现递归的移动方式。

最后 你学废了吗?

image.png