Golang 面向对象深入理解

1 封装

Java 中封装是基于类(Class),Golang 中封装是基于结构体(struct)

Golang 的开发中经常直接将成员变量设置为大写使用,当然这样使用并不符合面向对象封装的思想。

Golang 没有构造函数,但有一些约定俗成的方式:

  1. 提供 NewStruct(s Struct) *Struct 这样的函数
  2. 提供 (s *Struct) New() 这样的方法
  3. 也可以直接用传统的 new(struct) 或 Struct{} 来初始化,随后用 Set 方法对成员变量赋值
golang 复制代码
type People struct {
	name string
	age  int
}

func (p *People) SetName(name string) {
	p.name = name
}

func (p *People) GetName() string {
	return p.name
}

func (p *People) SetAge(age int) {
	p.age = age
}

func (p *People) GetAge() int {
	return p.age
}

func main() {
	peo := new(People)
	peo.SetName("张三")
	peo.SetAge(13)
	fmt.Println(peo.GetName(), peo.GetAge())
    // 张三 13
}

2 继承 or 组合

Golang 不支持继承,支持组合,算是"组合优于继承"思想的体现。

虽然不支持继承,但 Golang 的匿名组合组合可以实现面向对象继承的特性。

2.1 非匿名组合和匿名组合

组合分为非匿名组合匿名组合

非匿名组合不能直接使用内嵌结构体的方法,需要通过内嵌结构体的变量名,间接调用内嵌结构体的方法。

匿名组合可以直接使用内嵌结构体的方法,如果有多个内嵌结构体,可以直接使用所有内嵌结构体的方法。

非匿名组合实例:

golang 复制代码
type People struct {
	name string
	age  int
}

func (p *People) SetName(name string) {
	p.name = name
}

func (p *People) GetName() string {
	return p.name
}

func (p *People) SetAge(age int) {
	p.age = age
}

func (p *People) GetAge() int {
	return p.age
}

type Student struct {
	people People
	grade  string
}

func (s *Student) SetGrade(grade string) {
	s.grade = grade
}

func (s *Student) GetGrade() string {
	return s.grade
}

func main() {
	stu := Student{}
	stu.people.SetName("张三")
	stu.people.SetAge(13)
	stu.SetGrade("七年级")
	fmt.Println(stu.people.GetName(), stu.people.GetAge(), stu.GetGrade())
	// 张三 13 七年级
}

非匿名组合主要体现在 Student 结构体中对 People 的组合需要明确命名。

匿名组合实例:

golang 复制代码
type People struct {
	name string
	age  int
}

func (p *People) SetName(name string) {
	p.name = name
}

func (p *People) GetName() string {
	return p.name
}

func (p *People) SetAge(age int) {
	p.age = age
}

func (p *People) GetAge() int {
	return p.age
}

type Student struct {
	People
	grade  string
}

func (s *Student) SetGrade(grade string) {
	s.grade = grade
}

func (s *Student) GetGrade() string {
	return s.grade
}

func (s *Student) GetName() string {
	return fmt.Sprintf("student-%s",s.People.GetName())
}

func main() {
	stu := Student{}
	stu.SetName("张三")
	stu.SetAge(13)
	stu.SetGrade("七年级")
	fmt.Println(stu.GetName(), stu.GetAge(), stu.GetGrade())
	// student-张三 13 七年级
}

从上面实例可以看出,匿名组合中:

  1. Student 中的 People 没有显式命名
  2. Student 可以直接使用 People 的方法
  3. 注意 StudentGetName 方法,与 People 中的 GetName 方法重复,可以看做是面相对象编程中的重写(Overriding)
  4. StudentGetName 方法中使用 s.People.GetName() 调用People 中的 GetName 方法,这是对匿名组合的显式调用,类似 Java 中的 super 用法

可以看出,匿名组合的使用感官上类似面向对象编程的继承,可以说是一种『伪继承』的实现,但匿名组合并不是继承!

