作者都是各自领域经过审查的专家,并撰写他们有经验的主题. 我们所有的内容都经过同行评审,并由同一领域的Toptal专家验证.
Go是面向对象的吗?? 这是真的吗?? Go(或“Golang”)是一种后面向对象编程语言,它借用了它的结构(包), 类型, 函数)来自Algol/Pascal/Modula语言家族. 然而,在 Go, 面向对象模式对于以清晰易懂的方式构建程序仍然很有用. 本教程将采用一个简单的示例,演示如何将绑定函数的概念应用于类型(也称为类)。, 构造函数, 子类型化, 多态性, 依赖注入, 用模拟进行测试.
独特的 车辆识别号码 每辆车的旁边都有一个“跑步”(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文)
}
}
因此,当给定正确的输入时,这个函数可以正常工作,但它有一些问题:
恐慌
.为了解决这些问题,我们将使用面向对象模式对其进行重构.
第一个重构是使文成为它们自己的类型并绑定 制造商()
它的功能. 这使得函数的目的更清晰,并防止粗心的使用.
包文
类型文字符串
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文.制造商() // 恐慌!
}
插入最后一行是为了演示如何触发 恐慌
在使用 制造商()
函数. 在测试之外,这会使正在运行的程序崩溃.
为了避免 恐慌
在处理无效的文时,可以将有效性检查添加到 制造商()
函数本身. 缺点是每次调用时都要进行检查 制造商()
函数, 并且必须引入一个错误返回值, 如果没有中间变量,就不可能直接使用返回值(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文
构造函数.
接下来,我们要区分欧洲和非欧洲的文. 一种方法是扩展文 类型
到一个 结构体
并存储文是否是欧洲的,从而相应地增强构造函数:
类型文结构{
代码的字符串
欧洲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开发团队故意选择不支持非接口类型的动态绑定. 它使编译器能够知道在编译时将调用哪个函数,并避免了动态方法分派的开销. 这种选择也不鼓励使用继承作为一般的组合模式. 相反,接口才是正确的选择(请原谅我的双关语).
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
.
批评人士 可能会说,“既然要做面向对象,为什么不使用Java呢?“嗯, 因为您可以获得Go的所有其他优点,同时避免了资源消耗巨大的VM/JIT, 带有注释的框架, 异常处理, 在运行测试时喝咖啡休息(后者可能是一个问题).
通过上面的例子, 与普通语言相比,用Go语言进行面向对象编程可以产生更好理解和更快运行的代码,这一点很清楚, 必须实现. 尽管Go并不是一种面向对象的语言,但它提供了必要的工具 构建应用程序 以面向对象的方式. 以及包中的分组功能, 可以利用Golang中的OOP来提供可重用模块作为构建块 大型应用程序.
作为谷歌云合作伙伴,Toptal的谷歌认证专家可以为公司服务 对需求 为了他们最重要的项目.
Golang(或简称“Go”)是一种通用语言,适用于开发复杂的系统工具和api. 具有自动内存管理功能, 静态类型系统, 内置的并发性, 和一个富有的, 面向web的运行库, 它对分布式系统和云服务特别有用.
Golang及其运行时包都是用Go语言编写的. 直到Golang 1号.5,它于2015年发布,编译器和部分运行时是用C编写的.
Golang的构建模块是类型, 功能, 和包——与Java等面向对象语言中的类形成对比. 然而, OOP封装的四个概念中的三个, 抽象, 和多态性)都是可用的, 缺失的类型层次结构被接口和duck类型所取代.
Golang是一种内存管理语言,具有用于通用数据结构和并发编程的强大原语. 它直接编译为机器码,从而提供类似c的性能和资源效率. 它的快速编译和包含的测试工具提高了开发人员的工作效率.
Golang正在愉快地发展,最近庆祝了其成立10周年. 目前全球大约有100万活跃的Golang开发人员使用Golang进行各种项目. 还有Golang 2.0正在制作中,并将为地鼠带来令人兴奋的新功能.e.,更广泛的Go开发者社区.)
Go是由Google开发的,用来取代内部使用的Python、c++和其他系统语言. 2009年首次发布, 语言核心每六个月更新一次,同时保持编程接口的稳定. Go是开源的,并且有一个丰富的社区积极参与其开发.
莱昂哈德做了15年的专业开发人员. 作为一名Go专家,他喜欢这种语言的简单性、性能和生产力.
世界级的文章,每周发一次.
世界级的文章,每周发一次.