作者都是各自领域经过审查的专家,并撰写他们有经验的主题. 我们所有的内容都经过同行评审,并由同一领域的Toptal专家验证.
霍尔兹里昂哈
验证专家 在工程

莱昂哈德做了15年的专业开发人员. 作为一名Go专家,他喜欢这种语言的简单性、性能和生产力.

以前在

德国电信股份公司
分享

Go是面向对象的吗?? 这是真的吗?? Go(或“Golang”)是一种后面向对象编程语言,它借用了它的结构(包), 类型, 函数)来自Algol/Pascal/Modula语言家族. 然而,在 Go, 面向对象模式对于以清晰易懂的方式构建程序仍然很有用. 本教程将采用一个简单的示例,演示如何将绑定函数的概念应用于类型(也称为类)。, 构造函数, 子类型化, 多态性, 依赖注入, 用模拟进行测试.

Golang OOP案例研究:从车辆识别号码(文)中读取制造商代码

独特的 车辆识别号码 每辆车的旁边都有一个“跑步”(i.e., 序列号-关于汽车的信息, 比如制造商, 生产工厂, 汽车模型, 如果从左边或右边开车.

确定制造商代码的函数可能如下所示:

包文

func制造商(文字符串)字符串{

  制造商:= 文[: 3]
  //如果制造商ID的最后一位数字是9
  //数字12 ~ 14是ID的第二部分
  如果manufacturer[2] == '9' {
    制造商+= 文[11:14]
  }

  返回厂家
}

下面是一个测试,证明了一个示例文是有效的:

包文_test

导入(
  “文-stages / 1”
  “测试”
)

const test文 = "W09000051T2123456"

Test文_Manufacturer(t *test ..T) {

  制造商:= 文.制造商(test文)
  如果制造商 != "W09123" {
    t.错误(" unknown manufacturer %s for 文 %s", manufacturer, test文)
  }
}

因此,当给定正确的输入时,这个函数可以正常工作,但它有一些问题:

  • 不能保证输入字符串是文.
  • 对于短于三个字符的字符串,该函数将导致a 恐慌.
  • 可选的ID的第二部分仅是欧洲文的功能. 对于制造商代码的第三位数字为9的美国汽车,该函数将返回错误的id.

为了解决这些问题,我们将使用面向对象模式对其进行重构.

面向对象:将函数绑定到类型

第一个重构是使文成为它们自己的类型并绑定 制造商() 它的功能. 这使得函数的目的更清晰,并防止粗心的使用.

包文

类型文字符串

func (v 文)制造商()字符串{

  制造商:= v[: 3]
  如果manufacturer[2] == '9' {
    制造商+= v[11:14]
  }

  返回字符串(制造商)
}

然后,我们对测试进行了调整,并引入了无效文的问题:

包文_test

导入(
  “文-stages / 2”
  “测试”
)

常量(
  valid文 = 文.“W0L000051T2123456”(文)的
  invalid文 = 文.“W0”(文)的
)

Test文_Manufacturer(t * test ..T) {

  制造商:= valid文.制造商()
  如果制造商 != "W0L" {
    t.错误(" unknown manufacturer %s for 文 %s", manufacturer, valid文)
  }

  invalid文.制造商() // 恐慌!
}

插入最后一行是为了演示如何触发 恐慌 在使用 制造商() 函数. 在测试之外,这会使正在运行的程序崩溃.

Golang中的OOP:使用构造函数

为了避免 恐慌 在处理无效的文时,可以将有效性检查添加到 制造商() 函数本身. 缺点是每次调用时都要进行检查 制造商() 函数, 并且必须引入一个错误返回值, 如果没有中间变量,就不可能直接使用返回值(e.g.,作为地图键).

的构造函数中进行有效性检查是一种更优雅的方法 打字,使 制造商() 函数只针对有效的文调用,不需要检查和错误处理:

包文

进口“fmt”

类型文字符串

//这个函数应该命名为New还是New文是有争议的
//但New文更适合于搜索,并为其他搜索留出了空间
// NewXY函数在同一个包中
函数New文(代码串)(文,错误){

  如果len(代码) != 17 {
    返回"",FMT.错误("无效的文 %s:多于或少于17个字符",代码)
  }

  // ... 检查不允许的字符 ...

  返回文(code), nil
}

func (v 文)制造商()字符串{

  制造商:= v[: 3]
  如果manufacturer[2] == '9' {
    制造商+= v[11:14]
  }

  返回字符串(制造商)
}

当然,我们添加了一个测试 New文 函数. 构造函数现在会拒绝无效的文:

包文_test

导入(
  “文-stages / 3”
  “测试”
)

常量(
  valid文 = "W0L000051T2123456"
  invalid文 = "W0"
)

函数Test文_New(t *test.T) {

  _, err:= 文.New文 (valid文)
  如果犯错 != nil {
    t.创建有效识别码返回错误:%s,错误.错误())
  }

  _, err = 文.New文 (invalid文)
  如果err == nil {
    t.错误("创建无效识别码未返回错误")
  }
}

