Coming from C++/Python, where OOP is much better represented, and discovering go (now everything is web or web related, right?!) I too stumbled upon this issue. I feel that OOP in go is only half-baked. With embedding (struct's anonymous fields), methods of the inner type come along for free, inducing the idea of inheritance, only to learn later on the limitations. However, playing a bit with embedded interfaces within structs and with a bit of discipline, C++-like constructors, inheritance, polymorphism, and methods override can be emulated.
Considering the example - https://play.golang.org/p/nqt0haPYt2x
package main
import (
"bytes"
"fmt"
"log"
"math"
"unsafe"
)
//Emulate C++ like polymorphism in go, through template method design pattern
//========================== Shape interface ==============================
//like C++ abstract classes
type Shape interface {
Area() float32 //Shape's area
Perimeter() float32 //Shape's perimeter
Name() string //Shape's name (like rectangle, circle, square etc.)
}
//====================== PrintableShapeInfo =============================
type PrintableShapeInfo struct {
Shape //like C++ inheritance, although go has no such a thing
preetyPrintPrefix string
}
//Init a new PrintableShapeInfo object. The method is distinct so that it can be called from other contexts as well
//
//Remark: emulates the C++ constructor init part
func (printableShapeInfo *PrintableShapeInfo) Init(preetyPrintPrefix string) {
printableShapeInfo.preetyPrintPrefix = preetyPrintPrefix
}
//The central method emulates the template method design pattern. It prints some info about a shape by dynamically calling (through pointers) the right methods
//
//Remark: the design patterns best practices recommend to favor composition over inheritance (i.e. model a ShapeInfoPrinter class, which takes a Shape interface and prints its info),
//for the sake of showcasting the template method pattern, the "go's inheritange" like model was chosen
func (printableShapeInfo *PrintableShapeInfo) PrintInfo() {
log.Println("PrintableShapeInfo::PrintInfo")
fmt.Printf("%s PrintableShapeInfo::PrintInfo - %s:\n",
printableShapeInfo.preetyPrintPrefix, printableShapeInfo.Name()) //dynamically calls (through a pointer) a shape's Name method (like Rectangle.Name or Circle.Name or Square.Name)
fmt.Printf("\tArea: %f\n", printableShapeInfo.Area()) //dynamically calls (through a pointer) a shape's Area method (like Rectangle.Area or Circle.Area or Square.Area)
fmt.Printf("\tPerimeter: %f\n", printableShapeInfo.Perimeter()) //dynamically calls (through a pointer) a shape's Perimeter method (like Rectangle.Perimeter or Circle.Perimeter or Square.Perimeter)
}
//====================== Rectangle =============================
type Rectangle struct {
PrintableShapeInfo //like C++ inheritence, although go has no such a thing
width float32 //rectangle's width
height float32 //rectangle's heigh
}
//Creates and init a new rectangle object and properly set its Shape's interface methods set (similar to C++ class' vtable)
//
//Remark: emulates the C++ constructor
func NewRectangle(width float32, height float32) *Rectangle {
log.Println("NewRectangle")
rectangle := new(Rectangle) //allocate data
rectangle.Shape = rectangle //set the Shape's specific vtable with the Rectangle's methods. Critical for template method pattern
rectangle.Init(width, height) //init class
return rectangle
}
//Init a new rectangle object. The method is distinct so that it can be called from other contexts as well (such as a square Init method. See below)
//
//Remark: emulates the C++ constructor init part
func (rectangle *Rectangle) Init(width float32, height float32) {
log.Println("Rectangle::Init")
//call the base's PrintableShapeInfo struct Init method
rectangle.PrintableShapeInfo.Init("###")
rectangle.width = width
rectangle.height = height
}
//Compute the rectangle's area
func (rectangle *Rectangle) Area() float32 {
log.Println("Rectangle::Area")
return float32(rectangle.width * rectangle.height)
}
//Compute the rectangle's perimeter
func (rectangle *Rectangle) Perimeter() float32 {
log.Println("Rectangle::Perimeter")
return float32(2 * (rectangle.width + rectangle.height))
}
//Get the rectangle's object name
func (rectangle *Rectangle) Name() string {
log.Println("Rectangle::Name")
return "rectangle"
}
//====================== Circle =============================
type Circle struct {
PrintableShapeInfo //like C++ inheritence, although go has no such a thing
radius float32 //circle's radius
}
//Creates and init a new circle object and properly set its Shape's interface methods set (similar to C++ class' vtable)
//
//Remark: emulates the C++ constructor
func NewCircle(radius float32) *Circle {
log.Println("NewCircle")
circle := new(Circle) //allocate data
circle.Shape = circle //set the Shape's specific vtable with the Rectangle's methods. Critical for template method pattern
circle.Init(radius) //init class
return circle
}
//Init a new circle object. The method is distinct so that it can be called from other contexts as well if needed
//
//Remark: emulates the C++ constructor init part
func (circle *Circle) Init(radius float32) {
log.Println("Circle::Init")
//call the base's PrintableShapeInfo struct Init method
circle.PrintableShapeInfo.Init("ooo")
circle.radius = radius
}
//Compute the circle's area
func (circle *Circle) Area() float32 {
log.Println("Circle::Area")
return math.Pi * float32(circle.radius*circle.radius)
}
//Compute the circle's perimeter
func (circle *Circle) Perimeter() float32 {
log.Println("Circle::Perimeter")
return 2 * math.Pi * float32(circle.radius)
}
//Get the circle's object name
func (circle *Circle) Name() string {
log.Println("Circle::Name")
return "circle"
}
//====================== Rectangle =============================
//Implement Square in terms of Rectangle
type Square struct {
Rectangle //like C++ inheritance, although go has no such a thing
}
//Creates and init a new square object and properly set its Shape's interface methods set (similar to C++ class' vtable)
//
//Remark: emulates the C++ constructor init
func NewSquare(width float32) *Square {
log.Println("NewSquare")
square := new(Square) //allocate data
square.Shape = square //set the Shape's specific vtable with the Rectangle's methods. Critical for template method pattern
square.Init(width) //init class
return square
}
//Init a new square object. The method is distinct so that it can be called from other contexts as well if needed
//
//Remark: emulates the C++ constructor init part
func (square *Square) Init(width float32) {
log.Println("Square::Init")
//since the Rectangle field is anonymous it's nice that we can directly call its un-overwritten methods but we can still access it, as named Rectangle, along with its (even overwritten) methods
square.Rectangle.Init(width, width) //call Rectangle's init to initialize its members. Since Square is implemented in Rectangle's terms, there nothing else needed
}
//Compute the square's area
func (square *Square) Area() float32 {
log.Println("Square::Area")
//since the Rectangle field is anonymous it's nice that we can directly call it's un-overwritten methods but we can still access it, as named Rectangle, along with it's (even overwritten) methods
return square.Rectangle.Area()
}
//Compute the square's perimeter
func (square *Square) Perimeter() float32 {
log.Println("Square::Perimeter")
//since the Rectangle field is anonymous it's nice that we can directly call it's un-overwritten methods but we can still access it, as named Rectangle, along with it's (even overwritten) methods
return square.Rectangle.Perimeter()
}
//Get the square's object name
func (square *Square) Name() string {
log.Println("Square::Name")
return "square"
}
func main() {
//initialize log subsystem so that we can display them at the main's end
// bufWriter := bytes.NewBuffer()
logStringWriter := bytes.NewBufferString("")
log.SetOutput(logStringWriter)
rectangle := NewRectangle(2, 3) //create a Rectangle object
rectangle.PrintInfo() //should manifest polymorphism behavior by calling Rectangle's Area, Perimeter and Name methods
circle := NewCircle(2) //create a Circle object
circle.PrintInfo() //should manifest polymorphism behavior by calling Circle's Area, Perimeter and Name methods
square := NewSquare(3) //create a Square object
square.PrintInfo() //should manifest polymorphism behavior by calling Square's Area, Perimeter and Name methods
//print constructs sizes
fmt.Printf(`
Go constructs sizes:
Shape interface size as seen by Rectangle struct: %d
`, unsafe.Sizeof(rectangle.Shape))
fmt.Printf("\tRectangle struct size: %d", unsafe.Sizeof(rectangle))
fmt.Printf(`
Shape interface size as seen by Circle struct: %d
`, unsafe.Sizeof(circle.Shape))
fmt.Printf("\tCircle struct size: %d", unsafe.Sizeof(circle))
fmt.Printf(`
Shape interface size as seen by Square struct: %d
`, unsafe.Sizeof(square.Shape))
fmt.Printf("\tCircle struct size: %d", unsafe.Sizeof(square))
//print the logs
fmt.Println("\n\nDumping traces")
fmt.Print(logStringWriter)
return
}
The central method (template method) is PrintInfo which, called for any defined shape, works as expected, by calling the right Area, Perimeter, and Name methods. Ex. circle.PrintInfo() will call circle.Area, circle.Perimeter and circle.Name.
The constructor functions, NewRectangle, NewCircle, and NewSquare construct shape objects and they're split into three steps:
- space allocation
- methods set (C++ like vtable) init, needed for polymorphic behavior
- struct members initialization, through Init methods
The struct member initialization is a distinct step for better code reuse. For example, Rectangle Init calls the base PrintableShapeInfo Init method while the Square Init method calls the base Rectangle Init (which calls PrintableShapeInfo Init, as said before).
Also, due to interfaces embedding, the object sizes increases only a bit, with a pair of pointers to the methods set and data area as can be seen in the example output.
I think the code looks pretty decent and the only concern being if specifically setting the Shape's interface method set (as the case of NewRectangle, NewCircle, and NewSquare functions) would trigger some side effects, as the code appears to work correctly?!