Guides and Best Practices Tech Deep Dives

Golang Embedding

This blog is about embedding in Go language (which many, including me, refer to as “Golang,” because of its domain name, golang.org) and the various ways to do it. But before embedding, it is important to understand some Object-Oriented Programming (OOP) concepts and establish some contexts that may surface a need for embedding. We will follow this with a detailed discussion on embedding and end with a problem that will require the use of all the concepts discussed. I’ve even included some trivia along the way (answers to all of the trivia questions appear at the end of this post).

Contexts and concepts

Inheritance

Inheritance defines an is-a relationship between two entities, where one entity is the base and the other is derived. A derived entity inherits properties and behavior from the base entity. It may be said that “derived is a base,” or “derived is a type of base,” as below:

TCPConn and UDPConn is a Conn.

Association

An association defines a has-a(aggregation) or a owns-a(composition) relationship between two entities, where one entity contains the other as its member field:

Response has a Request — Aggregation

In Aggregation, the lifecycles of both entities are independent and one can exist even without the other:

Response has a Header — Composition

In Composition, the entities’ lifecycles are coupled and the contained entity cannot live independently.

Inheritance vs. association

Feature Inheritance Association
Relationship type is-a has-a/owns-a
Lifecycle coupled decoupled
Defined At compiled time runtime
Coupling tight loose
Access control no yes
Reuse style white-box black-box
Ease of testing hard easy with mocking
Forwarding methods not required required
Base class type abstract (pure virtual) or concrete concrete only

As Inheritance is “white-box” reuse, it breaks encapsulation by exposing the base class’ private entities to the derived class. Use Inheritance where there is a strict has-a relationship. Association would lead to forwarding all methods.

Golang embedding

Inheritance, when used incorrectly, can lead to less flexible and highly coupled designs. It is recommended to use association over the inheritance. 

In “Design Patterns: Elements of Reusable Object-Oriented Software,” the authors (Erich Gamma, John Vlissides, Richard Helm, and Ralph Johnson, a.k.a. the “Gang of Four”) say you should favor object composition over class inheritance. There is a lot of debate and conflict on whether Golang is truly an object-oriented language, because it does not support inheritance, although polymorphism can be achieved through interfaces. This has been answered in Golang FAQs.

