探索 Go 语言的无类设计:从 Struct 到组合的优雅之道

探索Go语言的无类设计:从Struct到组合的优雅之道在众多编程语言中,Go以其简洁和高性能著称,但它却刻意摒弃了传统的面向对象特性——class。与C++、Java等语言的继承体系不同,Go选择了一条别样的道路:通过struct、方法关联以及组合(composition)来实现

探索 Go 语言的无类设计:从 Struct 到组合的优雅之道

在众多编程语言中,Go 以其简洁和高性能著称,但它却刻意摒弃了传统的面向对象特性——class。与 C++、Java 等语言的继承体系不同,Go 选择了一条别样的道路:通过 struct、方法关联以及组合(composition)来实现数据与行为的组织。这种设计不仅简化了代码结构,还赋予了开发者更大的灵活性。本文将深入剖析 Go 如何在无 class 的世界中实现面向对象的目标,从 struct 的基本用法到组合与转发的强大特性,带你领略 Go 的独特哲学。

Go 语言摒弃了传统的 class 和继承机制,转而使用 struct 结合方法关联来定义数据和行为。通过 struct 的复合字面值和构造函数,开发者可以灵活初始化复杂数据结构;而嵌入(embedding)特性则通过组合与方法转发,实现了类似继承的功能,但更为简洁和灵活。本文从 struct 的基本使用入手,展示了如何将方法绑定到类型上,并通过构造函数和组合替代 class 的功能。代码示例进一步阐释了这些概念的实践应用,包括距离计算和温度转换等场景。文章还探讨了组合优于继承的设计哲学,帮助读者理解 Go 的无类之道。

Go语言没有class

Go语言里没有class

  • Go和其它经典语言不同,它没有class,没有对象,也没有继承。
  • 但是Go提供了struct和方法。

将方法关联到struct

  • 方法可以被关联到你声明的类型上
package main

import "fmt"

// coordinate in degrees, minutes, seconds in a N/S/E/W hemisphere.
type coordinate struct {
  d, m, s float64
  h rune
}

// decimal converts a d/m/s coordinate to decimal degrees.
func (c coordinate) decimal() float64 {
  sign := 1.0
  switch c.h {
  case 'S', 'W', 's', 'w':
    sign = -1
  }
  return sign * (c.d + c.m/60 + c.s/3600)
}

func main() {
  // Bradbury Landing: 4°35'22.2" S, 137°26‘30.1“ E
  lat := coordinate{4, 35, 22.2, 'S'}
  long := coordinate{137, 26, 30.12, 'E'}

  fmt.Println(lat.decimal(), long.decimal())
}

小测试

  • 上例中,decimal方法的接收者是谁?
  • 答案:coordinate 类型,c 是接收者的变量名。

构造函数

  • 可以使用struct复合字面值来初始化你所要的数据。
  • 但如果struct初始化的时候还要做很多事情,那就可以考虑写一个构造用的函数。
package main

import "fmt"

// coordinate in degrees, minutes, seconds in a N/S/E/W hemisphere.
type coordinate struct {
  d, m, s float64
  h rune
}

// decimal converts a d/m/s coordinate to decimal degrees.
func (c coordinate) decimal() float64 {
  sign := 1.0
  switch c.h {
  case 'S', 'W', 's', 'w':
    sign = -1
  }
  return sign * (c.d + c.m/60 + c.s/3600)
}

type location struct {
  lat, long float64
}

// newLocation from latitude, longitude d/m/s coordinates.
func newLocation(lat, long, coordinate) location {
  return location{lat.decimal(), long.decimal()}
}

func main() {
  // Bradbury Landing: 4°35'22.2" S, 137°26‘30.1“ E
  lat := coordinate{4, 35, 22.2, 'S'}
  long := coordinate{137, 26, 30.12, 'E'}

  fmt.Println(lat.decimal(), long.decimal())

  // curiosity := location{lat.decimal(), long.decimal()}
  curiosity := newLocation(lat, long)

  fmt.Println(curiosity)
}
  • Go语言没有专用的构造函数,但以new或者New开头的函数,通常是用来构造数据的。例如newPerson(),NewPerson()

