GO语言基础教程(72)Go函数的参数之形式参数和实际参数:函数参数の“纸片人”vs“实干家”:Go语言参数传递の灵魂拷问

第一章:参数界的“薛定谔的猫”——当我们在谈论参数时到底在说什么?

还记得第一次点外卖的迷惑行为吗?看着菜单图片上淋着芝士的汉堡(形参),实际收到的是被压扁的三明治(实参)。Go语言函数的参数传递,每天都在上演这样的“卖家秀vs买家秀”戏码。

形式参数(Formal Parameter)就像你写在求职简历上的“精通Python+Go+分布式系统”——是函数定义时吹出去的牛逼。而实际参数(Actual Parameter)就像入职后第一天被要求同时修复三个线上bug时——你真实的技术水平。

举个栗子🌰

// 简历上写的(形参)
func hireMe(skill string) {
    fmt.Printf("我能搞定%s\n", skill)
}

// 实际工作时的(实参)
func main() {
    realSkill := "同时写文档、改bug、应付产品经理" 
    hireMe(realSkill) // 传入真实的你
}

输出结果:

我能搞定同时写文档、改bug、应付产品经理

看到没?形参skill是函数hireMe对外宣称的能力,实参realSkill才是你实际要面对的工作内容。这就是为什么面试造火箭,入职拧螺丝——形参和实参从来都不是同一个世界的存在!

第二章:值传递——函数的“柏拉图式恋爱”

值传递就像网恋奔现:你把精心P过的照片(实参)发给对方,函数拿着你的照片(形参)疯狂心动,但永远触碰不到真实的你。

灵魂画手示意图:

你的美颜自拍 --复制一份--> 函数收到的电子照片
(实参在内存地址0x123) (形参在内存地址0x456)

代码还原奔现现场:

package main

import "fmt"

// 网恋对象以为的你(形参)
func blindDate(photo string) {
    photo = "卸妆后的样子" // 试图修改照片
    fmt.Printf("约会对象看到的是:%s\n", photo)
}

func main() {
    myPhoto := "精修磨皮大眼照片" // 真实的你(实参)
    blindDate(myPhoto)
    fmt.Printf("但实际你的照片还是:%s\n", myPhoto) // 原图未受影响
}

运行结果:

约会对象看到的是:卸妆后的样子
但实际你的照片还是:精修磨皮大眼照片

为什么myPhoto没有被修改?
因为Go在值传递时,实参myPhoto把自己的副本(拷贝)传给了形参photo,两个变量住在不同的内存地址,形参再怎么折腾,也影响不到实参本尊。

更扎心的例子——修改数值:

func addSalary(money int) {
    money += 10000  // 幻想老板给你加薪
    fmt.Printf("梦中工资:%d\n", money)
}

func main() {
    actualSalary := 5000 // 现实工资
    addSalary(actualSalary)
    fmt.Printf("实际到账:%d\n", actualSalary) // 残酷的现实
}

输出:

梦中工资:15000
实际到账:5000

看吧,在值传递的世界里,所有的改变都只是你的一厢情愿!

第三章:引用传递——和函数“穿同一条裤子”

如果说值传递是网恋,那引用传递就是同居——你们共用同一个空间,你乱扔的袜子(修改数据)对方立马就能看到。

引用传递的经典CP:切片(slice)和映射(map)

package main

import "fmt"

// 和你共用同一个衣柜(形参接收引用)
func messUpCloset(closet []string) {
    closet[0] = "皱巴巴的衬衫" // 直接修改共享空间里的衣服
    fmt.Printf("同居对象把衣服弄成了:%s\n", closet[0])
}

func main() {
    myCloset := []string{"崭新的白衬衫", "笔挺的西裤"} // 实参切片
    fmt.Printf("出门前衣柜里的:%s\n", myCloset[0])
    
    messUpCloset(myCloset) // 传入的是引用,不是副本!
    
    fmt.Printf("回家后衣柜里的:%s\n", myCloset[0]) // 你的衣服真的被弄皱了
}

运行结果:

出门前衣柜里的:崭新的白衬衫
同居对象把衣服弄成了:皱巴巴的衬衫  
回家后衣柜里的:皱巴巴的衬衫

为什么切片被修改了?
因为切片在Go中是基于数组的引用类型,传递切片时传递的是底层数组的引用(指针),形参和实参指向同一个内存地址。

指针传递——明明白白搞关系

如果你想要基本类型(如int、string)也能被函数修改,那就得明确地使用指针:

// 明确表示要共享银行账户(指针形参)
func sharedAccount(balance *int) {
    *balance -= 1000 // 直接动你账户里的钱
    fmt.Printf对象取走了1000,余额:%d\n", *balance)
}

func main() {
    myBalance := 5000 // 实参
    sharedAccount(&myBalance) // 把账户访问权交给对方
    fmt.Printf("查账发现余额真的变了:%d\n", myBalance)
}

输出:

对象取走了1000,余额:4000
查账发现余额真的变了:4000

重点总结传递方式:

传递类型

类似关系

是否影响实参

适用场景

值传递

网恋

不需要修改原数据

引用传递

同居

切片、map等引用类型

指针传递

联名账户

需要修改基本类型

第四章:变长参数——函数的“自助餐模式”

有时候你也不知道要传多少个参数,就像吃自助餐——拿盘子的时侯豪情万丈,实际能吃下的就那么几口。

