经验首页 前端设计 程序设计 Java相关 移动开发 数据库/运维 软件/图像 大数据/云计算 其他经验
当前位置:技术经验 » 程序设计 » Go语言 » 查看文章
Golang 面向对象深入理解
来源:cnblogs  作者:一流菜鸟  时间:2023/11/1 9:00:51  对本文有异议

1 封装

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

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

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

  1. 提供 NewStruct(s Struct) *Struct 这样的函数
  2. 提供 (s *Struct) New() 这样的方法
  3. 也可以直接用传统的 new(struct) 或 Struct{} 来初始化,随后用 Set 方法对成员变量赋值
  1. type People struct {
  2. name string
  3. age int
  4. }
  5. func (p *People) SetName(name string) {
  6. p.name = name
  7. }
  8. func (p *People) GetName() string {
  9. return p.name
  10. }
  11. func (p *People) SetAge(age int) {
  12. p.age = age
  13. }
  14. func (p *People) GetAge() int {
  15. return p.age
  16. }
  17. func main() {
  18. peo := new(People)
  19. peo.SetName("张三")
  20. peo.SetAge(13)
  21. fmt.Println(peo.GetName(), peo.GetAge())
  22. // 张三 13
  23. }

2 继承 or 组合

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

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

2.1 非匿名组合和匿名组合

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

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

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

非匿名组合实例:

  1. type People struct {
  2. name string
  3. age int
  4. }
  5. func (p *People) SetName(name string) {
  6. p.name = name
  7. }
  8. func (p *People) GetName() string {
  9. return p.name
  10. }
  11. func (p *People) SetAge(age int) {
  12. p.age = age
  13. }
  14. func (p *People) GetAge() int {
  15. return p.age
  16. }
  17. type Student struct {
  18. people People
  19. grade string
  20. }
  21. func (s *Student) SetGrade(grade string) {
  22. s.grade = grade
  23. }
  24. func (s *Student) GetGrade() string {
  25. return s.grade
  26. }
  27. func main() {
  28. stu := Student{}
  29. stu.people.SetName("张三")
  30. stu.people.SetAge(13)
  31. stu.SetGrade("七年级")
  32. fmt.Println(stu.people.GetName(), stu.people.GetAge(), stu.GetGrade())
  33. // 张三 13 七年级
  34. }

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

匿名组合实例:

  1. type People struct {
  2. name string
  3. age int
  4. }
  5. func (p *People) SetName(name string) {
  6. p.name = name
  7. }
  8. func (p *People) GetName() string {
  9. return p.name
  10. }
  11. func (p *People) SetAge(age int) {
  12. p.age = age
  13. }
  14. func (p *People) GetAge() int {
  15. return p.age
  16. }
  17. type Student struct {
  18. People
  19. grade string
  20. }
  21. func (s *Student) SetGrade(grade string) {
  22. s.grade = grade
  23. }
  24. func (s *Student) GetGrade() string {
  25. return s.grade
  26. }
  27. func (s *Student) GetName() string {
  28. return fmt.Sprintf("student-%s",s.People.GetName())
  29. }
  30. func main() {
  31. stu := Student{}
  32. stu.SetName("张三")
  33. stu.SetAge(13)
  34. stu.SetGrade("七年级")
  35. fmt.Println(stu.GetName(), stu.GetAge(), stu.GetGrade())
  36. // student-张三 13 七年级
  37. }

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

  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 结构体中内嵌接口