New函数

  • 有一些用于构造的函数的名称就是New(例如errors包里面的New函数)。
  • 这是因为函数调用时使用包名.函数名的形式。
  • 如果该函数叫NewError,那么调用的时候就是errors.NewError(),这就不如errors.New()简洁

小测试

  • 如果你想构建一个Universe类型的变量,那么你如何为该函数命名?

  • 答案:newUniverse 或 NewUniverse,推荐 NewUniverse(Go 惯例)。

    class的替代方案

  • Go语言没有class,但使用struct并配备几个方法也可以达到同样的效果。

package main

import (
  "fmt"
  "math"
)

type location struct {
  lat, long float64
}

type world struct {
  radius float64
}

// distance calculation using the Spherical Law of Cosines.
func (w world) distance(p1, p2 location) float64 {
  s1, c1 := math.Sincos(rad(p1.lat))
  s2, c2 := math.Sincos(rad(p2.lat))
  clong := math.Cos(rad(p1.long - p2.long))
  return w.radius * math.Acos(s1*s2+c1*c2*clong)
}

// rad converts degrees to radians.
func rad(deg float64) float64 {
  return deg * math.Pi / 180
}

func main() {
  var mars = world{radius: 3389.5}
  spirit := location{-14.5684, 175.472636}
  opportunity := location{-1.9462, 354.4734}

  dist := mars.distance(spirit, opportunity)
  fmt.Printf("%.2f km\n", dist)
}

小测试

  • 与不采用面向对象的方式相比,在world类型上声明一个distance方法的好处是什么?
  • 答案:封装性更好,world 的 radius 与 distance 逻辑绑定,便于扩展和维护。

作业题

  1. 使用例子中的代码,编写一个程序。并为下表中每个 位置都声明一个location,以十进制度数打印出每个位置。
  2. 使用例子中的distance方法,编写一个程序,来判定上题表中每对着陆点之间的距离。并回答:
    1. 哪两个着陆点之间最近?
    2. 哪两个着陆点之间最远?
    3. 计算伦敦到巴黎之间的距离(51°30’N 0°08’W),(48°51’N 2°21’E),地球半径为6371公里。
    4. 计算你的城市到北京距离
    5. 计算火星上Mount Sharp (5°4’ 48”S, 137°51’E)到Olympus Mons (18°39’N,226°12’E)之间的距离。火星的半径是3389.5公里。

组合与转发

Composition and forwarding

组合

  • 在面向对象的世界中,对象由更小的对象组合而成。
  • 术语:对象组合或组合
  • Go通过结构体实现组合(composition)。
  • Go提供了“嵌入”(embedding)特性,它可以实现方法的转发(forwarding)
  • 组合是一种更简单、灵活的方式。

组合结构体

例子一

package main

type report struct {
  sol int
  high, low float64
  lat, hong float64
}

func main() {

}

例子二

package main

type report struct {
  sol int 
  temperature temperature
  location location
}

type temperature struct {
  hign, low celsius
}

type location struct {
  lat, long float64
}

type celsius float64

func (t temperature) average() celsius {
  return (t.high + t.low) / 2
}

func (r report) average() celsius {
  return r.temperature.average()
}

func main() {
  bradbury := location{-4.5895, 137.4417}
  t := temperature{high: -1.0, low: -78.0}
  fmt.Println(t.average())
  report := report {
    sol: 15,
    temperature: t,
    location: bradbury
  }

  fmt.Println(report.temperature.average())
  fmt.Println(report.average())

  fmt.Printf("%+v\n", report)

  fmt.Printf("a balmy %v° C\n", report.temperature.high)
}

小测试

  • 比较例子1和2的代码,你更喜欢哪一种?原因是什么?
  • 答案:例子 2(组合)更好,结构清晰,易于扩展和复用。

转发方法

  • Go可以通过struct嵌入 来实现方法的转发。
  • 在struct中只给定字段类型,不给定字段名即可。
package main

import "fmt"

//type report struct {
//  sol int
//  temperature temperature
//  location location
//}

type report struct {
  sol int
  temperature
  location
}

type temperature struct {
  high, low celsius
}

type location struct {
  lat, long float64
}

type celsius float64

func (t temperature) average() celsius {
  return (t.high + t.low) / 2
}

