Higher level abstraction in Golang is now possible after the introduction of generics.
The addition of generics in Golang fills the missing gap. It also helps in writing abstract code allowing loose coupling.
This post will easily illustrate how to use higher-level abstraction in Golang using generics and interface. Read through it and practice it yourself. Remember, consistent and intelligent practice is the key to success in programming.
Prerequisites
Learn Generics in Golang
Implementation of Achieve Higher level abstraction in Golang using generics and interface
Let us understand this with an example.
Suppose we want to model fruit and vegetable packaging. So we will need a Fruit and Vegetable interface since they are different. Again we have many types of fruits and vegetables. We want to package them in a box, clearly their packaging will also be different. On top of this packaging (Box) will have its property. At this point, it seems complex.
Let us understand this with the below code.
// Implementation of Achieve Higher level abstraction in golang using generics and interface
We will start with defining Fruit interface
type Fruit interface {
Name() string
Color() string
Taste() string
PricePerKg() int
}
Also, let us create struct for apple and mango fruit
type Apple struct {
count int
}
func (a *Apple) Name() string {
return "apple"
}
func (a *Apple) Color() string {
return "green"
}
func (a *Apple) Taste() string {
return "sweet-sour"
}
func (a *Apple) PricePerKg() int {
return 100
}
type Mango struct {
count int
}
func (m *Mango) Name() string {
return "mango"
}
func (m *Mango) Color() string {
return "yellowish-green"
}
func (m *Mango) Taste() string {
return "sweet"
}
func (m *Mango) PricePerKg() int {
return 200
}
Now let us define Vegetable interface
type Vegetable interface {
Name() string
IsFresh() bool
PricePerUnit() int
IsFragile() bool
}
Now we will define Potato and Tomato fruit
type Potato struct {
name string
age int // value in days
}
func (p *Potato) Name() string {
return "potato"
}
func (p *Potato) IsFresh() bool {
return p.age < 2
}
func (p *Potato) PricePerUnit() int {
return 40
}
func (p *Potato) IsFragile() bool {
return false
}
type Tomato struct {
name string
age int // value in days
}
func (t *Tomato) Name() string {
return "tomato"
}
func (t *Tomato) IsFresh() bool {
return t.age < 2
}
func (t *Tomato) PricePerUnit() int {
return 50
}
func (t *Tomato) IsFragile() bool {
return true
}
Now we will proceed to complete box related code. First we will define Boxer interface which will match any type of box
type Boxer interface {
Name() string
PricePerBox() int
}
then we will define boxstructs for fruits and vegetables
type BoxFruit[T Fruit] struct {
item T
weightInKg int
}
func (b *BoxFruit[T]) Name() string {
return "box of fruit of " + b.item.Name()
}
func (b *BoxFruit[T]) PricePerBox() int {
return b.weightInKg * b.item.PricePerKg()
}
type BoxVegetable[T Vegetable] struct {
item T
weightInKg int
}
func (b *BoxVegetable[T]) Name() string {
return "vegetable box of " + b.item.Name()
}
func (b *BoxVegetable[T]) PricePerBox() int {
return b.weightInKg * b.item.PricePerUnit()
}
We will finish this by writing a driver code (main code) as below
func main() {
ap := &Apple{10}
m := &Mango{5}
boxApple := &BoxFruit[*Apple]{ap, 10} //Since *Apple implements Fruit interface
boxMango := &BoxFruit[*Mango]{m, 20} //Since *Mango implements Fruit interface
potato := &Potato{"potato", 1}
boxPotato := &BoxVegetable[*Potato]{potato, 5} //Since *Potato implements Vegetable interface
tomato := &Tomato{"tomato", 2}
boxTomato := &BoxVegetable[*Tomato]{tomato, 7} //Since *Tomato implements Fruit interface
boxes := []Boxer{boxApple, boxMango, boxPotato, boxTomato}
for _, box := range boxes {
fmt.Println(box.Name(), box.PricePerBox())
}
}
///////////////////////////////////////////////////////// Complete code as below //////////////////////////////////////////////////////////////////
Let us apply generics in Golang to decouple the above code and achieve higher level abstraction in Golang
//
package main
import "fmt"
type Fruit interface {
Name() string
Color() string
Taste() string
PricePerKg() int
}
type Apple struct {
count int
}
func (a *Apple) Name() string {
return "apple"
}
func (a *Apple) Color() string {
return "green"
}
func (a *Apple) Taste() string {
return "sweet-sour"
}
func (a *Apple) PricePerKg() int {
return 100
}
type Mango struct {
count int
}
func (m *Mango) Name() string {
return "mango"
}
func (m *Mango) Color() string {
return "yellowish-green"
}
func (m *Mango) Taste() string {
return "sweet"
}
func (m *Mango) PricePerKg() int {
return 200
}
type Vegetable interface {
Name() string
IsFresh() bool
PricePerUnit() int
IsFragile() bool
}
type Potato struct {
name string
age int // value in days
}
func (p *Potato) Name() string {
return "potato"
}
func (p *Potato) IsFresh() bool {
return p.age < 2
}
func (p *Potato) PricePerUnit() int {
return 40
}
func (p *Potato) IsFragile() bool {
return false
}
type Tomato struct {
name string
age int // value in days
}
func (t *Tomato) Name() string {
return "tomato"
}
func (t *Tomato) IsFresh() bool {
return t.age < 2
}
func (t *Tomato) PricePerUnit() int {
return 50
}
func (t *Tomato) IsFragile() bool {
return true
}
type Boxer interface {
Name() string
PricePerBox() int
}
type BoxFruit[T Fruit] struct {
item T
weightInKg int
}
func (b *BoxFruit[T]) Name() string {
return "box of fruit of " + b.item.Name()
}
func (b *BoxFruit[T]) PricePerBox() int {
return b.weightInKg * b.item.PricePerKg()
}
type BoxVegetable[T Vegetable] struct {
item T
weightInKg int
}
func (b *BoxVegetable[T]) Name() string {
return "vegetable box of " + b.item.Name()
}
func (b *BoxVegetable[T]) PricePerBox() int {
return b.weightInKg * b.item.PricePerUnit()
}
func main() {
apple := &Apple{10}
mango := &Mango{5}
boxApple := &BoxFruit[*Apple]{apple, 10} //Since *Apple implements Fruit interface
boxMango := &BoxFruit[*Mango]{mango, 20} //Since *Mango implements Fruit interface
potato := &Potato{"potato", 1}
boxPotato := &BoxVegetable[*Potato]{potato, 5} //Since *Potato implements Vegetable interface
tomato := &Tomato{"tomato", 2}
boxTomato := &BoxVegetable[*Tomato]{tomato, 7} //Since *Tomato implements Fruit interface
boxes := []Boxer{boxApple, boxMango, boxPotato, boxTomato}
for _, box := range boxes {
fmt.Printf("name: %s, price per box: %v\n", box.Name(), box.PricePerBox())
}
}
// output
name: box of fruit of apple, price per box: 1000
name: box of fruit of mango, price per box: 4000
name: vegetable box of potato, price per box: 200
name: vegetable box of tomato, price per box: 350
Program link
FAQs on Higher level abstraction in Golang
Q1 – When do I need to use Higher level abstraction in Golang?
A – Whenever you need to use multiple compositions of abstraction or multiple levels of abstraction in Golang (as the above example in this post clearly illustrates its use case), there you can apply Higher level abstraction in Golang to remove the coupling and write modular code.
Conclusion
In this post, you learned how to use Higher level abstraction in Golang using generics and interfaces (also pay attention to how easy it is to use generics in Golang). Just practice with simple programs then apply it to your projects.
Enjoy the post!