According to Rob Pike, one of the language’s major contributors, Golang is “a profoundly object-oriented language.” (See https://www.youtube.com/watch?v=rKnDgT73v8s&t=750s) Russ Cox, lead developer for Go at Google, the language is “object oriented, but it’s not type oriented.” (See https://www.youtube.com/watch?v=yx7lmuwUNv8&t=931s)

Golang does not support inheritance but supports association via embedding. This can be done in three ways:

  1. Interfaces in interfaces
  2. Interfaces in structs
  3. Structs in structs

Interfaces in interfaces

The basics:

  • If an interface is embedded inside another interface, the embedding interface becomes the union of its methods and the methods of all the embedded interfaces.
  • Interface embedding is shorthand for redeclaring all the methods of the embedded interfaces.
  • Before Go 1.14, embedding two interfaces with the same methods would throw an error. This is allowed in newer versions.
Graphical user interface, application, WordDescription automatically generated
(Get the code: https://play.golang.org/p/4cJtnI5q53m)

Trivia Question #1: What if both the embedded interfaces have a method(s) with the same name but with different signature(s)?

Graphical user interface, applicationDescription automatically generated with medium confidence

Interfaces in structs

The basics:

  • Embed an interface in place of a struct if the struct has many forms and all the forms adhere to an interface.
  • The concrete struct can be assigned to the interface when creating the embedding struct.
  • All the methods of the interface are promoted to the embedding struct.
  • The embedding struct has the option to override the implementation of the embedded struct.
  • By default, the interface will be nil until assigned and any call to forwarded methods will panic at runtime.
Graphical user interface, text, application, emailDescription automatically generated
(Get the code: https://play.golang.org/p/uvcettmvhed)

Trivia Question #2: What if both the embedded interfaces have a method(s) with the same name and same signature(s)?

Graphical user interface, text, applicationDescription automatically generated

Structs in structs

The basics:

  • A struct can be embedded in another struct directly without a variable name. This will lead to a promotion of all the fields and methods of the embedded struct, which can be called directly by the embedding struct.
  • Non-exported fields or methods will not be promoted across packages.
  • The forwarding will not happen if there is a conflict in the field or method name of the embedding and embedded struct. 
  • The conflicting field and method will have to be explicitly called using the embedded struct name.
Graphical user interface, text, applicationDescription automatically generated
(Get the code: https://play.golang.org/p/D2w4KxpiI3C)

What if we do not want to expose all the properties or methods of the base struct?

  • A struct can be declared as a field in another struct using any variable name. This will not promote any fields or methods of the enclosed struct.
  • The methods will have to be forwarded by the enclosing struct or the caller will have to use the field variable to access the composed struct.
  • Use this over direct embedding if only selective features of the enclosed struct are to be exposed.
Graphical user interface, text, application, emailDescription automatically generated
(Get the code: https://play.golang.org/p/hXMF_9ZKhi9)

Trivia Question #3: What if both the embedded structs have a method(s) with the same name and same signature(s)?

Graphical user interface, applicationDescription automatically generated

Trivia question #4: (A combination of trivia questions #2 and #3.) Can you have duplicate methods in embedded interfaces? Duplicate methods in embedded structs? Can you call the duplicate method on the embedding struct that implements the embedding interface?

Graphical user interface, text, applicationDescription automatically generated

Putting this into use: how to call derived object functions from base object functions

Consider an interface with methods Start() and Process(). Assume Start() must call Process(). Let Foo and Bar be two structs that implement the above interface. Now if both the derived structs have the same code for Start(), how do we avoid duplicating code for the Start()?

The obvious solution is to introduce a base struct that implements the Start() and both Foo and Bar embed the base struct. Now how do we make the base object’s Start() call the corresponding Process() of derived objects Foo and Bar?

This idea is to create a cycle between the base struct and the derived structs Foo and Bar.

  1. Assign the derived structs base interface to itself and return the derived struct. Foo: Returns its own instance by assigning itself to the base interface.
  2. Assign the base structs interface to the derived struct and return the base struct. Bar: Returns base instance by embedding itself.
Graphical user interface, text, applicationDescription automatically generated
(Get the code: https://play.golang.org/p/1xU0BemAYsV)

Though Golang does not directly support inheritance, it can be “mimicked” in some sense using embedding. Embedding is the bridge between Golang and OOP, making it a very powerful and important concept to understand. Hope you had some takeaway from this blog. Keep a watch on the VMware OCTO blog for future posts.

Trivia Answers:

  1. Error: duplicate method Close
  2. Error: ambiguous selector ps.Close
  3. Error: ambiguous selector rw.Close
  4. Error: PrinterScanner.Close is ambiguous/cannot use ps (type PrinterScanner) as type ReaderWriter in argument to Close: PrinterScanner does not implement ReaderWriter (missing close method)

Playground Links

TopicLink
Interfaces in interfaces https://play.golang.org/p/4cJtnI5q53m
Interfaces in structs https://play.golang.org/p/uvcettmvhed
Structs in structs by embedding https://play.golang.org/p/D2w4KxpiI3C
Structs in structs as fields https://play.golang.org/p/hXMF_9ZKhi9
Putting it to use https://play.golang.org/p/1xU0BemAYsV

Further Reading:

  1. https://en.wikipedia.org/wiki/Design_Patterns
  2. https://www.oreilly.com/library/view/design-patterns-elements/0201633612/
  3. https://golang.org/doc/effective_go#embedding

Comments

Leave a Reply

Your email address will not be published. Required fields are marked *