变长参数语法:...类型

package main

import "fmt"

// 自助餐取餐区(变长形参)
func buffet(foods ...string) {
    fmt.Printf("你拿了%d种食物:", len(foods))
    for _, food := range foods {
        fmt.Printf("%s ", food)
    }
    fmt.Println()
}

func main() {
    // 场景1:正常点餐
    buffet("寿司", "牛排", "蛋糕")
    
    // 场景2:胃口不好只拿一样
    buffet("沙拉")
    
    // 场景3:大胃王模式
    manyFoods := []string{"龙虾", "鹅肝", "烤肉", "冰淇淋", "水果"}
    buffet(manyFoods...) // 切片展开语法
}

输出:

你拿了3种食物:寿司 牛排 蛋糕 
你拿了1种食物:沙拉 
你拿了5种食物:龙虾 鹅肝 烤肉 冰淇淋 水果 

变长参数的底层原理:
Go编译器会把变长参数转换为切片,所以foods ...string实际上就是foods []string

实用技巧——混合固定参数和变长参数:

// 固定套餐 + 自助加料
func comboMeal(mainCourse string, extras ...string) {
    fmt.Printf("主菜:%s,加料:", mainCourse)
    for _, extra := range extras {
        fmt.Printf("%s ", extra)
    }
    fmt.Println()
}

func main() {
    comboMeal("牛肉面", "鸡蛋", "豆干", "青菜")
    comboMeal("炒饭") // 可以不加料
}
第五章:实战演练——从外卖系统看参数传递

让我们用一个完整的外卖系统例子,把今天学的所有知识点串起来:

package main

import "fmt"

// 值传递:用户评价(不影响原评分)
func addReview(score int, comment string) {
    score += 1 // 平台默认加1分福利
    fmt.Printf("显示评分:%d分,评价:%s\n", score, comment)
}

// 引用传递:修改订单状态(需要影响原订单)
func updateOrderStatus(order map[string]string) {
    order["status"] = "已完成"
    fmt.Printf("订单状态更新为:%s\n", order["status"])
}

// 指针传递:更新用户余额(需要修改原值)
func deductBalance(balance *float64, amount float64) {
    if *balance >= amount {
        *balance -= amount
        fmt.Printf("扣款成功,余额:%.2f\n", *balance)
    } else {
        fmt.Printf("余额不足,当前余额:%.2f\n", *balance)
    }
}

// 变长参数:计算总价
func calculateTotal(prices ...float64) {
    total := 0.0
    for _, price := range prices {
        total += price
    }
    fmt.Printf("共计%d件商品,总价:%.2f元\n", len(prices), total)
}

func main() {
    fmt.Println("=== 外卖系统启动 ===")
    
    // 场景1:用户评价(值传递)
    originalScore := 4
    addReview(originalScore, "味道不错,配送很快")
    fmt.Printf("实际评分还是:%d分\n", originalScore) // 未改变
    
    // 场景2:更新订单(引用传递)
    order := map[string]string{
        "id": "ORDER123", 
        "status": "配送中",
        "rider": "张三",
    }
    fmt.Printf("原订单状态:%s\n", order["status"])
    updateOrderStatus(order)
    fmt.Printf("现订单状态:%s\n", order["status"]) // 已改变
    
    // 场景3:支付扣款(指针传递)
    userBalance := 100.0
    deductBalance(&userBalance, 68.5)
    fmt.Printf("确认余额:%.2f元\n", userBalance) // 已改变
    
    // 场景4:计算总价(变长参数)
    calculateTotal(25.5, 18.0, 12.8, 8.0)
    
    // 场景5:从切片计算总价
    itemPrices := []float64{32.0, 15.5, 9.9}
    calculateTotal(itemPrices...)
}

运行这个完整示例,你会看到:

=== 外卖系统启动 ===
显示评分:5分,评价:味道不错,配送很快
实际评分还是:4分
原订单状态:配送中
订单状态更新为:已完成
现订单状态:已完成
扣款成功,余额:31.50元
确认余额:31.50元
共计4件商品,总价:64.30元
共计3件商品,总价:57.40元
第六章:避坑指南——参数传递的那些“坑”
  1. 切片的部分引用陷阱
func modifySlice(s []int) {
    s[0] = 999 // 这能修改原切片
    s = append(s, 666) // 但这可能创建新切片!
}
  1. map的安全并发访问
// 错误做法:多个goroutine同时修改map
// 正确做法:用sync.Mutex或使用并发安全map
  1. 接口值的传递机制
var writer io.Writer = &Buffer{}
// 接口值实际上是一个(类型, 值)对
// 传递接口值时传递的是这个对的副本
总结:参数传递的本质

记住这个万能公式:

  • 值传递 = 给对方你的身份证复印件
  • 引用传递 = 给对方你家钥匙
  • 指针传递 = 明确告诉对方你的银行密码

在Go的世界里,理解参数传递就是理解数据的所有权和生命周期。形参是函数承诺要处理的数据格式,实参是真实世界扔给函数的数据本身。

下次写函数时,不妨问问自己:这个参数应该是网恋对象(值传递)、同居伴侣(引用传递)还是联名账户(指针传递)?想清楚这个,你的代码bug率至少降低50%!

评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

值引力

持续创作,多谢支持!

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值