用 Go 的 method 实现面向对象编程:接收者、自定义类型、继承与重写(build-web-application-with-golang 第 2.5 节实战解读)
文档教程【免费下载链接】build-web-application-with-golangA golang ebook intro how to build a web with golang项目地址https://gitcode.com/gh_mirrors/bu/build-web-application-with-golang点击查看免费下载面向对象编程是构建 Web 应用时组织业务逻辑的重要方式而 Go 语言实现面向对象的核心机制正是本节的主角——带接收者receiver的method。本文基于《Build Web Application with Golang》开源电子书 tr/02.5.md 的完整内容展开先展示普通函数处理结构体时的局限再系统讲解 method 的语法、值/指针接收者的区别、任意自定义类型的方法、方法继承与重写并结合仓库 en/code/src/apps/ch.2.5 下可运行的示例源码逐一佐证。读完本文你将能熟练地为 struct 与自定义类型编写方法并用「首字母大小写」控制公开/私有可见性写出具备面向对象风格的 Go 代码。method绑定到类型的函数前面两节分别讲解了函数和struct但你是否想过把函数作为 struct 的「属主」来使用普通函数与结构体是相互独立的两个东西要计算一个矩形的面积通常只能把矩形作为参数传入一个外部函数例如仓库中的示例 pass_struct_to_method/main.gopackage main import fmt type Rectangle struct { width, height float64 } func area(r Rectangle) float64 { return r.width * r.height } func main() { r1 : Rectangle{12, 2} r2 : Rectangle{9, 4} fmt.Println(Area of r1 is: , area(r1)) fmt.Println(Area of r2 is: , area(r2)) }这段代码虽然能算出面积但area只是游离在外的函数并不是Rectangle的「方法」类似经典面向对象语言里的类方法。一旦还要计算圆、正方形、五边形等更多图形的面积就不得不添加一堆名字极其相似的独立函数函数与 struct 之间变成完全割裂的关系代码逐渐失控图普通函数把 struct 当作参数传入二者彼此独立对应原文档 Figure 2.8显然面积这种能力本质上应该是圆或矩形的属性。这正是method登场的原因——method 是与某个类型绑定的函数。它的语法和函数几乎一样唯一区别是在func关键字之后多了一个名为接收者receiver的参数这个接收者就是方法的主体func (r ReceiverType) funcName(parameters) (results)正如 Rob Pike 所言A method is a function with an implicit first argument, called a receiver.方法就是带有一个隐式第一参数——接收者——的函数。接收者就是方法的隐式第一参数理解这一点后后续的值/指针传递行为就很容易推导了。用 method 重写面积计算把前面的普通函数示例改造成 method代码立即变得内聚height、width、Area()全部归属于Rectangle同理圆的radius与Area()归属于Circle。仓库中的可运行版本见 attach_methods_to_struct/main.gopackage main import ( fmt math ) type Circle struct { radius float64 } type Rectangle struct { width, height float64 } // method func (c Circle) Area() float64 { return c.radius * c.radius * math.Pi } // method func (r Rectangle) Area() float64 { return r.width * r.height } func main() { c1 : Circle{10} c2 : Circle{25} r1 : Rectangle{9, 4} r2 : Rectangle{12, 2} fmt.Println(Area of c1 is: , c1.Area()) fmt.Println(Area of c2 is: , c2.Area()) fmt.Println(Area of r1 is: , r1.Area()) fmt.Println(Area of r2 is: , r2.Area()) }从源码结构看Rectangle和Circle各自定义了同名方法Area却互不干扰——这正是 Go 以接收者区分同名方法的体现图area方法分别绑定到Rect与Circle接收者不同则方法不同对应原文档 Figure 2.9使用 method 时有几条要点方法同名但接收者不同则它们不是同一个方法。Rectangle.Area()与Circle.Area()是两个不同的方法各自计算各自类型的面积。方法可以访问接收者内部的字段。r.width、c.radius都能在方法体内直接读写。调用 struct 的方法和访问字段一样使用.运算符例如c1.Area()。值接收者与指针接收者原文档中有一处容易被忽略但非常重要的细节以虚线标注的方法意味着接收者是按值传递而非按引用传递。两者差异在于接收者按引用指针传递时方法可以直接修改接收者的字段值接收者按值传递时方法拿到的是接收者的一个副本修改不会影响原对象。因此凡是想在方法内「改变对象状态」的场景都应使用指针接收者。以Box示例box_example/main.go中的SetColor为例// method with a pointer receiver func (b *Box) SetColor(c Color) { b.color c }这里接收者是*BoxBox 的指针。之所以必须用指针是因为SetColor要修改Box的颜色——如果按值接收改的只是 Box 副本里的字段。把接收者看作方法的第一参数这个行为就顺理成章了传指针才能写回原对象传值只能得到一份拷贝。Go 在这里表现得相当智能在SetColor方法体内直接写b.color c即可无需手动解引用写(*b).color c因为 Go 知道如何自动解释对指针字段的赋值在PaintItBlack中调用bl[i].SetColor(BLACK)即可也无需手动写(bl[i]).SetColor(BLACK)因为 Go 知道SetColor需要指针接收者并会自动取地址。这两种写法等价Go 都能正确解读这让代码更简洁也更贴近「面向对象」的直觉。接收者不限于 struct自定义类型与方法接收者只能是 struct 吗当然不是。任何类型都可以成为方法的接收者。struct只是自定义类型的一种特例Go 允许你用下面的格式定义更多自定义类型type typeName typeLiteral例如type age int type money float32 type months map[string]int m : months{ January: 31, February: 28, // ... December: 31, }这有点类似于 C 语言中的typedef上面用age替代int、用money替代float32months则是map[string]int的别名。自定义类型同样可以挂载任意多个方法。原文档用「箱子管理」这个贴近实战的完整案例展示了自定义类型上挂载多个方法的威力仓库可运行版本见 box_example/main.gopackage main import fmt const ( WHITE iota BLACK BLUE RED YELLOW ) type Color byte type Box struct { width, height, depth float64 color Color } type BoxList []Box // a slice of boxes func (b Box) Volume() float64 { return b.width * b.height * b.depth } func (b *Box) SetColor(c Color) { b.color c } func (bl BoxList) BiggestsColor() Color { v : 0.00 k : Color(WHITE) for _, b : range bl { if b.Volume() v { v b.Volume() k b.color } } return k } func (bl BoxList) PaintItBlack() { for i, _ : range bl { bl[i].SetColor(BLACK) } } func (c Color) String() string { strings : []string{WHITE, BLACK, BLUE, RED, YELLOW} return strings[c] } func main() { boxes : BoxList{ Box{4, 4, 4, RED}, Box{10, 10, 1, YELLOW}, Box{1, 1, 20, BLACK}, Box{10, 10, 1, BLUE}, Box{10, 30, 1, WHITE}, Box{20, 20, 20, YELLOW}, } fmt.Printf(We have %d boxes in our set\n, len(boxes)) fmt.Println(The volume of the first one is, boxes[0].Volume(), cm³) fmt.Println(The color of the last one is, boxes[len(boxes)-1].color.String()) fmt.Println(The biggest one is, boxes.BiggestsColor().String()) // Lets paint them all black boxes.PaintItBlack() fmt.Println(The color of the second one is, boxes[1].color.String()) fmt.Println(Obviously, now, the biggest one is, boxes.BiggestsColor().String()) }这个示例先定义了一组常量WHITE、BLACK、BLUE、RED、YELLOW用iota自动递增赋值和三个自定义类型Colorbyte的别名用来表示颜色Box包含width、height、depth长宽高和color字段的 structBoxList以Box为元素的切片类型[]Box。随后为这些自定义类型分别定义了方法Volume()以Box为接收者返回箱子的体积长×宽×高SetColor(c Color)以*Box为接收者修改箱子的颜色BiggestsColor()以BoxList为接收者遍历全部箱子并返回体积最大的那个箱子的颜色PaintItBlack()以BoxList为接收者把列表中所有箱子的颜色设为BLACKString()以Color为接收者把颜色值转换为 WHITE、BLACK 等名称字符串。注意几个有意思的细节Color只有byte一个字节却能挂载String()方法说明任何自定义类型都可以有方法哪怕它底层只是一个基本类型BoxList是切片类型同样可以作为接收者方法体内用for _, b : range bl遍历每个BoxBoxList的方法内部还可以调用Box的方法如b.Volume()、bl[i].SetColor(BLACK)方法之间可以自由组合常量用iota生成WHITE iota值为 0之后每行自动 1所以BLACK1、BLUE2、RED3、YELLOW4恰好可以作为Color字节值和String()方法中字符串数组的下标。把需求「翻译」成方法名Volume、SetColor、BiggestsColor、PaintItBlack、String之后主函数里fmt.Println(The biggest one is, boxes.BiggestsColor().String())这样的调用几乎就是在读一句自然语言——这正是用方法组织代码带来的可读性提升。方法继承匿名字段提升方法上一节tr/02.4.md介绍了字段的继承嵌入struct 中嵌入的匿名字段其字段会被提升到外层 struct。Go 的方法同样支持继承——如果匿名字段带有方法那么包含该字段的 struct 也会拥有这些方法。仓库中的可运行版本见 embedded_method/main.gopackage main import fmt type Human struct { name string age int phone string } type Student struct { Human // anonymous field school string } type Employee struct { Human company string } // define a method in Human func (h *Human) SayHi() { fmt.Printf(Hi, I am %s you can call me on %s\n, h.name, h.phone) } func main() { sam : Employee{Human{Sam, 45, 111-888-XXXX}, Golang Inc} mark : Student{Human{Mark, 25, 222-222-YYYY}, MIT} sam.SayHi() mark.SayHi() }Student和Employee都匿名嵌入了Human因此它们都自动「继承」了定义在*Human上的SayHi()方法。主函数中sam.SayHi()与mark.SayHi()都能正常工作即使sam和mark的类型分别是Employee与Student。这正是面向对象中「继承」在 Go 中的体现字段继承与方法继承一起构成了 Go 版面向对象的基础。方法重写Method Overriding如果希望Employee拥有自己的SayHi版本——例如自我介绍时要带上所在公司——只需在Employee上定义同名方法即可。调用时外层类型的方法会**隐藏覆盖**内嵌类型中同名的方法。仓库中的可运行版本见 method_overload/main.gopackage main import fmt type Human struct { name string age int phone string } type Student struct { Human school string } type Employee struct { Human company string } func (h *Human) SayHi() { fmt.Printf(Hi, I am %s you can call me on %s\n, h.name, h.phone) } func (e *Employee) SayHi() { fmt.Printf(Hi, I am %s, I work at %s. Call me on %s\n, e.name, e.company, e.phone) //Yes you can split into 2 lines here. } func main() { sam : Employee{Human{Sam, 45, 111-888-XXXX}, Golang Inc} mark : Student{Human{Mark, 25, 222-222-YYYY}, MIT} sam.SayHi() mark.SayHi() }运行结果中sam.SayHi()输出的是Employee版本包含公司信息而mark.SayHi()仍走Human版本。由此可以看出 Go 的方法解析规则外层 struct 的同名方法优先于内嵌类型的方法这与上一节「外层字段覆盖内嵌字段」的规则完全一致。这里还有一个值得注意的细节方法体内可以像上面那样把fmt.Printf的调用拆成两行书写Go 允许在参数列表内部换行。可见性控制与小结现在你已经可以写出面向对象的 Go 程序了。与包级函数一样方法名首字母大写即为公开public可被其他包调用首字母小写即为私有private仅包内可见。例如原文档正文中Area()、Volume()首字母大写而仓库 attach_methods_to_struct/main.go 里的area()首字母小写——同样的逻辑导出与否完全取决于首字母大小写这与字段的可见性规则一致。回顾本节的核心要点method 是绑定到类型的函数语法为func (r ReceiverType) funcName(parameters) (results)接收者相当于隐式的第一参数接收者可以是任何自定义类型不限于 struct——typeName typeLiteral定义的别名类型同样可以挂载方法值接收者修改的是副本指针接收者才能修改原对象写b.color c与(*b).color c、bl[i].SetColor(BLACK)与(bl[i]).SetColor(BLACK)等价Go 会自动解释匿名嵌入字段会带来方法继承包含匿名字段的 struct 自动拥有该字段类型上的全部方法在外层类型定义同名方法即可重写/隐藏内嵌方法调用时外层方法优先首字母大写/小写决定方法的公开/私有可见性。下一节将介绍interface它是 Go 实现多态与「面向接口编程」的关键而本节的方法与接收者正是 interface 得以成立的基础——你可以沿着 tr/02.6.md 继续学习。本节所有示例源码均可在仓库 en/code/src/apps/ch.2.5 目录下找到并直接运行例如cd en/code/src/apps/ch.2.5/box_example go run main.go建议动手运行一遍观察输出以加深对值/指针接收者行为差异的理解。赞分享文档教程【免费下载链接】build-web-application-with-golangA golang ebook intro how to build a web with golang项目地址https://gitcode.com/gh_mirrors/bu/build-web-application-with-golang点击查看免费下载相关推荐Go 面向对象编程method 接收者、指针 receiver 与继承重写全解析 —— build-web-application-with-golang 第 2.5 章实战Go 面向对象编程method 接收者、指针 receiver 与继承重写全解析 —— build web application with golang 第文档教程Go 语言面向对象编程方法Method、接收者与类型体系实战指南 —— build-web-application-with-golang 第 2.5 节精讲Go 语言面向对象编程方法Method、接收者与类型体系实战指南 —— build web application with golang 第 2.5 节文档教程Go 语言面向对象编程方法Method与接收者Receiver完全指南 —— build-web-application-with-golang 第 2.5 节精读Go 语言面向对象编程方法Method与接收者Receiver完全指南 —— build web application with golang 第文档教程创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考