Skip to content

Embedding

Johnny Boursiquot edited this page Jun 7, 2024 · 1 revision

Go is not an OOP language in the classical sense. There is no inheritance hierachy, one of the hallmarks of object-oriented languages. Go favors a form of composition known as embedding which allows a struct to include fields and methods of another struct or interface directly.

Basics

https://go.dev/play/p/5cHjucMiV_d

package main

import (
	"fmt"
)

type Person struct {
	Name string
	Age  int
}

func (p Person) Greet() {
	fmt.Printf("Hello, my name is %s and I am %d years old.\n", p.Name, p.Age)
}

type Employee struct {
	Person
	EmployeeID string
}

func (e Employee) ShowEmployeeID() {
	fmt.Printf("Employee ID is %s.\n", e.EmployeeID)
}

func main() {
	emp := Employee{
		Person: Person{
			Name: "James Baldwin",
			Age:  30,
		},
		EmployeeID: "E12345",
	}

	// Access fields and methods from the embedded struct
	emp.Greet()             // Calls the Greet method from the Person struct
	emp.ShowEmployeeID()    // Calls the ShowEmployeeID method from the Employee struct
}

Method Overrides

https://go.dev/play/p/9SrnktNszk3

package main

import (
	"fmt"
)

type Person struct {
	Name string
	Age  int
}

func (p Person) Greet() {
	fmt.Printf("Hello, my name is %s and I am %d years old.\n", p.Name, p.Age)
}

type Employee struct {
	Person
	Company    string
	EmployeeID string
}

// Override the Greet method in the Employee struct
func (e Employee) Greet() {
	fmt.Printf("Hello, my name is %s and I work at %s with ID %s.\n", e.Name, e.Company, e.EmployeeID)
}

func main() {
	emp := Employee{
		Person: Person{
			Name: "Jane Doe",
			Age:  28,
		},
		Company:    "Acme Corp",
		EmployeeID: "E67890",
	}

	// Access overridden method
	emp.Greet() // Calls the Greet method from the Employee struct
}

Method Overriding and Ambiguity

When you embed multiple structs that have methods with the same name, Go will not allow you to directly call the ambiguous method without specifying which embedded struct to use.

https://go.dev/play/p/6rdPcK-XutK

package main

import (
	"fmt"
)

type Person struct {
	Name string
}

func (p Person) Show() {
	fmt.Printf("Person: %s\n", p.Name)
}

type Contact struct {
	Phone string
}

func (c Contact) Show() {
	fmt.Printf("Phone: %s\n", c.Phone)
}

type Employee struct {
	Person
	Contact
}

func main() {
	emp := Employee{
		Person:  Person{Name: "John Doe"},
		Contact: Contact{Phone: "123-456-7890"},
	}

	// This will cause a compile-time error due to ambiguity
	emp.Show()

	// You need to specify which Show method to call
	emp.Person.Show()
	emp.Contact.Show()
}

Field Name Conflicts

If the embedding struct and the embedded struct have fields with the same name, the field in the embedding struct will shadow the field in the embedded struct.

https://go.dev/play/p/FcEQEqEu4ET

package main

import (
	"fmt"
)

type Person struct {
	Name string
}

type Employee struct {
	Person
	Name string // This shadows Person's Name
}

func main() {
	emp := Employee{
		Person: Person{Name: "John Doe"},
		Name:   "Jane Doe",
	}

	// Accessing the shadowed field directly
	fmt.Println(emp.Name)        // Outputs: Jane Doe
	fmt.Println(emp.Person.Name) // Outputs: John Doe
}

Interface Embedding

Interface embedding provides a powerful mechanism to combine behaviors.

https://go.dev/play/p/GTlAPX9xrco

package main

import (
	"fmt"
)

type Reader interface {
	Read() string
}

type Writer interface {
	Write(data string)
}

type Closer interface {
	Close()
}

type ReadWriterCloser interface {
	Reader
	Writer
	Closer
}

type File struct {
	data   string
	closed bool
}

func (f *File) Read() string {
	if f.closed {
		return "Cannot read, file is closed."
	}
	return f.data
}

func (f *File) Write(data string) {
	if f.closed {
		fmt.Println("Cannot write, file is closed.")
		return
	}
	f.data = data
}

func (f *File) Close() {
	f.closed = true
	fmt.Println("File closed.")
}

func main() {
	var device ReadWriterCloser = &File{}

	device.Write("Hello, Gophers!")
	fmt.Println(device.Read()) // Outputs: Hello, Gophers!

	device.Close()

	device.Write("Trying to write after closing")
	fmt.Println(device.Read()) // Outputs: Cannot read, file is closed.
}

Though not exactly the same, does the above remind you of anything from the standard library itself? See ReadWriteCloser.

Caution: If two embedded interfaces have methods with the same name and signature, the resulting interface will include only one of those methods. This can lead to unexpected behavior if you expect both methods to be available separately.

https://go.dev/play/p/PZepCH9dRL0

package main

import "fmt"

type Reader interface {
	Read() string
}

type ReadingWriter interface {
	Write(data string)
	Read() string
}

type ReadWriter interface {
	Reader
	ReadingWriter
}

type MyType struct{}

func (ms MyType) Read() string {
	return "Reading data"
}

func (ms MyType) Write(data string) {
	fmt.Println("Writing data:", data)
}

func main() {
	var rw ReadWriter = MyType{}

	// Only one Read method is available, not two separate Read methods
	fmt.Println(rw.Read()) // Outputs: Reading data
	rw.Write("some data")  // Outputs: Writing data: some data
}