前言
在Go语言的编程战场上,函数就是我们的LPL战队,每个函数都是一位冠军选手,拥有独特的技能和战术。就像T1在S14英雄联盟LPL中夺冠一样,go就是成为你赢得编程胜利的关键力量。
下面开始知识点讲解和案例学习,愿你超越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]
}
3.匿名函数
🤹 匿名函数的戏法:匿名函数就像舞台上的魔术师,它们可以突然出现,执行一个神奇的戏法,然后消失不见。它们可以被藏在变量的帽子里,或者在通道中被传递。Golang匿名函数可赋值给变量,做为结构字段,或者在 channel 里传送。
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。当一个错误发生时,你可以念出这个咒语,它就会告诉你错误的具体内容。
🔍 探索错误之书的秘法
- 断言探秘法:有时候,错误之书的某一页(比如
*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)
}
}
- 方法探秘法:有些错误页面(比如
*DNSError)不仅有Error() string咒语,还有其他特殊的咒语,比如Timeout() bool和Temporary() 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)
}
}
}
- 直接比较法:有时候,我们可以通过直接比较错误的内容来识别特定的错误模式。
// 案例:识别错误的模式
func recognizePatternError() {
files, err := filepath.Glob("[")
if err != nil && err == filepath.ErrBadPattern {
fmt.Println("模式错误:", err)
}
fmt.Println("匹配的文件:", files)
}
🎨 自定义error:绘制自己的错误之书
- New咒语:使用
errors.New咒语,你可以创建自己的错误页面。
// 案例:创建自定义错误
func createCustomError() {
customErr := errors.New("自定义错误:魔法能量不足")
fmt.Println(customErr)
}
- Errorf咒语:
fmt.Errorf咒语允许你在错误页面上添加更多的细节。
// 案例:添加错误细节
func addErrorDetails() {
err := fmt.Errorf("计算错误:%d + %d 超出预期", 5, 5)
fmt.Println(err)
}
- 结构体魔法:通过创建实现
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)
}
- 方法描述法:给你的错误页面添加更多的方法,以描述不同的错误。
// 案例:描述不同的错误
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语言中的错误处理有了更深的理解呢?现在,拿起你的魔杖(键盘),去编程的世界里施展这些强大的防护咒语吧!🧙♂️✨
小练习 :走迷宫游戏
游戏的目标是通过一个迷宫,玩家需要找到出口。迷宫将使用二维数组表示,其中 '#' 表示墙壁,. 表示通路,'S' 表示起点,'E' 表示终点。玩家可以通过输入 w、a、s、d 来控制角色在迷宫中的移动。
以下是游戏的代码实现:
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 函数来实现递归的移动方式。