2.2 组合的使用方式

2.2.1 结构体中内嵌结构体

上面实例所用的就是结构体中内嵌结构体,不再多说。

2.2.2 结构体中内嵌接口

内嵌接口如下面例子:

golang 复制代码
type Member interface {
	SayHello() // 问候语
	DoWork()   // 开始工作
	SitDown()  // 坐下
}

type Normal struct{}

func (n *Normal) SayHello() {
	fmt.Println("normal:", "大家好!")
}

func (n *Normal) DoWork() {
	fmt.Println("normal:", "记笔记")
}

func (n *Normal) SitDown() {
	fmt.Println("normal:", "坐下")
}

type People struct {
	name string
	age  int
}

func (p *People) SetName(name string) {
	p.name = name
}

func (p *People) GetName() string {
	return p.name
}

func (p *People) SetAge(age int) {
	p.age = age
}

func (p *People) GetAge() int {
	return p.age
}

type Student struct {
	Member
	People
	grade string
}

func (s *Student) SetGrade(grade string) {
	s.grade = grade
}

func (s *Student) GetGrade() string {
	return s.grade
}

func (s *Student) SayHello() {
	fmt.Println("student:", s.name, "说: 老师好!")
}

type Teacher struct {
	Member
	People
	subject string
}

func (t *Teacher) SetSubject(subject string) {
	t.subject = subject
}

func (t *Teacher) GetSubject() string {
	return t.subject
}

func (t *Teacher) SayHello() {
	fmt.Println("teacher", t.name, "说: 同学们好!")
}

func (t *Teacher) DoWork() {
	fmt.Println("teacher", t.name, "讲课!")
}

func main() {
	stu := &Student{Member: &Normal{}}
	stu.SetName("张三")
	stu.SetAge(13)
	stu.SetGrade("七年级")

	tea := &Teacher{Member: &Normal{}}
	tea.SetName("李四")
	tea.SetAge(31)
	tea.SetSubject("语文")

	var member Member
	member = stu
	member.SayHello()
	member.SitDown()
	member.DoWork()
	// student: 张三 说: 老师好!
	// normal: 坐下
	// normal: 记笔记

	member = tea
	member.SayHello()
	member.SitDown()
	member.DoWork()
	// teacher 李四 说: 同学们好!
	// normal: 坐下
	// teacher 李四 讲课!
}

从上面例子可以看出:

  1. TeacherStudent 结构体都没有完全实现 Member 的方法
  2. Normal 结构体实现了 Member 方法
  3. TeacherStudent 初始化时注入了 Normal 结构
  4. TeacherStudent 可以作为 Member 类型的接口使用,并默认使用 Normal 的实现。除非 TeacherStudent 有自己的实现。

TeacherStudent 并非没有实现 Member 接口。编译器自动为类型 *Teacher*Student 实现了 Member 中定义的方法,类似:

golang 复制代码
func (t *Teacher) SitDown() {  
    t.Member.SitDown()  
}

所以即使在初始化时没有指定 NormalTeacherStudent 也可以赋值给 Member 类型的变量。但调用未实现的 Member 的方法时会报 panic

官方实践

可以参考 sort.Reverse 方法,reverse 结构体内嵌了 Interface 接口,并只实现了 Less 方法

golang 复制代码
type IntSlice []int

func (x IntSlice) Len() int           { return len(x) }
func (x IntSlice) Less(i, j int) bool { return x[i] < x[j] }
func (x IntSlice) Swap(i, j int)      { x[i], x[j] = x[j], x[i] }

type Interface interface {
    // 长度
	Len() int
    // 对比两个数 i,j,返回结果作为排序的依据
	Less(i, j int) bool
    // 交换两个数 i,j
	Swap(i, j int)
}

type reverse struct {
	Interface
}

func (r reverse) Less(i, j int) bool {
	return r.Interface.Less(j, i)
}

func Reverse(data Interface) Interface {
	return &reverse{data}
}

使用时:

golang 复制代码
lst := []int{4, 5, 2, 8, 1, 9, 3}
sort.Sort(sort.Reverse(sort.IntSlice(lst)))
fmt.Println(lst)
// 打印:[9 8 5 4 3 2 1]

可以看出,Reverse 就是用内嵌接口的方式,接收一个 Interface 接口,将 Less 方法的两个参数反转了。

2.2.3 接口中内嵌接口

是针对方法的组合。如 Golang 的 ReadWriter 接口就是 Reader 和 Writer 接口的组合。

golang 复制代码
type Reader interface {
	Read(p []byte) (n int, err error)
}

type Writer interface {
	Write(p []byte) (n int, err error)
}

// ReadWriter is the interface that groups the basic Read and Write methods.
type ReadWriter interface {
	Reader
	Writer
}

3 多态

多态的定义比较宽松:指一个行为具有多种不同的表现形式。

本质上多态分两种:编译时多态(静态)和运行时多态(动态)

  1. 编译时多态在编译期间,多态就已经确定。重载是编译时多态的一个例子。
  2. 运行时多态在编译时不确定调用哪个具体方法,一直延迟到运行时才能确定。

通常情况下,我们讨论的多态都是运行时多态。Golang 接口就是基于动态绑定实现的多态。

由于 Golang 结构体是『组合』而非『继承』,不能相互转换,所以只有基于接口的多态。

3.1 向上转型

向上转型是实现多态的必要条件。即用父类的引用指向一个子类对象,通过父类引用调用方法时会调用子类的方法。通过父类引用无法调用子类的特有方法,需要『向下转型』。

golang 复制代码
type Member interface {
	SayHello() // 问候语
	DoWork()   // 开始工作
	SitDown()  // 坐下
}

type People struct {
	name string
	age  int
}

func (p *People) SetName(name string) {
	p.name = name
}

func (p *People) GetName() string {
	return p.name
}

func (p *People) SetAge(age int) {
	p.age = age
}

func (p *People) GetAge() int {
	return p.age
}

type Student struct {
	People
	grade string
}

func (s *Student) SetGrade(grade string) {
	s.grade = grade
}

func (s *Student) GetGrade() string {
	return s.grade
}

func (s *Student) SayHello() {
	fmt.Println("student:", s.name, "说: 老师好!")
}

func (s *Student) DoWork() {
	fmt.Println("student:", s.name, "记笔记")
}

func (s *Student) SitDown() {
	fmt.Println("student:", s.name, "坐下")
}

type Teacher struct {
	People
	subject string
}

func (t *Teacher) SetSubject(subject string) {
	t.subject = subject
}

func (t *Teacher) GetSubject() string {
	return t.subject
}

func (t *Teacher) SayHello() {
	fmt.Println("teacher", t.name, "说: 同学们好!")
}

func (t *Teacher) DoWork() {
	fmt.Println("teacher", t.name, "讲课!")
}

func (t *Teacher) SitDown() {
	fmt.Println("teacher:", t.name, "站着讲课,不能坐下!")
}

func main() {
	stu := &Student{}
	stu.SetName("张三")
	stu.SetAge(13)
	stu.SetGrade("七年级")

	tea := &Teacher{}
	tea.SetName("李四")
	tea.SetAge(31)
	tea.SetSubject("语文")

	var member Member
	member = stu
	member.SayHello()
	member.SitDown()
	member.DoWork()
	// student: 张三 说: 老师好!
	// student: 张三 坐下
	// student: 张三 记笔记

	member = tea
	member.SayHello()
	member.SitDown()
	member.DoWork()
	// teacher 李四 说: 同学们好!
	// teacher: 李四 站着讲课,不能坐下!
	// teacher 李四 讲课!
}

