专栏名称: CSDN
CSDN精彩内容每日推荐。我们关注IT产品研发背后的那些人、技术和故事。
目录
相关文章推荐
新浪科技  ·  【#iPhone16首批用户将陆续收到新机# ... ·  4 天前  
虎嗅APP  ·  换家电的门槛,又被京东拉低了 ·  4 天前  
51好读  ›  专栏  ›  CSDN

C++程序员是如何评价GO语言的(模块化和面向对象)

CSDN  · 公众号  · 科技媒体  · 2017-07-13 13:40

正文


作者丨Murray

翻译Peter



这是关于评论GO语言的第二部分,第一部分:C++程序员是如何评价GO的,第三部分会在不日后在CSDN公众号(ID:csdnnews)发布。


在第一部分里面就GO语言的简单功能(特征)做了论述,如常用语法,基本类型等。本文将主要提及GO所支持的package(包)和面向对象。在这之前呢,还是建议读者阅读一下此书,照旧,欢迎各方高人点评和纠错。


总的来说,我发现GO语言面向对象的语法有点乱,一致性差、不明显,所以对于大多数使用场合,个人更倾向于C++明显的继承层次结构。


在这个部分的文章里面故意不提及系统构建,分发或者配置等内容。


Packages(包)


Go代码是以软件包的形式组织的,Java也有包的概念,二者很像,跟C++命名空间也有点类似。 在源文件的开头声明包的名称:


package foo


当需要用到某个包时,用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)中,你现在必须手动编码大量的转发方法来实现此目的,您仍然需要继承某种东西,以告知支持封装接口的类型系统。