Go 面向对象编程:从 struct 字段到方法与接收者——基于《Build Web Application with Golang》第二章解读

发布时间:2026/10/6 2:06:15
Go 面向对象编程:从 struct 字段到方法与接收者——基于《Build Web Application with Golang》第二章解读
文档教程【免费下载链接】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点击查看免费下载本文是开源电子书《Build Web Application with Golang》th/02.5.md第二章「Object-oriented」的深度解读。Go 语言没有 class 关键字但通过method方法 receiver接收者机制完整地实现了面向对象的核心能力把行为绑定到类型、封装数据、继承方法与覆盖方法。读完本文你将掌握 Go 方法的完整语法、值接收者与指针接收者的本质区别、自定义类型上的任意方法绑定以及匿名内嵌字段带来的方法继承与方法覆盖并能在实际 Web 项目中像组织类一样组织你的领域模型。从函数到方法为什么需要 receiver在上一节struct中我们学会了用type定义结构体并访问其字段。但函数与结构体是相互独立的要计算一个矩形的面积我们通常会写出一个接收矩形作为参数的普通函数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并不是矩形结构体的成员类似经典面向对象语言中的类方法函数与结构体是两个独立的东西。仓库中对应的完整可运行示例位于 th/code/src/apps/ch.2.5/pass_struct_to_method/main.go。更糟糕的是当需要计算圆、正方形、五边形乃至更多形状的面积时你将被迫不断添加名字高度相似的独立函数形成下图所示的外围函数爆炸这种写法既不优雅也不符合直觉——面积本应是圆或矩形自身的一个属性。方法method正是为解决这个问题而生。什么是方法带隐式首参数的函数Rob Pike 对方法有一段经典定义A method is a function with an implicit first argument, called a receiver. 方法是一个带有隐式第一个参数——接收者——的函数。方法与函数的语法几乎一样唯一的区别在于func关键字之后、函数名之前多了一个被称为receiver接收者的参数它正是该方法所属的主体类型func (r ReceiverType) funcName(parameters) (results)沿用上面的矩形例子把面积计算改写成方法后Rectangle.Area()就直接归属于矩形本身width、height和Area()同属于矩形这一个类型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()) }注意Circle和Rectangle都定义了名为Area的方法但它们拥有不同的接收者因此互不冲突——方法名相同并不代表它们是同一个方法。仓库中对应的可运行示例见 th/code/src/apps/ch.2.5/attach_methods_to_struct/main.go该示例使用小写area方法名其余逻辑与上文一致。使用方法的三个要点方法名相同但接收者不同它们是不同的方法方法可以访问接收者内部的字段调用结构体中的方法与调用字段一样使用.运算符。上图中Area()方法分别归属于Rectangle和Circle接收者分别是Rectangle与Circle。虚线表示的接收者说明接收者默认按值传递。按值传递时方法拿到的是接收者的副本按引用指针传递时方法可以直接修改接收者的值——两者的区别将在下文「使用指针作为接收者」一节详细展开。自定义类型接收者不限于结构体接收者只能是一个 struct 吗当然不是。任何类型都可以作为方法的接收者。你可能对自定义类型感到陌生——struct 只是自定义类型的一种特殊形式还有更多自定义类型。使用如下格式定义自定义类型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的一个别名。有了自定义类型你可以在任意类型上绑定任意数量的方法。综合示例Box、BoxList 与 Color下面的完整示例同时展示了自定义类型、常量、值接收者方法与指针接收者方法的组合用法package main import fmt const ( WHITE iota BLACK BLUE RED YELLOW ) type Box struct { width, height, depth float64 color Color } type Color byte type BoxList []Box //a slice of boxes // method func (b Box) Volume() float64 { return b.width * b.height * b.depth } // method with a pointer receiver func (b *Box) SetColor(c Color) { b.color c } // method 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 } // method func (bl BoxList) PaintItBlack() { for i, _ : range bl { bl[i].SetColor(BLACK) } } // method 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()) }这个程序先定义了一批常量与自定义类型Color是byte的别名自定义类型Box结构体包含width、height、depth和color四个字段BoxList是[]Box切片类型自定义类型。随后为这些自定义类型绑定了五个方法方法接收者作用Volume()Box值返回盒子的体积SetColor(c Color)*Box指针修改盒子的颜色BiggestsColor()BoxList值返回体积最大的盒子的颜色PaintItBlack()BoxList值把列表中所有盒子涂成黑色String()Color值返回颜色名对应的字符串注意Volume()方法中使用了math.Pi之外的基础运算全部代码可在 th/code/src/apps/ch.2.5/box_example/main.go 中查看并运行。用自然语言描述需求计算最大体积盒子的颜色把所有盒子涂黑后再编码代码的可读性与可维护性会大幅提升。使用指针作为接收者仔细观察SetColor方法它的接收者是*Box——Box 的指针。为什么这里要使用指针因为我们需要在这个方法中真正改变 Box 的颜色如果使用值接收者方法只会修改一份 Box 副本的颜色原始数据不会被改变。理解了接收者就是方法的第一个参数指针接收者的行为就顺理成章了。同时Go 提供了便捷的自动解引用能力在SetColor()方法内部写b.Color c与(*b).Color c完全等价Go 编译器能自动解释赋值在PaintItBlack()中调用bl[i].SetColor(BLACK)时写bl[i].SetColor(BLACK)与(bl[i]).SetColor(BLACK)也完全等价Go 同样知道如何解释。选择建议如果方法需要修改接收者的状态或者接收者是较大的结构体避免每次调用都复制一份应使用指针接收者如果方法只读取接收者的数据且结构体较小值接收者更直观。在 Web 项目中常见的模式是控制器Controller或模型Model上的写操作方法一律使用指针接收者读操作使用值接收者。方法继承内嵌字段自动继承方法上一节学习了字段继承struct 的内嵌字段方法同样支持继承如果匿名内嵌字段anonymous field带有方法那么包含该字段的结构体将自动获得这些方法。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()都可以直接调用输出各自的人名与电话。对应源码见 th/code/src/apps/ch.2.5/embedded_method/main.go。方法覆盖同名方法遮蔽内嵌方法如果希望Employee拥有自己的SayHi只需在Employee上定义同名方法调用时它将遮蔽hideHuman中的SayHipackage 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 的方法覆盖Method Overriding。值得注意的是Go 并不像 C/Java 那样支持重载overloading同名同接收者类型的方法只能定义一个覆盖发生在不同接收者类型之间此处*Employee覆盖了内嵌的*Human。对应源码见 th/code/src/apps/ch.2.5/method_overload/main.go。方法可见性首字母大小写即访问控制现在你已经可以用 Go 编写面向对象风格的代码了。最后一点关键规则方法以及字段使用首字母大小写决定其公开或私有——与 Go 包级函数完全一致方法名以大写字母开头如Area、SetColor跨包可见public可被其他包调用方法名以小写字母开头如仓库示例中的area仅包内可见private。在 th/code/src/apps/ch.2.5/attach_methods_to_struct/main.go 中可以看到小写area()方法的实际写法。这一规则同样作用于字段名、类型名与常量名是 Go 语言统一的可见性模型无需public/private关键字。如何运行本章示例本章所有示例代码都收录于仓库的 th/code/src/apps/ch.2.5 目录共五个独立程序pass_struct_to_method/main.go普通函数接收结构体的旧写法attach_methods_to_struct/main.go方法绑定到 Rectangle 与 Circlebox_example/main.goBox / BoxList / Color 综合示例embedded_method/main.go方法继承method_overload/main.go方法覆盖。进入任一示例目录后运行go run main.go即可观察对应输出。本章处于电子书第二章「Go 语言基础」的末尾位置紧接着的 下一节 interface接口 将基于本节的方法机制继续深入正是由于Student与Employee各自拥有SayHi()、Sing()等方法接口才能通过方法集合实现多态——方法的接收者概念是整个 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文档教程面向对象编程用 build-web-application-with-golang 第 2.5 节吃透 Go 语言方法与接收者面向对象编程用 build web application with golang 第 2.5 节吃透 Go 语言方法与接收者 在 Go 语言中面向对象的核文档教程Go 语言面向对象编程方法Method与接收者Receiver完全指南 —— build-web-application-with-golang 第 2.5 节精读Go 语言面向对象编程方法Method与接收者Receiver完全指南 —— build web application with golang 第文档教程上一篇ZMK键盘固件打造无线机械键盘的完整指南下一篇Evolution API混沌工程实践构建高容错消息系统的终极指南创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考