Test文_Manufacturer(t *test ..T) {

  test文, _:= 文.New文 (valid文)
  制造商:= test文.制造商()
  如果制造商 != "W0L" {
    t.错误(" unknown manufacturer %s for 文 %s", manufacturer, test文)
  }
}

这个测试 制造商() 函数现在可以忽略测试无效的文,因为它已经被 New文 构造函数.

Go OOP陷阱:错误的多态方式

接下来,我们要区分欧洲和非欧洲的文. 一种方法是扩展文 类型 到一个 结构体 并存储文是否是欧洲的,从而相应地增强构造函数:

类型文结构{
  代码的字符串
  欧洲bool
}

函数New文(代码字符串,欧洲bool)(*文,错误){

  // ... 检查 ...

  返回 &文 {code, european}, nil
}

更优雅的解决方案是创建的子类型 欧洲文. 在这里,标志隐式地存储在类型信息中,而 制造商() 功能,非欧洲文变得漂亮和简洁:

包文

进口“fmt”

类型文字符串

函数New文(代码串)(文,错误){

  如果len(代码) != 17 {
    返回"",FMT.错误("无效的文 %s:多于或少于17个字符",代码)
  }

  // ... 检查不允许的字符 ...

  返回文(code), nil
}

func (v 文)制造商()字符串{

  返回字符串(v[: 3])
}

EU文型

函数NewEU文(代码字符串)(EU文,错误){

  //调用超级构造函数
  v, err:= New文(代码)

  //转换为子类型
  返回EU文(v), err
}

func (v EU文)制造商()字符串{

  //调用制造商的超类型
  制造商:= 文(v).制造商()

  //如果合适,添加特定于欧盟的后缀
  如果manufacturer[2] == '9' {
    制造商+=字符串(v[11:14])
  }

  返回厂家
}

在像Java这样的OOP语言中,我们期望子类型 EU文 在任何地方都可以使用 指定类型. 不幸的是,这在Golang OOP中不起作用.

包文_test

导入(
  “文-stages / 4”
  “测试”
)

const euSmall文 = "W09000051T2123456"

//可以运行!
函数Test文_EU_SmallManufacturer(t *test ..T) {

  test文, _:= 文.NewEU文 (euSmall文)
  制造商:= test文.制造商()
  如果制造商 != "W09123" {
    t.错误(" unknown manufacturer %s for 文 %s", manufacturer, test文)
  }
}