func main() {
  bradbury := location{-4.5895, 137.4417}
  t := temperature{high: -1.0, low: -78.0}
  fmt.Println(t.average())
  report := report {
    sol: 15,
    temperature: t,
    location: bradbury
  }

  fmt.Println(report.average())

  fmt.Println(report.high)

  fmt.Printf("%+v\n", report)

  fmt.Printf("a balmy %v° C\n", report.temperature.high)
}
  • 在struct中,可以转发任意类型。
package main

import "fmt"

//type report struct {
//  sol int
//  temperature temperature
//  location location
//}

type sol int

type report struct {
  sol 
  temperature
  location
}

type temperature struct {
  high, low celsius
}

type location struct {
  lat, long float64
}

type celsius float64

func (t temperature) average() celsius {
  return (t.high + t.low) / 2
}

func (s sol) days(s2 sol) int {
  days := int(s2 - s)
  if days < 0 {
    days = -days
  }
  return days
}

func main() {
  report := report {
    sol: 15,
  }

  fmt.Println(report.sol.days(1446))

  fmt.Println(report.days(1446))
}

小测试

  • 结构体可以嵌入什么类型?
  • 访问report.lat字段是否合法?如果合法,那么上面例子中它指向哪个字段?

命名冲突

package main

import "fmt"

//type report struct {
//  sol int
//  temperature temperature
//  location location
//}

type sol int

type report struct {
  sol 
  temperature
  location
}

type temperature struct {
  high, low celsius
}

type location struct {
  lat, long float64
}

type celsius float64

func (t temperature) average() celsius {
  return (t.high + t.low) / 2
}

func (s sol) days(s2 sol) int {
  days := int(s2 - s)
  if days < 0 {
    days = -days
  }
  return days
}

func (l location) days(l2 location) int {
  // To-do: complicated distance calculation
  return 5
}

func (r report) days(s2 sol) int {
  return r.sol.days(s2)
}

func main() {
  report := report {
    sol: 15,
  }

  fmt.Println(report.sol.days(1446))

  //fmt.Println(report.days(1446)) // 报错
  fmt.Println(report.days(1446))
}

继承 还是 组合?

  • Favor object composition over class inheritance.
  • 优先使用对象组合而不是类的继承。
  • Use of classical inheritance is always optional; every problem thatit solves can be solved another way.
  • 对传统的继承不是必需的;所有使用继承解决的问题都可以通过其它方法解决。

小测试

  • 如果多个嵌入的类型都实现了同名的方法,那么Go编译器会报错吗?
  • 答案:Go 编译器在多个嵌入类型有同名方法时不会直接报错,但要求显式指定调用哪个类型的方法,否则报“歧义”错误。 这种设计避免了继承中复杂的多重继承冲突,保持了简洁性。也就是说,在 Go 中,当一个结构体通过嵌入(embedding)包含多个类型,并且这些类型实现了同名方法时,编译器不会直接报错,而是要求开发者在调用时明确指定使用哪个嵌入类型的方法。如果不指定,编译器会报错提示“ambiguous selector”(选择器歧义)。

总结

Go 语言的无类设计并不是对面向对象的否定,而是对其的一种重新定义。通过 struct 和方法关联,Go 提供了简洁的数据与行为绑定方式;通过组合与转发,它在避免继承复杂性的同时,保留了灵活性和复用性。从构造函数的优雅实现到嵌入带来的方法共享,Go 用更少的概念解决了 class 所能解决的问题。正如“一切有为法,如梦幻泡影”,Go 的设计哲学提醒我们:技术不必拘泥于传统,简单的组合往往胜过繁琐的继承。无论你是面向对象编程的拥趸,还是初探 Go 的新手,这种无类之道都值得一试。

  • 原创
  • 学分: 5
  • 分类: Go
  • 标签:
点赞 0
收藏 0
分享
本文参与登链社区写作激励计划 ,好文好收益,欢迎正在阅读的你也加入。
该文章收录于 Go 语言
4 订阅 21 篇文章

0 条评论

请先 登录 后评论
寻月隐君
寻月隐君
0x89EE...a439
不要放弃,如果你喜欢这件事,就不要放弃。如果你不喜欢,那这也不好,因为一个人不应该做自己不喜欢的事。