C++程序员眼中的go Part 2

本文探讨了Go语言中的包管理和面向对象特性。详细介绍了Go如何通过包来组织代码,以及其对于结构体、方法和接口的支持方式,并对比了与C++等语言的不同之处。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

A C++ developer looks at Go (the programming language), Part 2: Modularity and Object Orientation

原文 :https://www.murrayc.com/permalink/2017/06/28/a-c-developer-looks-at-go-the-programming-language-part-2-modularity-and-object-orientation/

This is the second part of a series. Here are all 3 parts:

In part 1, I looked at the simpler features of Go, such as its general syntax, its basic type system, and its for loop. Here I mention its support for packages and object orientation. As before, I strongly suggest that you read the book to learn about this stuff properly. Again, I welcome any friendly corrections and clarifications.

Overall, I find Go’s syntax for object orientation a bit messy, inconsistent and frequently too implicit, and for most uses I prefer the obviousness of C++’s inheritance hierarchy. But after writing the explanations down, I think I understand it.

I’m purposefully trying not to mention the build system, distribution, or configuration, for now.

Packages

Go code is organized in packages, which are much like Java package names, and a bit like C++ namespaces. The package name is declared at the start of each file:

package foo

And files in other packages should import them like so:

package bar

 

import (

  "foo"

  "moo"

)

 

func somefunc() {

  foo.Yadda()

  var a moo.Thing

  ...

}

The package name should match the file’s directory name. This is how the import statements find the packages’ files. You can have multiple files in the same directory that are all part of the same package.

Your “main” package, with your main function, is an exception to this rule. Its directory doesn’t need to be named “main” because you won’t be importing the main package from anywhere.

Go doesn’t seem to allow nested packages, unlike C++ (foo::thing::Yadda) and Java (foo.thing.Yadda), though people seem to work around this by creating separate libraries, which seems awkward.

I don’t know how people should specify the API of a Go package without providing the implementation. C and C++ have header files for this purpose, separating declaration and implementation.

Structs

You can declare structs in go much as in C. For instance:

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

As usual, I have used the var form to demonstrate the actual type, but you would probably want to use the short := form.

Notice that we can create it as a value or as a pointer (using the built-in new() function), though in Go, unlike C or C++, this does not determine whether its actual memory will be on the stack or the heap. The compiler decides, generally based on whether the memory needs to outlive the function call.

Previously we’ve seen the built-in make() function used to instantiate slices and maps (and we’ll see it in part 3 with channels). make() is only for those built-in types. For our own types, we can use the new() function. I find the distinction a bit messy, but I generally dislike the whole distinction between built-in types and types that can be implemented using the language itself. I like how the C++ standard library is implemented in C++, with very little special support from the language itself when something is added to the library.

Go types often have “constructor” functions (not methods) which you should call to properly instantiate the type, but I don’t think there is any way to enforce correct initialization like a default constructor in C++ or Java. For instance:

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

You can anonymously “embed” one struct within an other, like so:

type Person struct {

   Name string

}

 

type Employee struct {

  Person

  Position string

}

 

var a Employee

a.Name = "bob"

a.Position = "builder"

This feels a bit like inheritance in C++ and Java, but this is just containment. It doesn’t give us any real “is a” meaning and doesn’t give us real polymorphism. For instance, you can do this:

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()

Interfaces, which we’ll see later, do give us some sense of an “is a” meaning.

Methods

Unlike C, but like C++ and Java classes, structs in Go can have methods – functions associated with the struct. But the syntax is a little different than in C++ or Java. Methods are declared outside of the struct declaration, and the association is made by specifying a “receiver” before the function name. For instance, this declares (and implements) a DoSomething method for the Thing struct:

func (t Thing) DoSomething() {

  ...

}

Notice that you have to specify a name for the receiver – there is no built-in “self” or “this” instance name. This feels like an unnecessary invitation to inconsistency.

You can use a pointer type instead, and you’ll have to if you want to change anything about the struct instance:

func (t *Thing) ChangeSomething() {

  t.a = 4

}

Because you should also want to keep your code consistent, you’d therefore want to use a pointer type for all method receivers. So I don’t know why the language lets it ever be a struct value type.

Unlike C++ or Java, this lets you check the instance for nil (Go’s null or nullptr), making it acceptable to call your method on a null instance. This reminds me of how Objective-C happily lets you call a method (“send a message to” in Objective-C terminology) on a nil instance, with no crash, even returning a nil or zero return value. I find that undisciplined in Objective-C, and it bothers me that Go allows this sometimes, but not consistently.

Unlike C++ or Java, you can even associate methods with non struct (non class) types. For instance:

type Meters int

type Feet int

 

func (Meters) convertToFeet() (Feet) {

  ...

}

 

Meters m = 10

f := p.convertToFeet()

No equality or comparison operator overloading

In C++, you can overload operator =, !=, <, >, etc, so you can use instances of your type with the regular operators, making your code look tidy:

MyType a = getSomething();

MyType b = getSomethingElse();

if (a == b) {

  ...

}

You can’t do that in Go (or Java, though it has the awkward Comparable interface and equals() method). Only some built-in types are comparable in Go – the numeric types, string, pointers, or channels, or structs or arrays made up of these types. This is an issue when dealing with interfaces, which we’ll see later.