这里是基于接口实现的『向下转型』,没有父类、子类之分。而在 Java 中,当子类没有重写父类方法时,父类的引用会调用到父类的方法里。其实也有类似的实现,在上面已经有了,即[2.2.2 结构体中内嵌接口](#2.2.2 结构体中内嵌接口)

3.2 向下转型

上面提到,用父类的引用指向一个子类对象,通过父类引用无法调用子类的特有方法。但在某些情况下需要调用子类的特有方法,例如子类有一些特殊逻辑需要处理,这时就需要『向下转型』还原出子类的引用。

在 Java 里,通常用 User user = (User) people; 来向下转型。

Golang 向下转型通过类型断言。基本用法是t,ok := intefaceValue.(T)

一个例子:

golang 复制代码
type Member interface {
	SayHello() // 问候语
}

type People struct {
	name string
	age  int
}

func (p *People) SetName(name string) {
	p.name = name
}

func (p *People) GetName() string {
	return p.name
}

func (p *People) SetAge(age int) {
	p.age = age
}

func (p *People) GetAge() int {
	return p.age
}

type Student struct {
	People
	grade string
}

func (s *Student) SetGrade(grade string) {
	s.grade = grade
}

func (s *Student) GetGrade() string {
	return s.grade
}

func (s *Student) SayHello() {
	fmt.Println("student:", s.name, "说: 老师好!")
}

func main() {
	stu := &Student{}
	stu.SetName("张三")
	stu.SetAge(13)
	stu.SetGrade("七年级")

	var member Member = stu
	member.SayHello()
	// student: 张三 说: 老师好!
	if student, ok := member.(*Student); ok {
		student.SetName("王五")
		student.SayHello()
		// student: 王五 说: 老师好!
	}
}
  1. 使用 Member 类型的引用,指向一个 Student 对象
  2. 想对 Student 对象设置一个新名称,使用类型断言向下转型,可以调用 StudentSetName 方法(严格的说,是 PeopleSetName 方法)

除了上面场景,还有一种场景需要类型断言:当有一个函数,其参数是 interface{} 类型时:

golang 复制代码
func SayHello(inter interface{}) {
	if member, ok := inter.(Member); ok {
		member.SayHello()
	}
}

func main() {
	stu := &Student{}
	stu.SetName("张三")
	stu.SetAge(13)
	stu.SetGrade("七年级")

	SayHello(stu)
	// student: 张三 说: 老师好!
}

补充:除了类型断言,Golang 还有一种 type-switch 的方式可以用于对 interface 的类型探测,从而进行不同逻辑的处理。

4 参考资料

  1. Golang 中 struct 嵌入 interface
  2. 面向对象思考与 golang cobra 库实现原理
  3. 多态中的向上转型与向下转型
相关推荐
源代码•宸7 小时前
Leetcode—404. 左叶子之和【简单】
经验分享·后端·算法·leetcode·职场和发展·golang·dfs
Grassto10 小时前
10 Go 是如何下载第三方包的?GOPROXY 与源码解析
后端·golang·go·go module
源代码•宸10 小时前
Leetcode—513. 找树左下角的值【中等】
经验分享·算法·leetcode·面试·职场和发展·golang·dfs
bing.shao11 小时前
文心大模型 5.0 正式版上线:用 Golang 解锁全模态 AI 工业化落地新路径
人工智能·golang·dubbo
lanbing11 小时前
在Mac OS系统中安装Go语言环境教程
开发语言·后端·golang
无心水12 小时前
17、Go协程通关秘籍:主协程等待+多协程顺序执行实战解析
开发语言·前端·后端·算法·golang·go·2025博客之星评选投票
洛克大航海12 小时前
Python面向对象
开发语言·python·面向对象
源代码•宸14 小时前
GoLang八股(Go并发)
服务器·面试·golang·cap·gmp·三色标记法·混合写屏障
源代码•宸14 小时前
Golang原理剖析(彻底理解Go语言栈内存/堆内存、Go内存管理)
经验分享·后端·算法·面试·golang·span·mheap
沐雨风栉1 天前
用 Kavita+cpolar 把数字书房装进口袋
服务器·开发语言·数据库·后端·golang