作者丨Murray
翻译丨Peter
在第一部分里面就GO语言的简单功能(特征)做了论述,如常用语法,基本类型等。本文将主要提及GO所支持的package(包)和面向对象。在这之前呢,还是建议读者阅读一下此书,照旧,欢迎各方高人点评和纠错。
总的来说,我发现GO语言面向对象的语法有点乱,一致性差、不明显,所以对于大多数使用场合,个人更倾向于C++明显的继承层次结构。
在这个部分的文章里面故意不提及系统构建,分发或者配置等内容。
Packages(包)
Go代码是以软件包的形式组织的,Java也有包的概念,二者很像,跟C++命名空间也有点类似。 在源文件的开头声明包的名称:
当需要用到某个包时,用import方式导入:
package bar //定义了包名
import ( //告诉Go编译器这个程序需要使用 foo、moo 包(的函数,或其他元素)
"foo"
"moo"
)
func somefunc() {
foo.Yadda()
var a moo.Thing
...
} 包名称应与文件的目录名称匹配。 这是import语句找到对应包的关键。一个目录可允许有多个文件,这些文件都是同一个包的一部分。
package main不受以上规则约束。由于其唯一性,所以对应的目录不需要命名为main。
结构体
在Go语言中可以像C一样声明一个结构体:
type Thing struct {
// Member fields.
// Notice the lack of the var keyword.
a int
B int // See below about symbol visibility
}
var foo Thing
foo.B = 3
var bar Thing = Thing{3}
var goo *Thing = new(Thing)
goo.B = 5 我习惯使用var关键字演示变量的实际类型,也可能会选择较短的表达式 := 。
请注意,我们可以将其创建为一个值或一个指针(使用内置的new()函数),与C或C ++不同,Go中的结构体所占的实际内存并不能确定是在堆还是栈上。 具体由编译器决定,一般是根据内存是否需要延续功能调用来分配。
以前,我们已经看到内置的make()函数用于实例化slices(切片)和maps(集合)。 make()仅适用于那些内置类型。 对于自定义类型,可以使用new()函数。 我发现这个区别有点混乱,但是我一般不喜欢使用语言本身就可以实现的类型区别。 我喜欢C++标准库如何在C++中实现方式,当往库里面添加内容时,语言本身几乎没有什么特别的支持。
Go类型通常具有“构造函数”(而不是方法),应该调用该函数来正确实例化该类型,但是我认为没有办法强制执行正确的初始化,就像C++或Java中的默认构造函数。 例如:
type Thing struct {
a int
name string
...
}
func NewThing() *Thing {
// 100 is a suitable default value for a in this type:
f := Thing{100, nil}
return &f
}
// Notice that different "constructors" must have different names,
// because go doesn't have function or method overloading.
func NewThingWithName(name string) *Thing {
f := Thing{100, name}
return &f
} Embedding Structs(嵌套结构体)
可以匿名地将一个结构体“嵌入”到其他结构体中,如下所示:
type Person struct {
Name string
}
type Employee struct {
Person
Position string
}
var a Employee
a.Name = "bob"
a.Position = "builder" 这感觉有点像C ++和Java中的继承,例如,可以这样:
var e = new(Employee)
// Compilation error.
var p *Person = e
// This works instead.
// So if we thought of this as a cast (we probably shouldn't),
// this would mean that we have to explicitly cast to the base class.
var p *Person = e.Person
// This works.
e.methodOnPerson()
// And this works.
// Name is a field in the contained Person struct.
e.Name = 2
// These work too, but the extra qualification is unnecessary.
e.Person.methodOnPerson() Methods(方法)
Go语言中的结构体可以有Methods(与结构相关联的函数),这点和C/Java语言的classes(类)很像 , 但在语法方面略有不同。 Method在结构体外被声明,并且通过在函数名之前指定“receiver”来进行调用。 例如,它声明(并实现)Thing结构体的DoSomething方法:
func (t Thing) DoSomething() {
...
} 还有需要注意的一点,由于GO没有内置如“self”或“this”的实体名,故必须为receiver指定一个名称。这感觉有点相互矛盾。
可以使用指针替代,而且如果要更改关于struct实例的任何内容,指针是不二选择:
func (t *Thing) ChangeSomething() {
t.a = 4
} 如果需要保持代码的一致性,最好给method receivers指定为指针类型。
跟C++/Java不同,这允许检查实例是否为nil(Go为null或nullptr),使其可以在null实例上调用方法。 这让我想起Objective-C如何在nil实例上调用方法而没有崩溃,甚至返回一个nil/zero值。 我发现Objective-C没有这一机制,更让我沮丧的是,Go允许这样做,但没有一定的一致性。
与C++或Java不同,GO甚至可以将methods与非struct(非类)类型相关联。 例如:
type Meters int
type Feet int
func (Meters) convertToFeet() (Feet) {
...
}
Meters m = 10
f := p.convertToFeet() 没有赋值或比较运算符重载
在C++里面,可将=、 !=、 、等运算符重载,所以可以使用这些常规的运算符,使代码看起来更整洁:
MyType a = getSomething();
MyType b = getSomethingElse();
if (a == b) {
...
} 在Go语言中不能这么使用, 只有部分内建类型是可比较的,如数字类型,字符串,指针或通道,或由这些类型组成的结构体或数组。当处理接口时这会是一个麻烦,我们稍后会看到。
符号可见性: 大写或小写字母
以大写字母开头的符号 (类型、函数、变量) 可从包外部获得。结构方法和以大写字母开头的成员变量可从结构外部获得。否则,它们就是私有的包或结构。例如:
type Thing int // This type will be available outside of the package.
var Thingleton Thing// This variable will be available outside of the package.
type thing int // Not available outside of the package.
var thing1 thing // Not available outside of the package.
var thing2 Thing // Not available outside of the package.
// Available outside of the package.
func DoThing() {
...
}
// Not available outside of the package.
func doThing() {
...
}
type Stuff struct {
Thing1 Thing // Available outside of the package.
thing2 Thing // "private" to the struct.
}
// Available outside of the struct.
func (s Stuff) Foo() {
...
}
// Not available outside of the struct.
func (s Stuff) bar() {
...
}
// Not available outside of the package.
type localstuff struct {
...
} 感觉这有点奇怪。相对而言, c++和Java中明确用public和private关键字声明显得更加友善。
Interfaces(接口)
有方法的接口
如果两个Go类型满足一个接口,那它们都具有该接口的方法, 这与Java接口类似。 Go接口也有点像C ++中的一个完全抽象类(只有纯虚方法),跟C ++ Concept(概念)也很像(自C ++ 17)。
例如:
type Shape interface {
// The interface's methods.
// Note the lack of the func keyword.
SetPosition(x int, y int)
GetPosition() (x int, y int)
DrawOnSurface(s Surface)
}
type Rectangle struct {
...
}
// Methods to satisfy the Shape interface.
func (r *Rectangle) SetPosition(x int, y int) {
...
}
func (r *Rectangle) GetPosition() (x int, y int) {
...
}
func (r *Rectangle) DrawOnSurface(s Surface) {
...
}
// Other methods:
func (r *Rectangle) setCornerType(c CornerType) {
...
}
func (r *Rectangle) cornerType() (CornerType) {
...
}
type Circle struct {
...
}
// Methods to satisfy the Shape interface.
func (c *Circle) SetPosition(x int, y int) {
...
}
func (c *Circle) GetPosition() (x int, y int) {
...
}
func (c *Circle) DrawOnSurface(s Surface) {
...
}
// Other methods:
... 然后,就可以使用接口类型而不是特定的 “实际” 类型:
var someCircle *Circle = new(Circle)
var s Shape = someCircle
s.DrawOnSurface(someSurface) 请注意,这里使用的是Shape, 而不是使用Shape(指向Shape的指针), 即使是从 Circle(指向circle)转换。 “接口值”似乎是隐式指针,这似乎是不必要的混淆。 如果指向接口的指针只是具有与这些“接口值”相同的行为,即使语言禁止使用指针的接口类型, 它也会更加一致。
隐式满足接口类型
但是,没有明确声明类型应实现接口。
通过这种方式, 接口就像C++的概念, 虽然C++概念是纯编译时功能, 用于通用 (模板) 代码。您的类可以符合 c++ 概念,而无需具体声明。因此, 与 go 接口一样, 如果必须, 您可以使用现有类型而不更改它。
编译器仍需检查类型是否兼容, 但可能是检查类型的方法链表, 而不是检查类层次结构或已实现接口的链表。例如:
var a *Circle = new(Circle)
var b Shape = a // OK. The compiler can check that Circle has Shape's methods. 像 c++ 的dynamic_cast一样,GO 也可以在运行时检查。例如,可以检查一个接口值是否引用一个同时满足另一个接口的实例:
// Sometimes the Shape (our interface type) is also a Drawable
// (another interface type), sometimes not.
var a Shape = Something.GetShape()
// Notice that we want to cast to a Drawable, not a *Drawable,
// because Drawable is an interface.
var b = a.(Drawable) // Panic (crash) if this fails.
var b, ok = a.(Drawable) // No panic.
if ok {
b.DrawOnSurface(someSurface)
} 或者,可以检查接口值是否特指某种具体类型。例如:
// Get Shape() returns an interface value.
// Shape is our interface.
var a Shape = Something.GetShape()
// Notice that we want to cast to a *Thing, not a Thing,
// because Thing is a concrete type, not an interface.
var b = a.(*Thing) // Panic (crash) if this fails.
var b, ok = a.(*Thing) // No panic.
if ok {
b.DoSomething()
} Runtime调用
接口方法也类似于 c++ 虚方法 (或java 方法), 接口变量也类似于多态基类的实例。为了通过接口变量实际调用接口的方法, 程序需要在运行时检查其实际类型, 并调用该类型的特定方法。也许,与 c++ 一样,编译器有时可以优化掉这种间接寻址。
这显然不如直接调用 c++ 模板中的模板化类型在编译时标识的方法那样有效。但它显然是简单得多。
比较接口
接口值有时可以比较, 但这似乎是一个危险的业务。接口值为:
- 类型不同,则不相等。
- 类型相同,只有一个为nil,不相等。
- 类型相同,可比较,并且它们的值一样,则相等。
但是,如果类型是相同的,但这些类型是不可比较的, 将导致Go在运行时抛出异常 “panic”。(译者注:panic 是用来表示非常严重的不可恢复的错误的。在Go语言中这是一个内置函数,接收一个interface{}类型的值(也就是任何值了)作为参数。panic的作用就像我们平常接触C++的异常)
希望实现关键字
在C ++中,如果你愿意,可以显式声明一个类应符合该概念,或者你可以从一个基类中显示的派生出来,而在Java中,必须使用“implements”关键字。由于GO语言没有此机制,因此需要习惯。我想要这些声明来记录我的架构,根据他们的一般目的明确地显示我的“具体”类的预期,而不是仅仅用一些其他代码来表达它们。没有这个感觉很脆弱。
该书建议将这个笨拙的代码放在某处,以检查一个类型是否真正实现了一个接口。注意_(下划线)的使用意味着我们不需要为结果保留一个命名变量。
var _ MyInterface =(* MyType)(nil) 如果类型不满足接口,转换是不可能的,编译器应该报错。作为最初级测试,我认为这是明智之举。特别是如果您的包提供的类型,不是真正使用的包本身。对于我来说, 这是一个很糟糕的替代品,它使用特定的语言构造对类型本身进行明显的编译时检查。
接口嵌入
在接口中嵌入接口
GO不具有继承层次结构的概念, 但您可以在一个接口中 “嵌入”另 一个接口, 以指示满足一个接口的类也满足另一个接口。例如:
type Positionable interface {
SetPosition(x int, y int)
GetPosition() (x int, y int)
}
type Drawable interface {
drawOnSurface(s Surface) }
}
type Shape interface {
Positionable
Drawable
} 为了满足Shape接口,任何类型也必须满足Drawable和Positionable接口。 因此,任何满足Shape接口的类型都可以与Drawable或Positionable接口关联的方法使用。 这有点像一个java接口扩展另一个接口。
在结构体中嵌入一个满足接口的结构体
我们早些时候就看到了如何将一个结构嵌入另一个匿名结构体中。如果包含的struct实现了一个接口, 则包含的struct也可以实现该接口, 而不需要手动实现的转发方法。例如:
type Drawable interface {
drawOnSurface(s Surface)
}
type Painter struct {
...
}
// Make Painter satisfy the Drawable interface.
func (p *Painter) drawOnSurface(s Surface) {
...
}
type Circle struct {
// Make Circle satisfy the Drawable interface via Painter.
Painter
...
}
func main() {
...
var c *Circle = new(Circle)
// This is OK.
// Circle satisfies Drawable, via Painter
c.drawOnSurface(someSurface)
// This is also OK.
// Circle can be used as an interface value of type Drawable, via Painter.
var d Drawable = c
d.drawOnSurface(someSurface)
} 再一次感觉有点像继承
我实际上非常喜欢匿名地包含结构体的(接口)结构影响父结构的接口,即使是Go的异样接口系统,尽管我希望语法对于发生的事情更加明显。在C++中有类似的东西可能很好。封装而不是继承(和Decorator模式)是一种非常高效的技术,C ++通常会尝试以多种方式进行操作,而不会对最好的方式有所了解,尽管这本身就会成为复杂性的来源。但是在C++(和Java)中,你现在必须手动编码大量的转发方法来实现此目的,您仍然需要继承某种东西,以告知支持封装接口的类型系统。
|
|