内嵌接口如下面例子:

  1. type Member interface {
  2. SayHello() // 问候语
  3. DoWork() // 开始工作
  4. SitDown() // 坐下
  5. }
  6. type Normal struct{}
  7. func (n *Normal) SayHello() {
  8. fmt.Println("normal:", "大家好!")
  9. }
  10. func (n *Normal) DoWork() {
  11. fmt.Println("normal:", "记笔记")
  12. }
  13. func (n *Normal) SitDown() {
  14. fmt.Println("normal:", "坐下")
  15. }
  16. type People struct {
  17. name string
  18. age int
  19. }
  20. func (p *People) SetName(name string) {
  21. p.name = name
  22. }
  23. func (p *People) GetName() string {
  24. return p.name
  25. }
  26. func (p *People) SetAge(age int) {
  27. p.age = age
  28. }
  29. func (p *People) GetAge() int {
  30. return p.age
  31. }
  32. type Student struct {
  33. Member
  34. People
  35. grade string
  36. }
  37. func (s *Student) SetGrade(grade string) {
  38. s.grade = grade
  39. }
  40. func (s *Student) GetGrade() string {
  41. return s.grade
  42. }
  43. func (s *Student) SayHello() {
  44. fmt.Println("student:", s.name, "说: 老师好!")
  45. }
  46. type Teacher struct {
  47. Member
  48. People
  49. subject string
  50. }
  51. func (t *Teacher) SetSubject(subject string) {
  52. t.subject = subject
  53. }
  54. func (t *Teacher) GetSubject() string {
  55. return t.subject
  56. }
  57. func (t *Teacher) SayHello() {
  58. fmt.Println("teacher", t.name, "说: 同学们好!")
  59. }
  60. func (t *Teacher) DoWork() {
  61. fmt.Println("teacher", t.name, "讲课!")
  62. }
  63. func main() {
  64. stu := &Student{Member: &Normal{}}
  65. stu.SetName("张三")
  66. stu.SetAge(13)
  67. stu.SetGrade("七年级")
  68. tea := &Teacher{Member: &Normal{}}
  69. tea.SetName("李四")
  70. tea.SetAge(31)
  71. tea.SetSubject("语文")
  72. var member Member
  73. member = stu
  74. member.SayHello()
  75. member.SitDown()
  76. member.DoWork()
  77. // student: 张三 说: 老师好!
  78. // normal: 坐下
  79. // normal: 记笔记
  80. member = tea
  81. member.SayHello()
  82. member.SitDown()
  83. member.DoWork()
  84. // teacher 李四 说: 同学们好!
  85. // normal: 坐下
  86. // teacher 李四 讲课!
  87. }

从上面例子可以看出:

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

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

  1. func (t *Teacher) SitDown() {
  2. t.Member.SitDown()
  3. }

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

官方实践

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

  1. type IntSlice []int
  2. func (x IntSlice) Len() int { return len(x) }
  3. func (x IntSlice) Less(i, j int) bool { return x[i] < x[j] }
  4. func (x IntSlice) Swap(i, j int) { x[i], x[j] = x[j], x[i] }
  5. type Interface interface {
  6. // 长度
  7. Len() int
  8. // 对比两个数 i,j,返回结果作为排序的依据
  9. Less(i, j int) bool
  10. // 交换两个数 i,j
  11. Swap(i, j int)
  12. }
  13. type reverse struct {
  14. Interface
  15. }
  16. func (r reverse) Less(i, j int) bool {
  17. return r.Interface.Less(j, i)
  18. }
  19. func Reverse(data Interface) Interface {
  20. return &reverse{data}
  21. }

使用时:

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

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

2.2.3 接口中内嵌接口

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

  1. type Reader interface {
  2. Read(p []byte) (n int, err error)
  3. }
  4. type Writer interface {
  5. Write(p []byte) (n int, err error)
  6. }
  7. // ReadWriter is the interface that groups the basic Read and Write methods.
  8. type ReadWriter interface {
  9. Reader
  10. Writer
  11. }

3 多态

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

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

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

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

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

