news 2026/10/8 18:19:42

用 Go 的 method 实现面向对象编程:接收者、自定义类型、继承与重写(build-web-application-with-golang 第 2.5 节实战解读)

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
用 Go 的 method 实现面向对象编程:接收者、自定义类型、继承与重写(build-web-application-with-golang 第 2.5 节实战解读)
  • 文档
  • 教程

【免费下载链接】build-web-application-with-golang

A 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.go:

package 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.go:

package 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 }

这里接收者是*Box(Box 的指针)。之所以必须用指针,是因为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替代float32,months则是map[string]int的别名。自定义类型同样可以挂载任意多个方法。

原文档用「箱子管理」这个贴近实战的完整案例展示了自定义类型上挂载多个方法的威力,仓库可运行版本见 box_example/main.go:

package 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()) // Let's 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自动递增赋值)和三个自定义类型:

  • Color:byte的别名,用来表示颜色;
  • Box:包含width、height、depth(长宽高)和color字段的 struct;
  • BoxList:以Box为元素的切片类型([]Box)。

随后为这些自定义类型分别定义了方法:

  • Volume():以Box为接收者,返回箱子的体积(长×宽×高);
  • SetColor(c Color):以*Box为接收者,修改箱子的颜色;
  • BiggestsColor():以BoxList为接收者,遍历全部箱子并返回体积最大的那个箱子的颜色;
  • PaintItBlack():以BoxList为接收者,把列表中所有箱子的颜色设为BLACK;
  • String():以Color为接收者,把颜色值转换为 "WHITE"、"BLACK" 等名称字符串。

注意几个有意思的细节:

  • Color只有byte一个字节,却能挂载String()方法,说明任何自定义类型都可以有方法,哪怕它底层只是一个基本类型;
  • BoxList是切片类型,同样可以作为接收者,方法体内用for _, b := range bl遍历每个Box;
  • BoxList的方法内部还可以调用Box的方法(如b.Volume()、bl[i].SetColor(BLACK)),方法之间可以自由组合;
  • 常量用iota生成:WHITE = iota(值为 0)之后每行自动 +1,所以BLACK=1、BLUE=2、RED=3、YELLOW=4,恰好可以作为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.go:

package 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.go:

package 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()首字母小写——同样的逻辑,导出与否完全取决于首字母大小写,这与字段的可见性规则一致。

回顾本节的核心要点:

  1. method 是绑定到类型的函数,语法为func (r ReceiverType) funcName(parameters) (results),接收者相当于隐式的第一参数;
  2. 接收者可以是任何自定义类型,不限于 struct——typeName typeLiteral定义的别名类型同样可以挂载方法;
  3. 值接收者修改的是副本,指针接收者才能修改原对象;写b.color = c与(*b).color = c、bl[i].SetColor(BLACK)与(&bl[i]).SetColor(BLACK)等价,Go 会自动解释;
  4. 匿名嵌入字段会带来方法继承:包含匿名字段的 struct 自动拥有该字段类型上的全部方法;
  5. 在外层类型定义同名方法即可重写/隐藏内嵌方法,调用时外层方法优先;
  6. 首字母大写/小写决定方法的公开/私有可见性。

下一节将介绍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-golang

A golang ebook intro how to build a web with golang

项目地址:https://gitcode.com/gh_mirrors/bu/build-web-application-with-golang
点击查看免费下载

相关推荐

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/8 18:19:14

FDE实战:用Agent Harness、RAG与MCP串起AI应用全流程

从去年开始,我身边越来越多同事的title里出现了FDE三个字母。有人以为这是前端工程师(Front-End Developer)的缩写,也有人觉得是某个新职级。其实在我做的这条业务线里,FDE指的是Feature/Full-cycle Development Engin…

作者头像 李华
网站建设 2026/10/8 18:15:42

Python自动备份脚本实战:数据不丢,代码不慌

前言 数据丢了才想起来备份?服务器宕机、误删文件、数据库清空,这些事故每天都在发生。备份这件事,手动做容易忘,忘了就出事。**让脚本每天自动备份,是最省心的方案。**今天分享一套Python自动备份脚本:数据…

作者头像 李华
网站建设 2026/10/8 18:15:38

基于eFuse与STM32的电源路径保护及监控方案

前阵子做一块工业控制板的电源部分,24V输入要给后级各种负载供电,板卡要求热插拔不损坏、负载短路不烧板、故障能远程上报。用TPS259483AYWPR这颗eFuse加上STM32F302VC来做电源路径保护,算是把嵌入式系统的电源管理这个环节做扎实了。这块方案…

作者头像 李华
网站建设 2026/10/8 18:15:35

基于TPS259483与PIC18F45K80的智能电源路径保护设计

1. 为什么需要专门管电源路径 嵌入式系统和工业控制器里,电源设计往往是被低估的一环。很多人觉得“只要电压对、电流够就行”,可真正到了现场,上电瞬间的浪涌、负载短路、电源反接、甚至一路电源抖动导致整个板卡复位,这些问题远…

作者头像 李华
网站建设 2026/10/8 18:13:34

Claude Code 一站式体验:11 个 MCP 服务器赋能 TaoToken 统一接入

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/10/8 18:11:45

多线程测试实战:从竞态条件到死锁排查的完整指南

做后端开发这些年,我接手处理过不少线程应用程序的线上事故:功能测试全绿,一上线高并发就顶不住;代码明明没有死锁,可CPU却莫名飙到百分之百;日志里偶尔冒出一条脏数据,重启后一切正常。写多线程…

作者头像 李华