官方参考Go SpecBuilt-in functionsMethod_declarations写 Go 的时候函数是最先见到、也最容易被低估的角色。它表面上只是func关键字后面跟一段代码但真正写起来你会发现Go 把函数设计得非常“务实”。它可以有多个返回值可以被当作值传来传去可以临时组装成闭包还能摇身一变成为某个类型的方法。下面就来一起了解function有哪些基础的使用方法以及技巧1. Function 基础Go 里函数用func关键字定义func函数名(参数列表)返回值类型{// 函数体}例如funcadd(aint,bint)int{returnab}// 可以将同类型参数合并声明funcadd(a,bint)int{returnab}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){areaw*hreturn}funcrectangle(w,hint)int{// 等价area:w*hreturnarea}多返回值也可以命名funccalc(a,bint)(sumint,diffint){sumab diffa-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。对比点initmain是否自动执行是是是否建议手动调用否否数量多个只能一个执行时机程序启动前最后执行所在包任意包只能 main 包用途初始化程序入口2.2 立即执行函数对于写过 Js对这一套应该很眼熟。函数刚定义完后面立刻跟一对括号执行。func(){fmt.Println(run immediately)}()func(namestring){fmt.Println(Hello,name)}(XiaoYi)它在并发示例中经常出现用来保证每个 goroutine 拿到独立变量避免闭包捕获同一个i。在并发场景里相当于给变量拍一张当时的快照fori:0;in;i{gofunc(iint){// 使用 i}(i)}2.3 函数也是值这个用法让我想到了C的函数指针传递函数总之这一套函数操作自由度极高Go 的函数可以作为值传递。更有意思的是当函数引用了外部变量时它带走的不只是函数本体还包括它需要的那部分环境。这就是闭包的味道函数像是背了一个小包把路上需要的变量也一起带着。函数可以赋值给变量add:func(a,bint)int{// 匿名函数作为值传递returnab}result:add(1,2)要注意的是内置函数例如slice的lencap等不能作为值在变量之间传递Built-in functionsThe 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{returnxy})2.3.2 函数作为返回值函数作为返回值时外层函数就像一个工厂根据传入参数生产出一个定制版函数。funcmakeAdder(baseint)func(int)int{returnfunc(xint)int{returnbasex}}使用add10:makeAdder(10)fmt.Println(add10(5))// 15这里内部函数引用了外部变量base。makeAdder(10)返回的不是一个普通加法器而是一个已经记住base 10的加法器这就是闭包。2.4 闭包闭包可以记住外部变量。如果普通函数像一次性工具用完就散场闭包更像带记忆的工具它能把某些状态留在身上下次调用时继续用。funccounter()func()int{count:0returnfunc()int{countreturncount}}使用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_declarationsGo 可以给类型定义方法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{totaln}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如果前面的内容都能读懂再看下面这个综合案例就会顺很多。packagemainimportfmttypePlayerstruct{NamestringHPint}funcNewPlayer(namestring)*Player{returnPlayer{Name:name,HP:100,}}func(p*Player)TakeDamage(damageint){p.HP-damage}func(p Player)IsAlive()bool{returnp.HP0}funcmain(){player:NewPlayer(Cat Knight)player.TakeDamage(30)fmt.Println(player.Name)fmt.Println(player.HP)fmt.Println(player.IsAlive())}输出Cat Knight 70 true