ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

Uber Go 语言编码规范:指向 interface 的指针(Pointers to Interfaces)深入解读

Uber Go 语言编码规范:指向 interface 的指针(Pointers to Interfaces)深入解读 文档【免费下载链接】uber_go_guide_cnUber Go 语言编码规范中文版. The Uber Go Style Guide .项目地址https://gitcode.com/gh_mirrors/ub/uber_go_guide_cn点击查看免费下载本指南是 Uber Go 语言编码规范基于 uber-go/guide 的中文翻译版当前仓库更新至 2024-08-10 版本中指导原则部分的核心条目之一。其核心结论简洁有力几乎永远不需要指向接口类型的指针接口应以值传递而底层数据仍可以是指针。读完本文你将掌握接口在底层的内存表示、何时必须传指针以修改底层数据、以及如何避免指向接口的指针这一经典误区并结合仓库中 interface-receiver.md接收器与接口与 interface-compliance.md接口合理性验证进行联动实践。规范原文核心结论interface-pointer.md 作为单独文档给出了四条核心论断它们是整个条目的骨架几乎不需要指向接口的指针——接口应当作为值来传递底层数据可以是指针——传接口值≠传值拷贝接口内部保存的仍然可以是指向真实对象的指针接口底层由两个字段构成——一个类型信息指针 一个数据指针要修改底层数据必须使用指针——将对象指针赋值给接口变量T赋值给接口而不是传值。在仓库主文档 README.md 中这一条目被译为指向 interface 的指针并补充了更多细节与反例代码下文将逐层展开。接口的底层表示两个字段规范原文interface-pointer.md明确指出接口在底层由两个字段构成。一个指向某些特定类型信息的指针——可以把它理解为type字段它记录了接口中保存的具体动态类型例如*S2还是S1Go 运行时用它来做类型断言、方法分派等操作。数据指针——这是理解本规范的关键如果存储的数据本身就是指针如S2{}那么该指针直接存储在接口中如果存储的数据是值如S1{}那么接口中存储的是指向该值的指针即运行时会在堆上复制一份值并把它的地址存入接口。因此接口本质上是一个引用类型无论你往里塞的是值还是指针接口内部都只持有一个指针级别的间接引用接口本身的开销是固定的两个 word。这也解释了为什么传接口值并不会像传普通结构体那样复制整个对象——传的只是这个二字段的头。补充说明仓库 README.md 的译者注进一步强调在 go 语言中接口本身就是引用类型换句话说接口类型本身就是一个指针。这正是下文永远不要使用指向接口的指针的底层原因如果接口本身已是间接引用再包一层*interface{}就完全多此一举。规范示例值接收器 vs 指针接收器规范在 README.md 与 interface-pointer.md 中给出了同一个核心示例用以说明什么时候需要指针传递type F interface { f() } type S1 struct{} func (s S1) f() {} // 值接收器 type S2 struct{} func (s *S2) f() {} // 指针接收器 // f1.f() 无法修改底层数据 // f2.f() 可以修改底层数据给接口变量 f2 赋值时使用的是对象指针 var f1 F S1{} var f2 F S2{}解析f1保存的是S1{}的值拷贝其f()方法在接收器s上操作的是接口内部持有的那份拷贝因此对方法内字段的任何修改都不会反映到原始对象上f2保存的是S2{}指针直接存储其f()方法通过指针间接操作的是原始对象因此方法内对字段的修改会真实生效。修改底层数据的正确姿势若接口的方法需要修改底层数据规范给出的做法是将对象指针赋值给接口变量。var f F S2{} // 传对象指针而非指向接口的指针也就是说你仍然是在传接口值只是这个接口值内部存的是指针。这与指向接口的指针*F是完全不同的两回事。反例剖析为什么*myinterface是错误写法仓库 README.md 的译者补充了一段真实场景的反例用于说明永远不要使用指向 interface 的指针type myinterface interface { print() } func test(value *myinterface) { //something to do ... } type mystruct struct { i int } // 实现接口 func (this *mystruct) print() { fmt.Println(this.i) this.i 1 } func main() { m : mystruct{0} test(m) // 错误 test(*m) // 错误 }这段代码暴露了两个典型错误编译期就会失败test(m)错误m的类型是*mystruct而test要求的是*myinterface。虽然*mystruct实现了myinterface但 Go 的接口方法集规则决定了*mystruct只匹配接口myinterface本身而不会匹配*myinterface。一个指向接口的指针与一个实现了接口的类型指针在类型系统里是截然不同的类型。test(*m)错误*m解引用后得到mystruct值而mystruct值类型只有指针接收器方法print它的值方法集为空根本不满足接口myinterface——这属于方法集不匹配的编译错误详见下文方法集与接口匹配。正确的写法应当是让test接收接口本身并把实现者的指针作为实参传入func test(value myinterface) { // something to do ... } func main() { m : mystruct{0} test(m) // 正确*mystruct 满足 myinterface }这里test(m)传入的是*mystruct自动装箱为接口值接口内部直接存储该指针print方法可以修改底层数据this.i——这正是规范要求的接口以值传递底层数据用指针。为什么指向接口的指针没有意义从接口的两个字段表示可以推导出接口本身就是一个两字段的引用头相当于一个间接层。若再使用*myinterface等于在这个间接层上再加一层间接而接口的动态类型信息type字段已经足以支撑运行时的一切操作类型断言、方法分派。多包一层指针只会带来需要额外的解引用与空指针判断赋值、比较、类型断言的语义变得混乱几乎没有任何收益你几乎找不到需要修改接口变量本身的场景。因此规范一句话总结永远不要使用指向 interface 的指针这是没有意义的。关联规则接收器与接口方法集匹配指向接口的指针误区之所以常见根源在于对方法集method set与接口匹配规则的混淆。仓库 interface-receiver.md 对此有完整阐述值得联动阅读值接收器的方法既可以通过值调用也可以通过指针调用指针接收器的方法只能通过指针或可寻址值addressable values调用。由此推导出的方法集规则README.md 译者补充值接收器方法集是指针接收器方法集的子集值对象只能使用值接收器方法集指针对象可以使用值接收器 指针接收器全部方法集接口匹配要么类型的值方法集匹配接口要么指针方法集匹配接口。若值方法集匹配接口给接口变量赋值时传值对象或指针对象都 OK两者都包含值方法集若只有指针方法集匹配接口如只有指针接收器方法只能传指针对象给接口变量传值对象会在编译期报错触发接口合理性检查。规范示例interface-receiver.mdtype F interface { f() } type S1 struct{} func (s S1) f() {} // 值接收器 type S2 struct{} func (s *S2) f() {} // 指针接收器 s1Val : S1{} s1Ptr : S1{} s2Val : S2{} s2Ptr : S2{} var i F i s1Val // OKS1 的值方法集匹配 F i s1Ptr // OK*S1 也包含值方法集 i s2Ptr // OK*S2 的指针方法集匹配 F // i s2Val // 编译错误S2 值方法集为空不匹配 F这也呼应了前面反例中test(*m)编译失败的真正原因——mystruct只有指针接收器方法值方法集不匹配接口。关联规则编译期接口合理性验证另一个与接口指针问题相伴而生的实践是编译期验证接口实现详见 interface-compliance.md。它常与接口以值传递配合使用用来在编译期锁定 API 契约type Handler struct { // ... } var _ http.Handler (*Handler)(nil) // 编译期验证 *Handler 实现 http.Handler func (h *Handler) ServeHTTP( w http.ResponseWriter, r *http.Request, ) { // ... }要点若*Handler与http.Handler不匹配var _ http.Handler (*Handler)(nil)会直接编译失败将运行时错误提前到编译期赋值右侧应是断言类型的零值指针、切片、映射用nil结构体用空结构体如var _ http.Handler LogHandler{}注意此处(*Handler)(nil)是把*Handler的零值nil 指针装箱进接口与指向接口的指针无关——它验证的是*Handler类型本身满足接口。实践清单与结论综合 interface-pointer.md、interface-receiver.md 与 interface-compliance.md可总结出四条可直接落地的编码准则接口参数一律按值传递函数签名写func test(value myinterface)不要写func test(value *myinterface)需要修改底层数据时传对象指针var f F S2{}让接口内部直接持有指针记住方法集规则只有指针接收器的类型其值类型无法满足接口接口变量赋值传值对象会编译报错用var _ SomeInterface (*T)(nil)做编译期契约校验把接口实现错误扼杀在编译期。Uber 规范的本质是接口是 Go 语言中天然的间接引用把接口当值传把实现当指针用二者结合既保证了代码简洁又保留了修改底层数据的灵活性。赞分享文档【免费下载链接】uber_go_guide_cnUber Go 语言编码规范中文版. The Uber Go Style Guide .项目地址https://gitcode.com/gh_mirrors/ub/uber_go_guide_cn点击查看免费下载相关推荐Uber Go 风格指南为什么你几乎从不需要接口指针Pointers to InterfacesUber Go 风格指南为什么你几乎从不需要接口指针Pointers to Interfaces 导读 本文基于 Uber Go Style Gui文档教程代码质量LintOpenCore Legacy Patcher深度解析让老Mac焕发新生的终极方案OpenCore Legacy Patcher深度解析让老Mac焕发新生的终极方案 OpenCore Legacy Patcher是一款革命性的开源工具专门操作系统固件驱动开发Uber Go 编码规范解读接收器Receiver与接口Interface的正确使用姿势Uber Go 编码规范解读接收器Receiver与接口Interface的正确使用姿势 导读 本文聚焦于 Uber Go 编码规范 uber_go文档上一篇html-loader实战案例处理CDN资源与服务器相对路径下一篇embassy-stm32 与 RTIC 协同开发指南STM32L4 家族示例运行与源码解析创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
返回列表