Go入门Go语言注释规范与文档生成大家好我是你们的Go语言向导。上一篇我们学习了gofmt的代码格式化。今天我们来聊一个同样重要但经常被忽视的话题——注释与文档。 好的代码自己会说话但好的注释能让代码说得更清楚。Go语言从诞生之初就非常重视文档它内建了go doc工具让文档成为了一等公民。在这篇文章中我会带你全面掌握Go的注释规范和文档生成机制。一、注释在Go中的角色1.1 代码是写给谁看的思考一个问题代码究竟是写给谁看的答案有两个编译器和人。编译器读取代码并生成机器码而人包括你自己、同事、未来的维护者需要理解代码的意图和设计决策。编译器不读注释注释是写给人的。但好的注释能帮助编译器做一件事——通过go doc生成漂亮的文档。Go语言的设计者Rob Pike说过“Comments in Go serve dual purposes: they explain the code to humans, and they serve as documentation that can be extracted by tools.”1.2 Go注释的两种形式Go支持两种注释形式// 单行注释Line Comment// 以 // 开头到行尾结束/* 多行注释Block Comment 以 /* 开头以 */结束 可以跨越多行*/Go语言的使用惯例包注释、函数注释、类型注释 → 使用//单行注释可多行连续大段注释掉的代码 → 使用/* */块注释行内短注释 → 使用//二、Go的文档注释规范2.1 基本规则Go的文档注释有几条黄金规则这些规则由go doc工具解析规则一注释在被注释对象的上方中间没有空行// Add 计算两个整数的和。// 它接收两个int类型的参数返回它们的和。funcAdd(a,bint)int{returnab}规则二注释以被注释对象的名字开头// User 表示系统中的用户实体。// User包含用户的基本信息如姓名、邮箱和年龄。typeUserstruct{NamestringEmailstringAgeint}// NewUser 创建一个新的User实例。// 它接受姓名和邮箱返回一个指向User的指针。funcNewUser(name,emailstring)*User{returnUser{Name:name,Email:email,}}// Calculate 根据给定的类型执行计算。// 注意这个函数名不够具体在真实项目中应避免。funcCalculate(operationstring,a,bint)int{switchoperation{caseadd:returnabcasesub:returna-bdefault:return0}}规则三包注释放在package声明之前// Package mathutil 提供数学计算相关的工具函数。//// 本包包含常用的数学运算、统计计算和数值处理工具。// 所有函数都是并发安全的。//// 使用示例://// result : mathutil.Add(1, 2)// avg : mathutil.Mean([]float64{1.0, 2.0, 3.0})packagemathutil重要细节包注释与package声明之间不能有空行否则go doc无法正确提取包注释。2.2 注释内容规范Go的注释风格追求简洁和专业// ✅ 好的注释简洁、信息丰富// Split 将字符串s按分隔符sep分割返回子字符串切片。// 如果s为空字符串Split返回一个包含空字符串的切片。funcSplit(s,sepstring)[]string{...}// ✅ 好的注释可以包含示例// Join 将字符串切片elements用分隔符sep连接起来。//// 示例:// Join([]string{a, b, c}, , ) // 返回 a, b, cfuncJoin(elements[]string,sepstring)string{...}// ❌ 不好的注释说了等于没说// Add 进行加法运算funcAdd(a,bint)int{returnab}// ❌ 不好的注释没有以函数名开头// 这个函数用来计算两个数的和funcAdd(a,bint)int{returnab}2.3 导出标识符必须加注释在Go语言中所有导出的标识符首字母大写的类型、函数、变量、常量都应该有文档注释。golint和golangci-lint会检查这一点。// Config 表示应用程序的配置信息。typeConfigstruct{// ServerHost 是HTTP服务器监听的地址。ServerHoststring// ServerPort 是HTTP服务器监听的端口。ServerPortint// Debug 表示是否启用调试模式。Debugbool}// LoadConfig 从指定路径加载配置文件。// 如果文件不存在或格式不正确返回错误。funcLoadConfig(pathstring)(*Config,error){...}2.4 包注释的详细写法一个完善的包注释应该包含// Package user 提供用户管理相关的功能。//// 本包实现了用户的创建、查询、更新和删除操作// 以及用户认证和权限管理。//// 基本用法//// 创建一个新用户//// u : user.New(张三, zhangsanexample.com)// u.SetPassword(secure_password)// err : u.Save()//// 查询用户//// u, err : user.FindByID(123)// if err ! nil {// log.Fatal(err)// }//// 注意事项//// 本包的所有数据库操作都在事务中执行。// 在并发环境中使用时请注意数据库连接池的配置。//// 更多信息请参考项目README或API文档。packageuser 包注释通常放在doc.go文件中这个文件专门用于存放包级别的文档// doc.go - user包//// 这个文件仅包含包的文档注释不包含任何代码逻辑。// 这是一种常见的Go项目约定。// Package user 提供用户管理功能。// ... 完整的包文档packageuser三、go doc 工具详解3.1 查看文档go doc是Go内建的文档查看工具可以从命令行快速查阅包、类型、函数的文档。# 查看包文档go docfmtgo doc encoding/json go doc net/http# 查看特定类型/函数go doc fmt.Println go doc json.Marshal go doc http.ServeMux# 查看特定方法go doc http.ServeMux.Handle go doc time.Time.Format# 在当前包中查看go doc MyType go doc MyFunc# 查看包的完整文档包括未导出的go doc-allfmt# 输出简短的一行说明go doc-shortfmt# 显示文档的源代码go doc-srcfmt.Println3.2 go doc 的输出格式当你执行go doc时它会提取源代码中的文档注释生成结构化的文档$ go doc fmt.Println packagefmt//importfmtfunc Println(a...any)(n int, err error)Println formats using the default formatsforits operands and writes to standard output. Spaces are always added between operands and a newline is appended. It returns the number of bytes written and anywriteerror encountered.输出包含包路径函数签名文档注释内容3.3 本地文档服务器Go还提供了Web界面的文档浏览器godoc需要单独安装# 安装godocgoinstallgolang.org/x/tools/cmd/godoclatest# 启动文档服务器godoc-http:6060# 浏览器访问# http://localhost:6060/pkg/ - 查看所有标准库和第三方库文档# http://localhost:6060/pkg/fmt/ - 查看fmt包文档启动后你在浏览器中看到的文档界面和pkg.go.dev非常相似——实际上pkg.go.dev的后端使用的就是godoc技术。四、pkg.go.dev 在线文档4.1 什么是 pkg.go.devpkg.go.dev是Go官方的包文档网站它会自动发现和展示所有公开的Go模块文档。当你发布一个Go模块到GitHub或其他公开仓库后pkg.go.dev会自动发现你的模块拉取代码提取文档注释生成漂亮的文档页面显示许可证、依赖、版本等信息4.2 优化pkg.go.dev展示为了让你的包在pkg.go.dev上展示得更专业建议1. 添加 README.mdREADME会显示在pkg.go.dev的概览页面上2. 添加 LICENSE 文件许可证信息会被自动识别3. 添加版本标签使用语义化版本标签如 v1.0.04. 包注释中不要包含HTML纯文本的包注释会被正确渲染4.3 如何确保文档被正确展示# 检查你的包文档本地预览go doc your/package/path# 使用 pkgsite 本地预览更接近线上效果goinstallgolang.org/x/pkgsite/cmd/pkgsitelatest pkgsite-open.五、代码注释的最佳实践5.1 好的注释 vs 坏的注释// ✅ 好的注释解释了为什么// 使用二分查找算法而非线性查找// 因为输入数据已经排序且数据量较大通常 1000条funcfindUser(users[]User,idint)(*User,error){// 二分查找实现...}// ❌ 坏的注释解释了是什么代码已经说明了的// 遍历用户列表for_,user:rangeusers{// 如果用户ID匹配ifuser.IDid{// 返回用户returnuser}}// ✅ 好的注释记录了设计决策和限制// ParseIP 解析IPv4和IPv6地址。// 注意此函数不支持IPv6 zone ID。// 如果输入不是有效的IP地址返回nil。funcParseIP(sstring)net.IP{...}// ✅ 好的注释标注了潜在陷阱// Marshal 将v序列化为JSON字节。// 注意JSON字段名默认使用结构体字段名驼峰转小写// 可以使用json标签自定义。循环引用会导致死循环。funcMarshal(v any)([]byte,error){...}5.2 注释的五个W原则 好的注释应该回答以下问题What这段代码做了什么代码本身能回答的话可以省略Why为什么这样做最重要的信息When什么情况下使用How怎么使用对于APIWarning有什么需要注意的// CalculateShippingFee 根据订单金额、重量和目的地计算运费。//// 计费规则:// - 订单金额超过99元免运费偏远地区除外// - 重量超过20kg每公斤加收2元附加费// - 偏远地区基础运费加收15元//// 为什么这样设计:// 参考了行业标准同时考虑了利润率和用户接受度。// 经过A/B测试99元免邮门槛的转化率最高。//// 注意事项:// - 此函数不是并发安全的内部使用了共享状态// - 国际订单请使用 CalculateInternationalFee// - 价格以分为单位避免浮点数精度问题funcCalculateShippingFee(order*Order)(int,error){...}5.3 TODO和FIXME注释Go社区有一套约定俗成的注释标记// TODO(username): 这个函数需要统一移动到auth包中// FIXME: 在高并发场景下存在数据竞争需要在v2.1修复// BUG(username): 当输入为nil时会panic// HACK: 临时绕过第三方库的bug等待上游修复后移除// NOTE: 这里的排序使用了稳定排序依赖于order字段的值// OPTIMIZE: 当前O(n²)的时间复杂度在大数据集下需要优化// DEPRECATED: 此函数已废弃请使用 NewMethod 替代这些标记的好处是可以用工具搜索。在VS Code中TODO和FIXME会高亮显示# 搜索所有TODO注释grep-rnTODO--include*.go.# 搜索所有FIXME注释grep-rnFIXME--include*.go.5.4 废弃标记Go 1.9引入了官方的废弃标记// OldMethod 使用旧的算法处理数据。//// Deprecated: 此函数已废弃请使用 NewMethod 替代。// NewMethod 提供了更好的性能和更少的边界情况。// 此函数将在 v2.0 中移除。funcOldMethod(data[]byte)[]byte{...}// NewMethod 使用优化的算法处理数据。funcNewMethod(data[]byte)[]byte{...}当用户使用OldMethod时大多数IDE会显示删除线提示该函数已废弃。六、文档生成的完整示例6.1 写一个文档良好的包让我展示一个完整的、文档良好的Go包// Package validator 提供常见的数据验证功能。//// 本包支持以下验证类型// - 字符串验证长度、格式、正则// - 数字验证范围、类型// - 邮箱和URL格式验证// - 自定义验证规则//// 基本用法//// 创建一个验证器//// v : validator.New()// v.ValidateEmail(userexample.com) // 返回 nil有效// v.ValidateEmail(not-an-email) // 返回 error无效//// 链式调用//// err : v.Chain(testexample.com).// NotEmpty().// IsEmail().// MaxLength(100).// Done()//// 并发安全//// 所有验证器实例都是并发安全的可以在多个goroutine中共享使用。//// 性能说明//// 邮箱和URL验证使用了预编译的正则表达式避免了重复编译的开销。// 简单验证如长度检查的耗时通常在纳秒级别。packagevalidatorimport(errorsregexp)// 预编译正则表达式避免每次验证时重复编译var(emailRegexregexp.MustCompile(^[a-zA-Z0-9._%-][a-zA-Z0-9.-]\.[a-zA-Z]{2,}$)urlRegexregexp.MustCompile(^(https?:\/\/)?[\w\-](\.[\w\-])[/#?]?.*$))// 预定义的验证错误var(// ErrEmpty 表示值为空。ErrEmptyerrors.New(validator: 值不能为空)// ErrInvalidEmail 表示邮箱格式不正确。ErrInvalidEmailerrors.New(validator: 邮箱格式不正确)// ErrInvalidURL 表示URL格式不正确。ErrInvalidURLerrors.New(validator: URL格式不正确)// ErrTooLong 表示字符串超过最大长度限制。ErrTooLongerrors.New(validator: 字符串过长))// Validator 数据验证器。// Validator实例是并发安全的应该作为单例使用。typeValidatorstruct{// maxStringLength 是字符串验证的最大长度限制默认为1000。maxStringLengthint// allowEmpty 控制是否允许空值通过验证。allowEmptybool}// New 创建一个新的验证器实例。// 返回的验证器使用默认配置最大长度1000不允许空值。funcNew()*Validator{returnValidator{maxStringLength:1000,allowEmpty:false,}}// NewWithConfig 使用指定的配置创建验证器实例。//// 示例://// v : validator.NewWithConfig(validator.Config{// MaxStringLength: 500,// AllowEmpty: true,// })funcNewWithConfig(cfg Config)*Validator{returnValidator{maxStringLength:cfg.MaxStringLength,allowEmpty:cfg.AllowEmpty,}}// Config 验证器的配置选项。typeConfigstruct{// MaxStringLength 设置字符串验证的最大长度限制。MaxStringLengthint// AllowEmpty 如果为true空字符串将通过所有验证。AllowEmptybool}// ValidateEmail 验证邮箱地址格式是否正确。//// 验证规则// - 必须包含 符号// - 域名部分必须包含点号// - 只允许字母、数字和常见的特殊字符//// 注意此函数不验证邮箱是否真实存在仅检查格式。//// 性能使用预编译正则单次验证约 200ns。func(v*Validator)ValidateEmail(emailstring)error{ifemail{ifv.allowEmpty{returnnil}returnErrEmpty}if!emailRegex.MatchString(email){returnErrInvalidEmail}returnnil}// ValidateURL 验证URL格式是否正确。func(v*Validator)ValidateURL(urlstring)error{ifurl{ifv.allowEmpty{returnnil}returnErrEmpty}if!urlRegex.MatchString(url){returnErrInvalidURL}returnnil}// validateLength 验证字符串长度未导出仅供内部使用。func(v*Validator)validateLength(sstring)error{iflen(s)v.maxStringLength{returnErrTooLong}returnnil}// Chain 创建一个验证链用于链式调用多个验证。//// 示例://// err : v.Chain(value).NotEmpty().IsEmail().MaxLength(100).Done()func(v*Validator)Chain(valuestring)*Chain{returnChain{v:v,value:value,}}// Chain 验证链支持流式调用。typeChainstruct{v*Validator valuestringerrerror}// NotEmpty 验证值不为空。func(c*Chain)NotEmpty()*Chain{ifc.err!nil{returnc}ifc.value{c.errErrEmpty}returnc}// IsEmail 验证值为有效的邮箱地址。func(c*Chain)IsEmail()*Chain{ifc.err!nil{returnc}c.errc.v.ValidateEmail(c.value)returnc}// MaxLength 验证值不超过指定长度。func(c*Chain)MaxLength(maxLenint)*Chain{ifc.err!nil{returnc}iflen(c.value)maxLen{c.errErrTooLong}returnc}// Done 完成验证链返回验证结果。func(c*Chain)Done()error{returnc.err}6.2 运行go doc查看效果$ go doc validator package validator //importexample.com/validatorPackage validator provides common data validation functionality.... $ go doc validator.ValidateEmail func(v *Validator)ValidateEmail(email string)error ValidateEmail validatesifthe email addressformatis correct.七、注释与代码生成7.1 //go:generate 注释Go支持一种特殊的注释——构建指令注释。它们以//go:开头不是给人看的而是给Go工具链看的。//go:generate stringer -typeStatustypeStatusintconst(StatusPending StatusiotaStatusActive StatusSuspended StatusDeleted)//go:generate mockgen -sourceuser.go -destinationmock/user_mock.gotypeUserRepositoryinterface{FindByID(idint)(*User,error)Save(user*User)error}运行go generate ./...会自动执行这些生成命令。7.2 构建约束注释//go:build linux amd64// build linux,amd64packageplatform// 这个文件只在 linux/amd64 平台编译⚠️//go:build和// build的区别Go 1.17使用//go:build语法旧版本使用// build语法在Go 1.17中仍然兼容八、本篇总结✅ 本篇我们全面学习了Go语言的注释规范与文档生成注释形式单行注释//和多行注释/* */文档注释规则以被注释对象名开头放在声明上方包注释在package之前go doc工具命令行查看文档本地启动文档服务器pkg.go.dev在线文档自动生成平台注释最佳实践解释为什么而非是什么使用TODO/FIXME/DEPRECATED标记完整示例一个文档良好的validator包的完整实现 最后分享我的心得把写注释当成写API文档。当你写一个导出的函数时想象你是这个函数的使用者——你需要知道什么输入是什么输出是什么有什么边界情况和注意事项回答了这些问题你就写出了一份好的文档注释。下一篇我们将学习Go的标识符命名规则与最佳实践让变量名、函数名、包名都充满Go味。
Go入门:Go语言注释规范与文档生成
发布时间:2026/7/14 9:27:02
Go入门Go语言注释规范与文档生成大家好我是你们的Go语言向导。上一篇我们学习了gofmt的代码格式化。今天我们来聊一个同样重要但经常被忽视的话题——注释与文档。 好的代码自己会说话但好的注释能让代码说得更清楚。Go语言从诞生之初就非常重视文档它内建了go doc工具让文档成为了一等公民。在这篇文章中我会带你全面掌握Go的注释规范和文档生成机制。一、注释在Go中的角色1.1 代码是写给谁看的思考一个问题代码究竟是写给谁看的答案有两个编译器和人。编译器读取代码并生成机器码而人包括你自己、同事、未来的维护者需要理解代码的意图和设计决策。编译器不读注释注释是写给人的。但好的注释能帮助编译器做一件事——通过go doc生成漂亮的文档。Go语言的设计者Rob Pike说过“Comments in Go serve dual purposes: they explain the code to humans, and they serve as documentation that can be extracted by tools.”1.2 Go注释的两种形式Go支持两种注释形式// 单行注释Line Comment// 以 // 开头到行尾结束/* 多行注释Block Comment 以 /* 开头以 */结束 可以跨越多行*/Go语言的使用惯例包注释、函数注释、类型注释 → 使用//单行注释可多行连续大段注释掉的代码 → 使用/* */块注释行内短注释 → 使用//二、Go的文档注释规范2.1 基本规则Go的文档注释有几条黄金规则这些规则由go doc工具解析规则一注释在被注释对象的上方中间没有空行// Add 计算两个整数的和。// 它接收两个int类型的参数返回它们的和。funcAdd(a,bint)int{returnab}规则二注释以被注释对象的名字开头// User 表示系统中的用户实体。// User包含用户的基本信息如姓名、邮箱和年龄。typeUserstruct{NamestringEmailstringAgeint}// NewUser 创建一个新的User实例。// 它接受姓名和邮箱返回一个指向User的指针。funcNewUser(name,emailstring)*User{returnUser{Name:name,Email:email,}}// Calculate 根据给定的类型执行计算。// 注意这个函数名不够具体在真实项目中应避免。funcCalculate(operationstring,a,bint)int{switchoperation{caseadd:returnabcasesub:returna-bdefault:return0}}规则三包注释放在package声明之前// Package mathutil 提供数学计算相关的工具函数。//// 本包包含常用的数学运算、统计计算和数值处理工具。// 所有函数都是并发安全的。//// 使用示例://// result : mathutil.Add(1, 2)// avg : mathutil.Mean([]float64{1.0, 2.0, 3.0})packagemathutil重要细节包注释与package声明之间不能有空行否则go doc无法正确提取包注释。2.2 注释内容规范Go的注释风格追求简洁和专业// ✅ 好的注释简洁、信息丰富// Split 将字符串s按分隔符sep分割返回子字符串切片。// 如果s为空字符串Split返回一个包含空字符串的切片。funcSplit(s,sepstring)[]string{...}// ✅ 好的注释可以包含示例// Join 将字符串切片elements用分隔符sep连接起来。//// 示例:// Join([]string{a, b, c}, , ) // 返回 a, b, cfuncJoin(elements[]string,sepstring)string{...}// ❌ 不好的注释说了等于没说// Add 进行加法运算funcAdd(a,bint)int{returnab}// ❌ 不好的注释没有以函数名开头// 这个函数用来计算两个数的和funcAdd(a,bint)int{returnab}2.3 导出标识符必须加注释在Go语言中所有导出的标识符首字母大写的类型、函数、变量、常量都应该有文档注释。golint和golangci-lint会检查这一点。// Config 表示应用程序的配置信息。typeConfigstruct{// ServerHost 是HTTP服务器监听的地址。ServerHoststring// ServerPort 是HTTP服务器监听的端口。ServerPortint// Debug 表示是否启用调试模式。Debugbool}// LoadConfig 从指定路径加载配置文件。// 如果文件不存在或格式不正确返回错误。funcLoadConfig(pathstring)(*Config,error){...}2.4 包注释的详细写法一个完善的包注释应该包含// Package user 提供用户管理相关的功能。//// 本包实现了用户的创建、查询、更新和删除操作// 以及用户认证和权限管理。//// 基本用法//// 创建一个新用户//// u : user.New(张三, zhangsanexample.com)// u.SetPassword(secure_password)// err : u.Save()//// 查询用户//// u, err : user.FindByID(123)// if err ! nil {// log.Fatal(err)// }//// 注意事项//// 本包的所有数据库操作都在事务中执行。// 在并发环境中使用时请注意数据库连接池的配置。//// 更多信息请参考项目README或API文档。packageuser 包注释通常放在doc.go文件中这个文件专门用于存放包级别的文档// doc.go - user包//// 这个文件仅包含包的文档注释不包含任何代码逻辑。// 这是一种常见的Go项目约定。// Package user 提供用户管理功能。// ... 完整的包文档packageuser三、go doc 工具详解3.1 查看文档go doc是Go内建的文档查看工具可以从命令行快速查阅包、类型、函数的文档。# 查看包文档go docfmtgo doc encoding/json go doc net/http# 查看特定类型/函数go doc fmt.Println go doc json.Marshal go doc http.ServeMux# 查看特定方法go doc http.ServeMux.Handle go doc time.Time.Format# 在当前包中查看go doc MyType go doc MyFunc# 查看包的完整文档包括未导出的go doc-allfmt# 输出简短的一行说明go doc-shortfmt# 显示文档的源代码go doc-srcfmt.Println3.2 go doc 的输出格式当你执行go doc时它会提取源代码中的文档注释生成结构化的文档$ go doc fmt.Println packagefmt//importfmtfunc Println(a...any)(n int, err error)Println formats using the default formatsforits operands and writes to standard output. Spaces are always added between operands and a newline is appended. It returns the number of bytes written and anywriteerror encountered.输出包含包路径函数签名文档注释内容3.3 本地文档服务器Go还提供了Web界面的文档浏览器godoc需要单独安装# 安装godocgoinstallgolang.org/x/tools/cmd/godoclatest# 启动文档服务器godoc-http:6060# 浏览器访问# http://localhost:6060/pkg/ - 查看所有标准库和第三方库文档# http://localhost:6060/pkg/fmt/ - 查看fmt包文档启动后你在浏览器中看到的文档界面和pkg.go.dev非常相似——实际上pkg.go.dev的后端使用的就是godoc技术。四、pkg.go.dev 在线文档4.1 什么是 pkg.go.devpkg.go.dev是Go官方的包文档网站它会自动发现和展示所有公开的Go模块文档。当你发布一个Go模块到GitHub或其他公开仓库后pkg.go.dev会自动发现你的模块拉取代码提取文档注释生成漂亮的文档页面显示许可证、依赖、版本等信息4.2 优化pkg.go.dev展示为了让你的包在pkg.go.dev上展示得更专业建议1. 添加 README.mdREADME会显示在pkg.go.dev的概览页面上2. 添加 LICENSE 文件许可证信息会被自动识别3. 添加版本标签使用语义化版本标签如 v1.0.04. 包注释中不要包含HTML纯文本的包注释会被正确渲染4.3 如何确保文档被正确展示# 检查你的包文档本地预览go doc your/package/path# 使用 pkgsite 本地预览更接近线上效果goinstallgolang.org/x/pkgsite/cmd/pkgsitelatest pkgsite-open.五、代码注释的最佳实践5.1 好的注释 vs 坏的注释// ✅ 好的注释解释了为什么// 使用二分查找算法而非线性查找// 因为输入数据已经排序且数据量较大通常 1000条funcfindUser(users[]User,idint)(*User,error){// 二分查找实现...}// ❌ 坏的注释解释了是什么代码已经说明了的// 遍历用户列表for_,user:rangeusers{// 如果用户ID匹配ifuser.IDid{// 返回用户returnuser}}// ✅ 好的注释记录了设计决策和限制// ParseIP 解析IPv4和IPv6地址。// 注意此函数不支持IPv6 zone ID。// 如果输入不是有效的IP地址返回nil。funcParseIP(sstring)net.IP{...}// ✅ 好的注释标注了潜在陷阱// Marshal 将v序列化为JSON字节。// 注意JSON字段名默认使用结构体字段名驼峰转小写// 可以使用json标签自定义。循环引用会导致死循环。funcMarshal(v any)([]byte,error){...}5.2 注释的五个W原则 好的注释应该回答以下问题What这段代码做了什么代码本身能回答的话可以省略Why为什么这样做最重要的信息When什么情况下使用How怎么使用对于APIWarning有什么需要注意的// CalculateShippingFee 根据订单金额、重量和目的地计算运费。//// 计费规则:// - 订单金额超过99元免运费偏远地区除外// - 重量超过20kg每公斤加收2元附加费// - 偏远地区基础运费加收15元//// 为什么这样设计:// 参考了行业标准同时考虑了利润率和用户接受度。// 经过A/B测试99元免邮门槛的转化率最高。//// 注意事项:// - 此函数不是并发安全的内部使用了共享状态// - 国际订单请使用 CalculateInternationalFee// - 价格以分为单位避免浮点数精度问题funcCalculateShippingFee(order*Order)(int,error){...}5.3 TODO和FIXME注释Go社区有一套约定俗成的注释标记// TODO(username): 这个函数需要统一移动到auth包中// FIXME: 在高并发场景下存在数据竞争需要在v2.1修复// BUG(username): 当输入为nil时会panic// HACK: 临时绕过第三方库的bug等待上游修复后移除// NOTE: 这里的排序使用了稳定排序依赖于order字段的值// OPTIMIZE: 当前O(n²)的时间复杂度在大数据集下需要优化// DEPRECATED: 此函数已废弃请使用 NewMethod 替代这些标记的好处是可以用工具搜索。在VS Code中TODO和FIXME会高亮显示# 搜索所有TODO注释grep-rnTODO--include*.go.# 搜索所有FIXME注释grep-rnFIXME--include*.go.5.4 废弃标记Go 1.9引入了官方的废弃标记// OldMethod 使用旧的算法处理数据。//// Deprecated: 此函数已废弃请使用 NewMethod 替代。// NewMethod 提供了更好的性能和更少的边界情况。// 此函数将在 v2.0 中移除。funcOldMethod(data[]byte)[]byte{...}// NewMethod 使用优化的算法处理数据。funcNewMethod(data[]byte)[]byte{...}当用户使用OldMethod时大多数IDE会显示删除线提示该函数已废弃。六、文档生成的完整示例6.1 写一个文档良好的包让我展示一个完整的、文档良好的Go包// Package validator 提供常见的数据验证功能。//// 本包支持以下验证类型// - 字符串验证长度、格式、正则// - 数字验证范围、类型// - 邮箱和URL格式验证// - 自定义验证规则//// 基本用法//// 创建一个验证器//// v : validator.New()// v.ValidateEmail(userexample.com) // 返回 nil有效// v.ValidateEmail(not-an-email) // 返回 error无效//// 链式调用//// err : v.Chain(testexample.com).// NotEmpty().// IsEmail().// MaxLength(100).// Done()//// 并发安全//// 所有验证器实例都是并发安全的可以在多个goroutine中共享使用。//// 性能说明//// 邮箱和URL验证使用了预编译的正则表达式避免了重复编译的开销。// 简单验证如长度检查的耗时通常在纳秒级别。packagevalidatorimport(errorsregexp)// 预编译正则表达式避免每次验证时重复编译var(emailRegexregexp.MustCompile(^[a-zA-Z0-9._%-][a-zA-Z0-9.-]\.[a-zA-Z]{2,}$)urlRegexregexp.MustCompile(^(https?:\/\/)?[\w\-](\.[\w\-])[/#?]?.*$))// 预定义的验证错误var(// ErrEmpty 表示值为空。ErrEmptyerrors.New(validator: 值不能为空)// ErrInvalidEmail 表示邮箱格式不正确。ErrInvalidEmailerrors.New(validator: 邮箱格式不正确)// ErrInvalidURL 表示URL格式不正确。ErrInvalidURLerrors.New(validator: URL格式不正确)// ErrTooLong 表示字符串超过最大长度限制。ErrTooLongerrors.New(validator: 字符串过长))// Validator 数据验证器。// Validator实例是并发安全的应该作为单例使用。typeValidatorstruct{// maxStringLength 是字符串验证的最大长度限制默认为1000。maxStringLengthint// allowEmpty 控制是否允许空值通过验证。allowEmptybool}// New 创建一个新的验证器实例。// 返回的验证器使用默认配置最大长度1000不允许空值。funcNew()*Validator{returnValidator{maxStringLength:1000,allowEmpty:false,}}// NewWithConfig 使用指定的配置创建验证器实例。//// 示例://// v : validator.NewWithConfig(validator.Config{// MaxStringLength: 500,// AllowEmpty: true,// })funcNewWithConfig(cfg Config)*Validator{returnValidator{maxStringLength:cfg.MaxStringLength,allowEmpty:cfg.AllowEmpty,}}// Config 验证器的配置选项。typeConfigstruct{// MaxStringLength 设置字符串验证的最大长度限制。MaxStringLengthint// AllowEmpty 如果为true空字符串将通过所有验证。AllowEmptybool}// ValidateEmail 验证邮箱地址格式是否正确。//// 验证规则// - 必须包含 符号// - 域名部分必须包含点号// - 只允许字母、数字和常见的特殊字符//// 注意此函数不验证邮箱是否真实存在仅检查格式。//// 性能使用预编译正则单次验证约 200ns。func(v*Validator)ValidateEmail(emailstring)error{ifemail{ifv.allowEmpty{returnnil}returnErrEmpty}if!emailRegex.MatchString(email){returnErrInvalidEmail}returnnil}// ValidateURL 验证URL格式是否正确。func(v*Validator)ValidateURL(urlstring)error{ifurl{ifv.allowEmpty{returnnil}returnErrEmpty}if!urlRegex.MatchString(url){returnErrInvalidURL}returnnil}// validateLength 验证字符串长度未导出仅供内部使用。func(v*Validator)validateLength(sstring)error{iflen(s)v.maxStringLength{returnErrTooLong}returnnil}// Chain 创建一个验证链用于链式调用多个验证。//// 示例://// err : v.Chain(value).NotEmpty().IsEmail().MaxLength(100).Done()func(v*Validator)Chain(valuestring)*Chain{returnChain{v:v,value:value,}}// Chain 验证链支持流式调用。typeChainstruct{v*Validator valuestringerrerror}// NotEmpty 验证值不为空。func(c*Chain)NotEmpty()*Chain{ifc.err!nil{returnc}ifc.value{c.errErrEmpty}returnc}// IsEmail 验证值为有效的邮箱地址。func(c*Chain)IsEmail()*Chain{ifc.err!nil{returnc}c.errc.v.ValidateEmail(c.value)returnc}// MaxLength 验证值不超过指定长度。func(c*Chain)MaxLength(maxLenint)*Chain{ifc.err!nil{returnc}iflen(c.value)maxLen{c.errErrTooLong}returnc}// Done 完成验证链返回验证结果。func(c*Chain)Done()error{returnc.err}6.2 运行go doc查看效果$ go doc validator package validator //importexample.com/validatorPackage validator provides common data validation functionality.... $ go doc validator.ValidateEmail func(v *Validator)ValidateEmail(email string)error ValidateEmail validatesifthe email addressformatis correct.七、注释与代码生成7.1 //go:generate 注释Go支持一种特殊的注释——构建指令注释。它们以//go:开头不是给人看的而是给Go工具链看的。//go:generate stringer -typeStatustypeStatusintconst(StatusPending StatusiotaStatusActive StatusSuspended StatusDeleted)//go:generate mockgen -sourceuser.go -destinationmock/user_mock.gotypeUserRepositoryinterface{FindByID(idint)(*User,error)Save(user*User)error}运行go generate ./...会自动执行这些生成命令。7.2 构建约束注释//go:build linux amd64// build linux,amd64packageplatform// 这个文件只在 linux/amd64 平台编译⚠️//go:build和// build的区别Go 1.17使用//go:build语法旧版本使用// build语法在Go 1.17中仍然兼容八、本篇总结✅ 本篇我们全面学习了Go语言的注释规范与文档生成注释形式单行注释//和多行注释/* */文档注释规则以被注释对象名开头放在声明上方包注释在package之前go doc工具命令行查看文档本地启动文档服务器pkg.go.dev在线文档自动生成平台注释最佳实践解释为什么而非是什么使用TODO/FIXME/DEPRECATED标记完整示例一个文档良好的validator包的完整实现 最后分享我的心得把写注释当成写API文档。当你写一个导出的函数时想象你是这个函数的使用者——你需要知道什么输入是什么输出是什么有什么边界情况和注意事项回答了这些问题你就写出了一份好的文档注释。下一篇我们将学习Go的标识符命名规则与最佳实践让变量名、函数名、包名都充满Go味。