函数函数是一段可复用的代码块接收参数、执行逻辑再通过return把结果交还给调用方。Go 的入口也是函数——程序从main开始跑。函数定义基本语法func函数名(参数名 参数类型)返回值类型{// 函数体return返回值}拆开看每一部分部分含义func声明函数的关键字函数名包内可调用的名字首字母大写表示对外导出参数列表名字 类型多个参数用逗号分隔同类型可简写为a, b int返回值类型写在参数列表后面无返回值时可省略return结束函数并把结果返回给调用方返回值类型必须与声明一致最小例子packagemainimportfmtfuncadd(aint,bint)int{returnab}funcmain(){fmt.Println(add(1,2))// 3}参数同类型时可以合并写法func add(a, b int) int。多返回值Go 支持一次返回多个值常见于「结果 错误」详见下一节「错误处理」packagemainimportfmtfunccalc(a,bint)(int,int){returnab,a-b}funcmain(){sum,sub:calc(1,2)fmt.Println(sum,sub)// 3 -1}也可以给返回值命名命名返回值。此时return可以不带表达式会返回当前命名变量的值funccalc(a,bint)(sumint,subint){sumab suba-breturn// 等价于 return sum, sub}不需要某个返回值时用_丢弃sum,_:calc(1,2)注意事项不能在函数内部声明具名函数。这不只是main任意函数里都不允许再写func foo() {}这种带名字的声明。需要嵌套逻辑时用匿名函数见下文。函数必须先声明再调用的说法不成立同一包内函数可以任意顺序定义。Go 是按值传递传入的是副本。想改调用方的变量要传指针或使用切片 / map 这类引用语义类型。可变参数用...类型例如func sum(nums ...int) int调用时写成sum(1, 2, 3)。错误示例无法编译funcmain(){// 不允许函数里再声明具名函数funcadd(a,bint)int{returnab}}正确写法把内部逻辑写成匿名函数赋给变量funcmain(){add:func(a,bint)int{returnab}fmt.Println(add(1,2))}错误处理Go没有try / catch。函数出错时惯例是多返回值最后一个是error。调用方必须自己检查。error是内置接口只有一个方法Error() string。没出错时返回nil有错时返回非nil。packagemainimport(errorsfmt)// 除数为 0 时返回错误funcdivide(a,bint)(int,error){ifb0{return0,errors.New(除数不能为 0)}returna/b,nil}funcmain(){result,err:divide(10,2)iferr!nil{fmt.Println(出错了:,err)return}fmt.Println(结果:,result)// 5_,errdivide(10,0)iferr!nil{fmt.Println(出错了:,err)// 出错了: 除数不能为 0}}常用写法说明写法作用errors.New(说明)创建一个简单错误fmt.Errorf(a%d 非法, a)带格式化信息的错误if err ! nil { ... }检查错误的标准姿势return 0, err把错误继续往上抛给调用方注意不要忽略error。写成result, _ : divide(...)等于把错误吞掉出了问题很难排查。出错时第一个返回值通常给零值如0、、nil真正该看的是err。很多标准库函数也是这个模式例如os.Open、strconv.Atoipackagemainimport(fmtstrconv)funcmain(){n,err:strconv.Atoi(123)iferr!nil{fmt.Println(转换失败:,err)return}fmt.Println(n)// 123_,errstrconv.Atoi(abc)iferr!nil{fmt.Println(转换失败:,err)}}和panic的区别先建立直觉即可日常可预期的失败文件不存在、输入不合法用error程序无法继续的严重问题才用panic。入门阶段优先掌握if err ! nil。匿名函数没有名字的函数叫匿名函数。可以立刻执行也可以赋给变量、当作参数传递。立刻执行IIFEpackagemainimportfmtfuncmain(){func(){fmt.Println(匿名函数)}()// 末尾 () 表示立刻调用}赋给变量后再调用packagemainimportfmtfuncmain(){greet:func(namestring){fmt.Println(你好,,name)}greet(Go)}注意事项闭包会捕获外层变量。匿名函数引用外层变量时拿到的是变量本身引用语义不是当时的拷贝。循环里起 goroutine 时尤其容易踩坑要额外注意。匿名函数可以访问外层作用域的变量适合做回调、延迟逻辑配合defer。只有「函数类型」的值才能赋值、传递具名函数本身不是变量但可以写成f : add把函数当值用。需要反复复用、导出给其他包用的逻辑优先写成包级具名函数可读性和调试都更好。闭包示例packagemainimportfmtfuncmakeCounter()func()int{n:0returnfunc()int{nreturnn}}funcmain(){next:makeCounter()fmt.Println(next())// 1fmt.Println(next())// 2}泛型函数没有泛型时想对int、float64做同样的加法往往要写两套几乎一样的函数很繁琐funcaddInt(a,bint)int{returnab}funcaddFloat(a,bfloat64)float64{returnab}Go 1.18 起支持类型参数。把「类型」也参数化一套函数就能服务多种类型——这就是泛型的中心思想packagemainimportfmt// T 被约束为 int、uint 或 float64这些类型都能做 funcadd[Tint|uint|float64](a,b T)T{returnab}funcmain(){fmt.Println(add[int](1,2))// 显式指定类型参数fmt.Println(add[uint](1,2))fmt.Println(add(1.5,2.5))// 编译器从实参推断 T 为 float64}[T int | uint | float64]类型参数T以及它允许的类型集合类型约束。调用时可以写add[int](1, 2)多数情况也能靠实参类型推断省略。约束里的类型必须都支持函数体里用到的运算否则编译报错。也可以用comparable、自定义 interface 约束等更复杂的写法入门先记住泛型用来消灭「同逻辑、多类型」的重复函数。defer 关键字defer会把一次函数调用推迟到当前函数即将返回之前执行常用于关闭文件、解锁、收尾打印等「成对出现」的清理工作。基本例子packagemainimportfmtfuncmain(){fmt.Println(main 1)deferfmt.Println(defer 1)fmt.Println(main 2)}输出main 1 main 2 defer 1多个 defer后进先出LIFO多个defer按栈顺序执行最后注册的最先跑packagemainimportfmtfuncmain(){deferfmt.Println(第一)deferfmt.Println(第二)deferfmt.Println(第三)fmt.Println(函数体)}输出函数体 第三 第二 第一实用案例保证资源释放打开文件后立刻defer Close()后面无论正常返回还是提前return都会关闭packagemainimport(fmtos)funcreadFile(pathstring)error{f,err:os.Open(path)iferr!nil{returnerr}deferf.Close()// 函数返回前关闭避免泄漏buf:make([]byte,64)n,err:f.Read(buf)iferr!nil{returnerr}fmt.Println(string(buf[:n]))returnnil}再记两点参数在 defer 语句执行时就已经算好不是等到真正调用时才算packagemainimportfmtfuncmain(){x:1deferfmt.Println(defer:,x)// 这里已经把 x1 传进去了x2fmt.Println(main:,x)}输出是main: 2然后defer: 1。defer绑定的是函数不是代码块。写在for里的defer会等到外层函数返回才执行循环里反复defer可能堆积很多调用一般要包一层函数或换写法。用接口模拟「面向对象」能力先把话说清楚Go没有class、没有基于类的继承也不是传统 Java / C 那种面向对象语言。Go 提供的是结构体存数据、方法绑行为、接口描述能力。多态主要靠 interface 实现。所以更准确的说法是Go 用「组合 方法 接口」达到类似面向对象里「多态、面向行为编程」的效果而不是复制一套 class 体系。接口只声明「要有哪些方法」不关心具体类型packagemainimportfmt// Speaker 描述「能说话」这种能力typeSpeakerinterface{Speak()string}typeDogstruct{Namestring}func(d Dog)Speak()string{returnd.Name: 汪汪}typeCatstruct{Namestring}func(c Cat)Speak()string{returnc.Name: 喵喵}// 只依赖接口不依赖具体类型funcsay(s Speaker){fmt.Println(s.Speak())}funcmain(){say(Dog{Name:旺财})say(Cat{Name:咪咪})}要点类型不用显式写 implements。只要方法集满足接口就自动实现了该接口。函数参数写成接口类型后传入任何实现了这些方法的值都可以——这就是接口带来的多态。需要「复用字段 / 方法」时优先用结构体嵌入组合而不是继承。和「有没有面向对象」相关的常见误区看到方法、接口就以为 Go 是 class OOP——其实它刻意避开了继承树把重点放在接口和组合上。作业基本函数写一个max(a, b int) int返回较大的那个数在main里调用并打印结果。多返回值写一个swap(a, b int) (int, int)交换两个整数并返回用x, y : swap(1, 2)验证。错误处理写一个sqrt(n float64) (float64, error)n 0时返回错误可用errors.New否则返回平方根可用math.Sqrt。在main里分别用正数和负数调用正确处理err。匿名函数在main里用匿名函数实现「打印 1 到 5」立刻执行再把同一个逻辑赋给变量后调用一次。综合练习写一个parseAndDouble(s string) (int, error)用strconv.Atoi把字符串转成整数后乘以 2转换失败则把错误返回。在main里测试21和abc两种输入。