Go function - 有关function我能告诉你的一切
官方参考:
- Go Spec
- Built-in functions
- Method_declarations
写 Go 的时候,函数是最先见到、也最容易被低估的角色。
它表面上只是func关键字后面跟一段代码,但真正写起来你会发现:Go 把函数设计得非常“务实”。它可以有多个返回值,可以被当作值传来传去,可以临时组装成闭包,还能摇身一变成为某个类型的方法。下面就来一起了解function有哪些基础的使用方法以及技巧
1. Function 基础
Go 里函数用func关键字定义:
func函数名(参数列表)返回值类型{// 函数体}例如:
funcadd(aint,bint)int{returna+b}// 可以将同类型参数合并声明funcadd(a,bint)int{returna+b}1.1 多返回值
多返回值是 Go 很有辨识度的特性。
在很多语言里,一个函数想返回两个结果,往往要塞进对象、数组、元组,或者靠异常去表达错误。Go 更直接:既然函数可能天然产出多个东西,那就把它们摊开放在返回值里。
funcdivide(a,bint)(int,int){returna/b,a%b}funcmain(){q,r:=divide(10,3)q2,_:=divide(10,3)// 也可以忽略某个返回值:}1.2 命名返回值(Go独有)
返回值可以提前命名。
这有点像在函数出口提前摆好几个盒子,函数体里只负责往盒子里放东西,最后一句return就能把盒子一起端出去:
funcrectangle(w,hint)(areaint){area=w*hreturn}funcrectangle(w,hint)int{// 等价area:=w*hreturnarea}多返回值也可以命名:
funccalc(a,bint)(sumint,diffint){sum=a+b diff=a-breturn}不过实际开发里不要滥用裸return,函数长了会降低可读性。
此外再遇上defer+闭包的情况,可能会产生一些意想不到的输出结果
functest()(resultint){deferfunc(){result++}()return0}很多人第一反应是0,但它的实际输出是1
官方文档这样解释:
For instance, if the deferred function is a function literal and the surrounding function has named result parameters that are in scope within the literal, the deferred function may access and modify the result parameters before they are returned. If the deferred function has any return values, they are discarded when the function completes. (See also the section on handling panics.)
例如,如果延迟函数是一个函数字面量 ,并且其外层函数具有命名的结果参数 ,且这些参数的作用域位于该字面量内部,则延迟函数可以在结果参数返回之前访问并修改它们。如果延迟函数有任何返回值,则这些返回值会在函数执行完毕时被丢弃。(另请参阅关于处理 panic 的部分。)
2. Function 核心用法
2.1 main 与 init 函数
Go 程序启动时,并不是一上来就冲进main。
它会先把包依赖、全局变量、init函数这些“开场准备”处理完,再把舞台交给main。
funcmain(){fmt.Println("Hello Go")}funcinit(){fmt.Println("init")}程序运行时会先初始化包依赖,执行所有init,最后执行main。
| 对比点 | init | main |
|---|---|---|
| 是否自动执行 | 是 | 是 |
| 是否建议手动调用 | 否 | 否 |
| 数量 | 多个 | 只能一个 |
| 执行时机 | 程序启动前 | 最后执行 |
| 所在包 | 任意包 | 只能 main 包 |
| 用途 | 初始化 | 程序入口 |
2.2 立即执行函数
对于写过 Js,对这一套应该很眼熟。函数刚定义完,后面立刻跟一对括号执行。
func(){fmt.Println("run immediately")}()func(namestring){fmt.Println("Hello",name)}("XiaoYi")它在并发示例中经常出现,用来保证每个 goroutine 拿到独立变量,避免闭包捕获同一个i。
在并发场景里相当于给变量拍一张当时的快照:
fori:=0;i<n;i++{gofunc(iint){// 使用 i}(i)}2.3 函数也是值
这个用法让我想到了C++的函数指针传递函数,总之这一套函数操作自由度极高
Go 的函数可以作为值传递。
更有意思的是,当函数引用了外部变量时,它带走的不只是函数本体,还包括它需要的那部分环境。
这就是闭包的味道:函数像是背了一个小包,把路上需要的变量也一起带着。
函数可以赋值给变量:
add:=func(a,bint)int{// 匿名函数作为值传递returna+b}result:=add(1,2)要注意的是内置函数(例如slice的len,cap等)不能作为值在变量之间传递:
Built-in functions
The built-in functions do not have standard Go types, so they can only appear in call expressions; they cannot be used as function values.
2.3.1 函数作为参数
函数作为参数时,我们可以把“要做什么”交给调用方决定。
外层函数只负责搭台,真正的计算逻辑由传进来的函数登场:
funcoperate(a,bint,fnfunc(int,int)int)int{returnfn(a,b)}result:=operate(3,4,func(x,yint)int{returnx+y})2.3.2 函数作为返回值
函数作为返回值时,外层函数就像一个工厂:根据传入参数,生产出一个定制版函数。
funcmakeAdder(baseint)func(int)int{returnfunc(xint)int{returnbase+x}}使用:
add10:=makeAdder(10)fmt.Println(add10(5))// 15这里内部函数引用了外部变量base。makeAdder(10)返回的不是一个普通加法器,而是一个已经记住base = 10的加法器,这就是闭包。
2.4 闭包
闭包可以记住外部变量。
如果普通函数像一次性工具,用完就散场;闭包更像带记忆的工具,它能把某些状态留在身上,下次调用时继续用。
funccounter()func()int{count:=0returnfunc()int{count++returncount}}使用:
c:=counter()fmt.Println(c())// 1fmt.Println(c())// 2fmt.Println(c())// 3这里的count明明定义在counter里面,但counter返回以后它并没有立刻消失。
因为返回的匿名函数还需要它,所以这个变量会继续活着。每次调用c(),都是在修改同一个被记住的count。
2.5 defer 让函数延迟执行
被defer标记的函数会在当前函数结束前执行
它特别适合做“收尾动作”:打开文件后记得关闭,加锁后记得解锁,申请资源后记得释放。
funcreadFile(){deferfmt.Println("close file")fmt.Println("read file")}输出:
read file close file多个defer是后进先出,可以想象成栈的结构
functest(){deferfmt.Println("A")deferfmt.Println("B")deferfmt.Println("C")}输出:
C B A3. Function 进阶
3.1 面向对象
Go 没有class关键字,但这并不影响它表达“对象行为”。
它没有把方法塞进类里,而是把方法绑定到类型上:类型负责数据,方法负责行为,两者组合起来也能写出清晰的对象模型。
对象方法:本质是带接收者的函数
官方文档:Method_declarations
Go 可以给类型定义方法:
MethodDecl = "func" Receiver MethodName Signature [ FunctionBody ] . Receiver = Parameters .官方定义里面,将接收者插在了func与MethodName之间。
下面这段代码看起来像成员方法,但本质仍然是一种带接收者的函数:
typePlayerstruct{NamestringHPint}func(p Player)SayName(){fmt.Println(p.Name)}调用:
player:=Player{Name:"Knight",HP:100}player.SayName()下面有几点注意事项:
- receiver 只能有一个,不能是可变参数。
- receiver 必须绑定到当前包里的 defined type。
- 方法名绑定到 receiver base type,不是绑定到某个实例。
- 泛型类型可以有方法,但 receiver 的类型参数声明有严格规则。
值接收者和指针接收者是 Go 方法里非常关键的一道分水岭。
值接收者会复制一份对象,像是拿到一份复印件;指针接收者拿到的是原对象的地址,修改会真正落到原对象身上。
可以通过下面一个例子来证明:
type Player struct { HP int } func (p Player) ValueChange() { p.HP = 0 } func (p *Player) PointerChange() { p.HP = 0 } func main() { p := Player{HP: 100} p.ValueChange() fmt.Println(p.HP) // 100 p.PointerChange() fmt.Println(p.HP) // 0 }虽然方法接收者是*Player,但 Go 通常允许你直接用player.TakeDamage(),不用手动写(&player).TakeDamage()。
这是 Go 在可寻址变量上提供的一点语法照顾,写起来更像普通方法调用:
Effective Go: methods
值接收者方法:T和*T都能调用
指针接收者方法:原则上只有*T能调用
但如果T是“可寻址的变量”,Go 会自动帮你取地址
3.2 可变参数函数
可变参数函数适合处理“参数数量不固定”的场景。
语法是...类型,意思是:这里可以接 0 个、1 个,也可以接很多个同类型参数。
funcsum(nums...int)int{total:=0for_,n:=rangenums{total+=n}returntotal}调用:
sum(1,2,3)sum()如果已经有 slice,可以用...展开。
这一步有点像把一整盒零件倒出来,一个个交给函数:
nums:=[]int{1,2,3}sum(nums...)注意:可变参数必须放在参数列表最后。
funclog(prefixstring,values...int){}3.3 函数类型:给函数起一个别名
可以给函数签名起别名,让意图更清楚:
typeOperationfunc(int,int)intfunccalculate(a,bint,op Operation)int{returnop(a,b)}4. 小结
到这里,Go function 的主要地图基本展开了。
它不是只有“输入参数,返回结果”这么简单,而是一整套围绕代码复用、行为传递、状态保存、资源收尾组织起来的工具。
看看下面这些函数写法,你是否都能一眼读出含义?
funcf()funcf(aint)funcf(a,bint)funcf(aint,bstring)funcf()intfuncf()(int,string)funcf(aint)(resultint)funcf(nums...int)funcf(fnfunc(int)int)funcf()func(int)int如果前面的内容都能读懂,再看下面这个综合案例就会顺很多。
packagemainimport"fmt"typePlayerstruct{NamestringHPint}funcNewPlayer(namestring)*Player{return&Player{Name:name,HP:100,}}func(p*Player)TakeDamage(damageint){p.HP-=damage}func(p Player)IsAlive()bool{returnp.HP>0}funcmain(){player:=NewPlayer("Cat Knight")player.TakeDamage(30)fmt.Println(player.Name)fmt.Println(player.HP)fmt.Println(player.IsAlive())}输出:
Cat Knight 70 true