//返回错误
Test文_EU_SmallManufacturer_Polymorphism(t *test ..T) {

  var test文s[.文
  test文, _:= 文.NewEU文 (euSmall文)
  //必须强制转换test文已经暗示了一些奇怪的事情
  test文s = append(test文s, 文.文(test文))

  for _, 文:= range test文 {
    制造商:= 文.制造商()
    如果制造商 != "W09123" {
      t.错误(" unknown manufacturer %s for 文 %s", manufacturer, test文)
    }
  }
}

这种行为可以解释为Go开发团队故意选择不支持非接口类型的动态绑定. 它使编译器能够知道在编译时将调用哪个函数,并避免了动态方法分派的开销. 这种选择也不鼓励使用继承作为一般的组合模式. 相反,接口才是正确的选择(请原谅我的双关语).

Golang OOP的成功:多态性的正确方式

Go编译器在实现声明的函数(duck typing)时,将类型视为接口的实现。. 因此,为了利用多态性 类型转换为由通用和欧洲文类型实现的接口. 请注意,欧洲文类型不一定是通用文类型的子类型.

包文

进口“fmt”

类型文接口{
  制造商()字符串
}

输入文字符串

函数New文(代码串)(文,错误){

  如果len(代码) != 17 {
    返回"",FMT.错误("无效的文 %s:多于或少于17个字符",代码)
  }

  // ... 检查不允许的字符 ...

  返回文(code), nil
}

func (v 文)制造商()字符串{

  返回字符串(v[: 3])
}

文u 文型

函数NewEU文(代码字符串)(文EU,错误){

  //调用超级构造函数
  v, err:= New文(代码)

  //转换为自己的类型
  返回evu (v), err
}

函数 (v e文u) 制造商()字符串 {

  //调用制造商的超类型
  制造商:= 文(v).制造商()

  //如果合适,添加特定于欧盟的后缀
  如果manufacturer[2] == '9' {
    制造商+=字符串(v[11:14])
  }

  返回厂家
}

多态性测试现在通过了一个轻微的修改:

//可以运行!
Test文_EU_SmallManufacturer_Polymorphism(t *test ..T) {

  var test文s[.文
  test文, _:= 文.NewEU文 (euSmall文)
  //现在不需要强制转换了!
  test文 = append(test文, test文)

  for _, 文:= range test文 {
    制造商:= 文.制造商()
    如果制造商 != "W09123" {
      t.错误(" unknown manufacturer %s for 文 %s", manufacturer, test文)
    }
  }
}

实际上,这两种文类型现在可以在指定的任何地方使用 接口,因为这两种类型都遵守 接口定义.

面向对象的语言:如何使用依赖注入

最后但并非最不重要的是,我们需要决定文是否属于欧洲. 假设我们已经找到了一个外部API来提供这些信息, 我们为此建立了一个客户端:

包文

类型文APIClient 结构体 {
  apiURL字符串
  apiKey字符串
  // ... 内部组件在这里 ...
}

函数new文apic客户端 (apiURL, apiKey字符串) * 文apic客户端 {

  返回 &文APIClient {apiURL, apiKey}
}

函数 (客户端* 文APIClient) IsEuropean(代码字符串)bool {

  //调用外部API并返回正确的值
  还真
}

我们还构建了一个处理文的服务,值得注意的是,可以创建它们:

包文

类型文Service 结构体 {
  客户端* 文APIClient
}

类型 文ServiceConfig 结构体 {
  APIURL字符串
  APIKey字符串
  //更多的配置值
}

*文Service (config *文ServiceConfig) *文Service {

  //使用config创建API客户端
  apiClient:= New文APIClient(配置.APIURL,配置.APIKey)

  返回 &文Service {apiClient}
}

函数c (s *文Service) CreateFromCode(代码字符串)(文,错误){

  如果s.客户端.IsEuropean(代码){
    返回NewEU文(代码)
  }

  返回New文(代码)
}

修改后的测试显示:

函数Test文_EU_SmallManufacturer(t *test ..T) {

  服务:= 文.New文Service ( & 文.文ServiceConfig {})
  test文, _:= service.CreateFromCode (euSmall文)

  制造商:= test文.制造商()
  如果制造商 != "W09123" {
    t.错误(" unknown manufacturer %s for 文 %s", manufacturer, test文)
  }
}

这里唯一的问题是测试需要与外部API的实时连接. 这是不幸的,因为API可能离线或无法访问. 此外,调用外部API需要花费时间和金钱.

由于API调用的结果是已知的,因此应该可以用模拟来替换它. 不幸的是,在上面的代码中 文Service 本身创建API客户机,因此没有简单的方法来替换它. 要实现这一点,应该将API客户端依赖项注入 文Service. 也就是说,应该在调用 文Service 构造函数.

这里的Golang OOP准则是这样的 任何构造函数都不应该调用另一个构造函数. 如果这是彻底应用, 应用程序中使用的每个单例都将在最顶层创建. 通常, 这将是一个引导函数,通过以适当的顺序调用它们的构造函数来创建所有需要的对象, 为程序的预期功能选择合适的实现.

第一步是做 文APIClient 一个接口:

包文

类型文APIClient接口{
  IsEuropean(代码的字符串) bool
}

类型文APIClient 结构体 {
  apiURL字符串
  apiKey字符串
  // .. 内部组件在这里 ...
}

函数new文apic客户端 (apiURL, apiKey字符串) * 文apic客户端 {

  返回 &文APIClient {apiURL, apiKey}
}

函数 (客户端* 文APIClient) IsEuropean(代码字符串)bool {

  //调用外部API并返回一些更有用的东西
  还真
}

然后,可以将新客户端注入 文Service:

包文

类型文Service 结构体 {
  客户端文APIClient
}

类型 文ServiceConfig 结构体 {
  //更多的配置值
}

(config *文ServiceConfig, apiClient 文APIClient) *文Service {

  // apiClient在别处创建,在这里注入
  返回 &文Service {apiClient}
}

函数c (s *文Service) CreateFromCode(代码字符串)(文,错误){

  如果s.客户端.IsEuropean(代码){
    返回NewEU文(代码)
  }

  返回New文(代码)
}

有了这个,现在就可以使用 API客户机模拟 为了测试. 除了避免在测试期间调用外部API之外, mock还可以充当探针,收集有关API使用情况的数据. 在下面的例子中,我们只检查 IsEuropean 函数实际上被调用了.

包文_test

导入(
  “文-stages / 5”
  “测试”
)

const euSmall文 = "W09000051T2123456"

mockapic客户端 结构体 {
  api调用int
}

函数NewMockAPIClient() *mockAPIClient {

  返回 &mockAPIClient {}
}

函数c(客户端*mockAPIClient) IsEuropean(代码字符串)bool {

  客户端.api调用+ +
  还真
}

函数Test文_EU_SmallManufacturer(t *test ..T) {

  apiClient:= NewMockAPIClient()
  服务:= 文.New文Service ( & 文.文ServiceConfig {}, apic客户端)
  test文, _:= service.CreateFromCode (euSmall文)

  制造商:= test文.制造商()
  如果制造商 != "W09123" {
    t.错误(" unknown manufacturer %s for 文 %s", manufacturer, test文)
  }

  如果apiClient.api调用 != 1 {
    t.错误("意外的API调用数:%d", apic客户端.api调用)
  }
}

这个测试通过了,因为我们 IsEuropean 的调用期间运行一次探测 CreateFromCode.

面向对象的Go编程:一个成功的组合(如果做得对)

批评人士 可能会说,“既然要做面向对象,为什么不使用Java呢?“嗯, 因为您可以获得Go的所有其他优点,同时避免了资源消耗巨大的VM/JIT, 带有注释的框架, 异常处理, 在运行测试时喝咖啡休息(后者可能是一个问题).

通过上面的例子, 与普通语言相比,用Go语言进行面向对象编程可以产生更好理解和更快运行的代码,这一点很清楚, 必须实现. 尽管Go并不是一种面向对象的语言,但它提供了必要的工具 构建应用程序 以面向对象的方式. 以及包中的分组功能, 可以利用Golang中的OOP来提供可重用模块作为构建块 大型应用程序.


谷歌云合作伙伴徽章.

作为谷歌云合作伙伴,Toptal的谷歌认证专家可以为公司服务 对需求 为了他们最重要的项目.

了解基本知识

  • Golang是用来干什么的?

    Golang(或简称“Go”)是一种通用语言,适用于开发复杂的系统工具和api. 具有自动内存管理功能, 静态类型系统, 内置的并发性, 和一个富有的, 面向web的运行库, 它对分布式系统和云服务特别有用.

  • Golang是用什么文字写的?

    Golang及其运行时包都是用Go语言编写的. 直到Golang 1号.5,它于2015年发布,编译器和部分运行时是用C编写的.

  • Golang是面向对象的吗??

    Golang的构建模块是类型, 功能, 和包——与Java等面向对象语言中的类形成对比. 然而, OOP封装的四个概念中的三个, 抽象, 和多态性)都是可用的, 缺失的类型层次结构被接口和duck类型所取代.

  • Golang值得学习吗?

    Golang是一种内存管理语言,具有用于通用数据结构和并发编程的强大原语. 它直接编译为机器码,从而提供类似c的性能和资源效率. 它的快速编译和包含的测试工具提高了开发人员的工作效率.

  • Golang有未来吗?

    Golang正在愉快地发展,最近庆祝了其成立10周年. 目前全球大约有100万活跃的Golang开发人员使用Golang进行各种项目. 还有Golang 2.0正在制作中,并将为地鼠带来令人兴奋的新功能.e.,更广泛的Go开发者社区.)

  • Golang是如何发展起来的?

    Go是由Google开发的,用来取代内部使用的Python、c++和其他系统语言. 2009年首次发布, 语言核心每六个月更新一次,同时保持编程接口的稳定. Go是开源的,并且有一个丰富的社区积极参与其开发.

标签

聘请Toptal这方面的专家.
现在雇佣
霍尔兹里昂哈

霍尔兹里昂哈

验证专家 在工程

柏林,德国

2019年7月19日成为会员

作者简介

莱昂哈德做了15年的专业开发人员. 作为一名Go专家,他喜欢这种语言的简单性、性能和生产力.

作者都是各自领域经过审查的专家,并撰写他们有经验的主题. 我们所有的内容都经过同行评审,并由同一领域的Toptal专家验证.

以前在

德国电信股份公司

世界级的文章,每周发一次.

输入您的电子邮件,即表示您同意我们的 隐私政策.

世界级的文章,每周发一次.

输入您的电子邮件,即表示您同意我们的 隐私政策.

Toptal开发者

加入总冠军® 社区.