Symbol Visibility: Uppercase or lowercase first letter

Symbols (types, functions, variables) that start with an uppercase letter are available from outside the package. Struct methods and member variables that start with an uppercase letter are available from outside the struct. Otherwise they are private to the package or struct.

For instance:

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 {

...

}

I find this a bit strange. I prefer the explicit public and private keywords in C++ and Java.

Interfaces

Interfaces have methods

If two Go types satisfy an interface then they both have the methods of that interface. This is similar to Java interfaces. A Go interface is also a bit like a completely abstract class in C++ (having only pure virtual methods), but it’s also a lot like a C++ concept (not yet in C++, as of C++17). For instance:

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:

...

You can then use the interface type instead of the specific “concrete” type:

var someCircle *Circle = new(Circle)

var s Shape = someCircle

s.DrawOnSurface(someSurface)

Notice that we use a Shape, not a *Shape (pointer to Shape), even though we are casting from a *Circle (pointer to circle). “Interface values” seem to be implicitly pointer-like, which seems unnecessarily confusing. I guess it would feel more consistent if pointers to interfaces just had the same behaviour as these “interface values”, even if the language had to disallow interface types that weren’t pointers.

Types satisfy interfaces implicitly

However, there is no explicit declaration that a type should implement an interface.

In this way Go interfaces are like C++ concepts, though C++ concepts are instead a purely compile-time feature for use with generic (template) code. Your class can conform to a C++ concept without you declaring that it does. And therefore, like Go interfaces, you can, if you must, use an existing type without changing it.

The compiler still checks that types are compatible, but presumably by checking the types’ list of methods rather than checking a class hierarchy or list of implemented interfaces. For instance:

var a *Circle = new(Circle)

var b Shape = a // OK. The compiler can check that Circle has Shape's methods.

Like C++ with dynamic_cast, Go can also check at runtime. For instance, you can check if one interface value refers to an instance that also satisfies another interface:

// 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)

}

Or we can check that an interface value refers to a particular concrete type. For instance:

// 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 dispatch

Interface methods are also like C++ virtual methods (or any Java method), and interface variables are also like instances of polymorphic base classes. To actually call the interface’s method via an interface variable, the program needs to examine its actual type at runtime and call that type’s specific method. Maybe, as with C++, the compiler can sometimes optimize away that indirection.

This is obviously not as efficient as directly calling a method, identified at compile time, of a templated type in a C++ template. But it is obviously much simpler.

Comparing interfaces

Interface values can be compared sometimes, but this seems like a risky business. Interface values are:

  • Not equal if their types are different.
  • Not equal if their types are the same and only one is nil.
  • Equal if their types are the same, and the types are comparable (see above), and their values are equal.

But if the types are the same, yet those types are not comparable, Go will cause a “panic” at runtime.

Wishing for an implements keyword

In C++ you can, if you wish, explicitly declare that a class should conform to the concept, or you can explicitly derive from a base class, and in Java you must use the “implements” keyword. Not having this with Go would take some getting used to. I’d want these declarations to document my architecture, explicitly showing what’s expected of my”concrete” classes in terms of their general purpose instead of just expressing their that by how some other code happens to use them. Not having this feels fragile.

The book suggests putting this awkward code somewhere to check that a type really implements an interface. Note the use of _ to mean that we don’t need to keep a named variable for the result.

var _ MyInterface = (*MyType)(nil)

The compiler should complain that the conversion is impossible if the type does not satisfy the interface. I think it would be wise this as the very minimum of testing, particularly if your package is providing types that are not really used in the package itself. For me, this is a poor substitute for an obvious compile-time check, using a specific language construct, on the type itself.

Interface embedding

Embedding an interface in an interface

Go has no notion of inheritance hierarchies, but you can “embed” one interface in another, to indicate that a class that satisfies one interface also satisfies the other. For instance:

type Positionable interface {

  SetPosition(x int, y int)

  GetPosition() (x int, y int)

}

 

type Drawable interface {

  drawOnSurface(s Surface) }

}

 

type Shape interface {

  Positionable

  Drawable

}

To satisfy the Shape interface, any type must also satisfy the Drawable and Positionable interfaces. Therefore, any type that satisfies the Shape interface can be used with a method associated with the Drawable or Positionable interfaces. So it’s a bit like a Java interface extending another interface.

Embedding an interface-satisfying struct in a struct

We saw earlier how you can embed one struct in another anonymously. If the contained struct implements an interface, then the containing struct then also implements that interface, with no need for manually-implemented forwarding methods. For instance:

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)

}

This again feels a bit like inheritance.

I actually quite like how the (interfaces of the) anonymously contained structs affect the interface of the parent struct, even with Go’s curious interface system, though I wish the syntax was more obvious about what is happening. It might be nice to have something similar in C++. Encapsulation instead of inheritance (and the Decorator pattern) is a perfectly valid technique, and C++ generally tries to let you do things in multiple ways without having an opinion about what’s best, though that can itself be a source of complexity. But in C++ (and Java), you currently have to hand-code lots of forwarding methods to achieve this and you still need to inherit from something to tell the type system that you support the encapsulated type’s interface.

 

 

转载于:https://my.oschina.net/shannanzi/blog/1587174

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值