- 文档
- 教程
【免费下载链接】build-web-application-with-golang
A golang ebook intro how to build a web 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()首字母小写——同样的逻辑,导出与否完全取决于首字母大小写,这与字段的可见性规则一致。
回顾本节的核心要点:
- 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-golang
A golang ebook intro how to build a web 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),仅供参考