3.1 向上转型

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

  1. type Member interface {
  2. SayHello() // 问候语
  3. DoWork() // 开始工作
  4. SitDown() // 坐下
  5. }
  6. type People struct {
  7. name string
  8. age int
  9. }
  10. func (p *People) SetName(name string) {
  11. p.name = name
  12. }
  13. func (p *People) GetName() string {
  14. return p.name
  15. }
  16. func (p *People) SetAge(age int) {
  17. p.age = age
  18. }
  19. func (p *People) GetAge() int {
  20. return p.age
  21. }
  22. type Student struct {
  23. People
  24. grade string
  25. }
  26. func (s *Student) SetGrade(grade string) {
  27. s.grade = grade
  28. }
  29. func (s *Student) GetGrade() string {
  30. return s.grade
  31. }
  32. func (s *Student) SayHello() {
  33. fmt.Println("student:", s.name, "说: 老师好!")
  34. }
  35. func (s *Student) DoWork() {
  36. fmt.Println("student:", s.name, "记笔记")
  37. }
  38. func (s *Student) SitDown() {
  39. fmt.Println("student:", s.name, "坐下")
  40. }
  41. type Teacher struct {
  42. People
  43. subject string
  44. }
  45. func (t *Teacher) SetSubject(subject string) {
  46. t.subject = subject
  47. }
  48. func (t *Teacher) GetSubject() string {
  49. return t.subject
  50. }
  51. func (t *Teacher) SayHello() {
  52. fmt.Println("teacher", t.name, "说: 同学们好!")
  53. }
  54. func (t *Teacher) DoWork() {
  55. fmt.Println("teacher", t.name, "讲课!")
  56. }
  57. func (t *Teacher) SitDown() {
  58. fmt.Println("teacher:", t.name, "站着讲课,不能坐下!")
  59. }
  60. func main() {
  61. stu := &Student{}
  62. stu.SetName("张三")
  63. stu.SetAge(13)
  64. stu.SetGrade("七年级")
  65. tea := &Teacher{}
  66. tea.SetName("李四")
  67. tea.SetAge(31)
  68. tea.SetSubject("语文")
  69. var member Member
  70. member = stu
  71. member.SayHello()
  72. member.SitDown()
  73. member.DoWork()
  74. // student: 张三 说: 老师好!
  75. // student: 张三 坐下
  76. // student: 张三 记笔记
  77. member = tea
  78. member.SayHello()
  79. member.SitDown()
  80. member.DoWork()
  81. // teacher 李四 说: 同学们好!
  82. // teacher: 李四 站着讲课,不能坐下!
  83. // teacher 李四 讲课!
  84. }

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

3.2 向下转型

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

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

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

一个例子:

  1. type Member interface {
  2. SayHello() // 问候语
  3. }
  4. type People struct {
  5. name string
  6. age int
  7. }
  8. func (p *People) SetName(name string) {
  9. p.name = name
  10. }
  11. func (p *People) GetName() string {
  12. return p.name
  13. }
  14. func (p *People) SetAge(age int) {
  15. p.age = age
  16. }
  17. func (p *People) GetAge() int {
  18. return p.age
  19. }
  20. type Student struct {
  21. People
  22. grade string
  23. }
  24. func (s *Student) SetGrade(grade string) {
  25. s.grade = grade
  26. }
  27. func (s *Student) GetGrade() string {
  28. return s.grade
  29. }
  30. func (s *Student) SayHello() {
  31. fmt.Println("student:", s.name, "说: 老师好!")
  32. }
  33. func main() {
  34. stu := &Student{}
  35. stu.SetName("张三")
  36. stu.SetAge(13)
  37. stu.SetGrade("七年级")
  38. var member Member = stu
  39. member.SayHello()
  40. // student: 张三 说: 老师好!
  41. if student, ok := member.(*Student); ok {
  42. student.SetName("王五")
  43. student.SayHello()
  44. // student: 王五 说: 老师好!
  45. }
  46. }
  1. 使用 Member 类型的引用,指向一个 Student 对象
  2. 想对 Student 对象设置一个新名称,使用类型断言向下转型,可以调用 StudentSetName 方法(严格的说,是 PeopleSetName 方法)

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

  1. func SayHello(inter interface{}) {
  2. if member, ok := inter.(Member); ok {
  3. member.SayHello()
  4. }
  5. }
  6. func main() {
  7. stu := &Student{}
  8. stu.SetName("张三")
  9. stu.SetAge(13)
  10. stu.SetGrade("七年级")
  11. SayHello(stu)
  12. // student: 张三 说: 老师好!
  13. }

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

4 参考资料

  1. Golang 中 struct 嵌入 interface
  2. 面向对象思考与 golang cobra 库实现原理
  3. 多态中的向上转型与向下转型

原文链接:https://www.cnblogs.com/ccwd/p/17800795.html

 友情链接:直通硅谷  点职佳  北美留学生论坛

本站QQ群:前端 618073944 | Java 606181507 | Python 626812652 | C/C++ 612253063 | 微信 634508462 | 苹果 692586424 | C#/.net 182808419 | PHP 305140648 | 运维 608723728

W3xue 的所有内容仅供测试,对任何法律问题及风险不承担任何责任。通过使用本站内容随之而来的风险与本站无关。
关于我们  |  意见建议  |  捐助我们  |  报错有奖  |  广告合作、友情链接(目前9元/月)请联系QQ:27243702 沸活量
皖ICP备17017327号-2 皖公网安备34020702000426号