Consider a wall outlet (power provider) and devices (power consumers) as examples.
A wall outlet knows nothing about the devices (power consumers). However, it can charge devices after their power adapter cable is plugged in. Devices like lamps and blenders require a power adapter to take electricity from a wall outlet.
The following is a crude implementation based on our initial ideas.
dishwasher.go
type dishwasher struct {
brand string
model string
numOfWashCycles int
price float64
}
func newDishWasher(brand string, model string, numOfWashCycles int,
price float64) *dishwasher {
return &dishwasher{
brand: brand,
model: model,
numOfWashCycles: numOfWashCycles,
price: price,
}
}
func (dw *dishwasher) consume() {
fmt.Printf(" -> The Dishwasher (%s - %s) is powered\n", dw.brand,
dw.model)
}
hairdryer.go
type hairDryer struct {
brand string
model string
numOfHeatSetting int
price float64
}
func newHairDryer(brand string, model string, numOfHeatSetting
int, price float64) *hairDryer {
return &hairDryer{
brand: brand,
model: model,
numOfHeatSetting: numOfHeatSetting,
price: price,
}
}
func (hd *hairDryer) consume() {
fmt.Printf(" -> The Hair Dryer (%s - %s) is powered\n", hd.brand,
hd.model)
}
walloutlet.go
type wallOutlet struct {
// Connected Devices
connectedDishwasher *dishwasher
connectedHairDryer *hairDryer
}
func newWallOutlet() *wallOutlet {
return &wallOutlet{
connectedDishwasher: &dishwasher{},
connectedHairDryer: &hairDryer{},
}
}
func (w *wallOutlet) plugDishwasher(dw *dishwasher) {
w.connectedDishwasher = dw
}
func (w *wallOutlet) plugHairDryer(hd *hairDryer) {
w.connectedHairDryer = hd
}
func (w *wallOutlet) sendPower() {
fmt.Println("Wall Outlet")
w.connectedDishwasher.consume()
w.connectedHairDryer.consume()
}
main.go
func main() {
wallOutlet := newWallOutlet()
hairDryer := newHairDryer("Dyson", "Supersonic", 4, 579.99)
dishwasher := newDishWasher("Bosch", "SHE3AEM2N", 2, 794.99)
// Plug in devices
wallOutlet.plugHairDryer(hairDryer)
wallOutlet.plugDishwasher(dishwasher)
wallOutlet.sendPower()
}
Result
Wall Outlet
-> The Dishwasher (Bosch - SHE3AEM2N) is powered
-> The Hair Dryer (Dyson - Supersonic) is powered
From the example above, we can see that the struct of a hair dryer and a dishwasher are very similar. They contain the same behavior (method) known as 'consume'.
Although they appear to be similar, they are actually different struct types. As a result, the 'wallOutlet' struct must include a number of plug methods for connecting various types of power consumers.
Furthermore, adding new fields to the 'wallOutlet' struct will be challenging to maintain in the future.
Interface
To improve it, Go provides 'Interface', which allows us to design an abstract type that simply outlines the intended behaviors (methods) and does not go into detail about implementation. It is a similar concept to other OOP languages.
* 'Interface' is a Protocol.
* 'Interface' is an abstract type.
* There is no implementation in the 'Interface'.
Take a look below for further information on the 'Interface' syntax.
Format
type powerConsumer interface {
consume()
}
* The powerConsumer is the interface type.
* The interface name is better to end in 'er' (suffix) as a naming convention.
* We simply need to identify the expected behaviors (methods); we don't need to know how people will implement them.
* Unlike other OOP languages, there is no 'implements' keyword. When a type implements all of the interface's methods, it automatically satisfies it.
Based on the Go 'interface' description above, we can apply the previous example's common action 'consume' to the powerConsumer interface.
Then, using the 'powerConsumer' abstract type, we may tract/iterate the actual data type, such as dishwasher and hair dryer.
dishwasher.go
type dishwasher struct {
brand string
model string
numOfWashCycles int
price float64
}
func newDishWasher(brand string, model string, numOfWashCycles int,
price float64) *dishwasher {
return &dishwasher{
brand: brand,
model: model,
numOfWashCycles: numOfWashCycles,
price: price,
}
}
func (dw *dishwasher) consume() {
fmt.Printf(" -> The Dishwasher (%s - %s) is powered\n", dw.brand,
dw.model)
}
hairdryer.go
type hairDryer struct {
brand string
model string
numOfHeatSetting int
price float64
}
func newHairDryer(brand string, model string, numOfHeatSetting
int, price float64) *hairDryer {
return &hairDryer{
brand: brand,
model: model,
numOfHeatSetting: numOfHeatSetting,
price: price,
}
}
func (hd *hairDryer) consume() {
fmt.Printf(" -> The Hair Dryer (%s - %s) is powered\n", hd.brand,
hd.model)
}
walloutlet.go
// Abstract type
type powerConsumer interface {
consume()
}
type wallOutlet struct {
// Connected Devices
powerConsumers []powerConsumer
}
func newWallOutlet() *wallOutlet {
return &wallOutlet{
powerConsumers: []powerConsumer{},
}
}
func (w *wallOutlet) plug(pc powerConsumer) {
w.powerConsumers = append(w.powerConsumers, pc)
}
func (w *wallOutlet) sendPower() {
fmt.Println("Wall Outlet")
for _, pc := range w.powerConsumers {
pc.consume()
}
}
main.go
func main() {
wallOutlet := newWallOutlet()
hairDryer := newHairDryer("Dyson", "Supersonic", 4, 579.99)
dishwasher := newDishWasher("Bosch", "SHE3AEM2N", 2, 794.99)
// Plug in devices
wallOutlet.plug(hairDryer)
wallOutlet.plug(dishwasher)
wallOutlet.sendPower()
}
Result
Wall Outlet
-> The Dishwasher (Bosch - SHE3AEM2N) is powered
-> The Hair Dryer (Dyson - Supersonic) is powered
From the example above, since dishwasher and hair dryer do implement consume() function, they automatically satisfy the 'powerComsumer' interface.
More details in Interface
Zero value of interface is nil.
Ex:
var p powerConsumer
fmt.Printf("Zero value of interface: %v\n", p)
Result:
Zero value of interface: <nil>
An interface value (powerConsumer) wraps and hides the dynamic value (dishwasher).
You can change the dynamic value to different value in the runtime as long as the type of the assigned value can satisfy the interface.
Ex:
p = hairDryer
p.consume()
p = dishwasher
p.consume()
Result:
-> The Hair Dryer (Dyson - Supersonic) is powered
-> The Dishwasher (Bosch - SHE3AEM2N) is powered
Using interface can help us to collect the data type supporting all the methods defined in the interface.
Then we can easily iterate the collection and call their methods which is defined in the interface.
But we will have troubles to access the fields or methods which is not defined in the interface since we do not know its real data type.
Ex:
// We can access the fields and methods through the concrete type
hairDryer.changePrice(100)
// But we cannot access the hidden fields or methods throguh interface
p = hairDryer
p.changePrice(100)
Compile Error:
p.changePrice undefined (type powerConsumer has no field or
method changePrice)
From the example above, we added a changePrice() method into the hairDryer struct, and we want to call this method from the powerConsumer interface only if dynamic value type is the dishwasher struct type.
How can we do that? Go provides "type assertion" to extract the hidden value and its hidden type.
Type Assertion
Type assertion allows you to extract the dynamic value.
Format
* pc is "the interface value" that you want to extract the dynamic value.
* *dishwasher is the "type name" of the dynamic value that you want to extract.
* It will return the hidden dishwasher value (dynamic value), and you can use this reference to access its fields or attach methods.
dishwasher.go
type dishwasher struct {
brand string
model string
numOfWashCycles int
price float64
}
func newDishWasher(brand string, model string, numOfWashCycles int,
price float64) *dishwasher {
return &dishwasher{
brand: brand,
model: model,
numOfWashCycles: numOfWashCycles,
price: price,
}
}
func (dw *dishwasher) consume() {
fmt.Printf(" -> The Dishwasher (%s - %s) is powered\n", dw.brand,
dw.model)
}
func (dw *dishwasher) print() {
fmt.Println("Dishwasher - Specifications")
fmt.Printf("\tBrand: %s\n", dw.brand)
fmt.Printf("\tModel: %s\n", dw.model)
fmt.Printf("\tNumber of Wash Cycles: %d\n", dw.numOfWashCycles)
fmt.Printf("\tPrice: %.2f\n", dw.price)
}
hairdryer.go
type hairDryer struct {
brand string
model string
numOfHeatSetting int
price float64
}
func newHairDryer(brand string, model string, numOfHeatSetting
int, price float64) *hairDryer {
return &hairDryer{
brand: brand,
model: model,
numOfHeatSetting: numOfHeatSetting,
price: price,
}
}
func (hd *hairDryer) consume() {
fmt.Printf(" -> The Hair Dryer (%s - %s) is powered\n", hd.brand,
hd.model)
}
walloutlet.go
// Abstract type
type powerConsumer interface {
consume()
}
type wallOutlet struct {
// Connected Devices
powerConsumers []powerConsumer
}
func newWallOutlet() *wallOutlet {
return &wallOutlet{
powerConsumers: []powerConsumer{},
}
}
func (w *wallOutlet) plug(pc powerConsumer) {
w.powerConsumers = append(w.powerConsumers, pc)
}
func (w *wallOutlet) sendPower() {
fmt.Println("Wall Outlet")
for _, pc := range w.powerConsumers {
pc.consume()
}
}
func (w *wallOutlet) print() {
fmt.Println("Wall Outlet Connected Devices")
for _, pc := range w.powerConsumers {
// Extract the dynamic value
// Only dishwasher type support print method
dw, isDishwasher := pc.(*dishwasher)
if !isDishwasher {
continue
}
dw.print()
}
}
main.go
func main() {
wallOutlet := newWallOutlet()
hairDryer := newHairDryer("Dyson", "Supersonic", 4, 579.99)
dishwasher := newDishWasher("Bosch", "SHE3AEM2N", 2, 794.99)
// Plug in devices
wallOutlet.plug(hairDryer)
wallOutlet.plug(dishwasher)
wallOutlet.sendPower()
}
Result
Wall Outlet
-> The Dishwasher (Bosch - SHE3AEM2N) is powered
-> The Hair Dryer (Dyson - Supersonic) is powered
Also, rather to inspecting types, we can use type assertion to determine whether the interface value contains the desired method.
walloutlet.go
func (w *wallOutlet) print2() {
fmt.Println("Wall Outlet Connected Devices")
// Declare the interface in function scope level
type printer interface {
print()
}
for _, pc := range w.powerConsumers {
// Check whether the interface value include methods you want or not
if p, ok := pc.(printer); ok {
p.print()
}
}
}
Empty Interface
Format
type empty_interface_name interface {}
* It does not have a method.
* It is empty - it says nothing
* Every type satisfies it.
* It can represent any type of value.
We can use the simplified Format:
Format
* You can store any type of value in an empty interface value.
Ex:
var emptyInterfaceVar interface{}
// Slice
emptyInterfaceVar = []int{1, 2, 3}
fmt.Println(emptyInterfaceVar)
// Map
emptyInterfaceVar = map[int]bool{
1: true,
2: false,
}
fmt.Println(emptyInterfaceVar)
// string
emptyInterfaceVar = "Hello World"
fmt.Println(emptyInterfaceVar)
// int
emptyInterfaceVar = 3
fmt.Println(emptyInterfaceVar)
Result:
[1 2 3]
map[1:true 2:false]
Hello World
3
* But you cannot directly use the dynamic value of an empty interface value.
Ex:
var emptyInterfaceVar interface{}
// int
emptyInterfaceVar = 3
fmt.Println(emptyInterfaceVar)
// We cannot directly use the dynamic value of an empty inteface value
fmt.Println(emptyInterfaceVar * 3)
Result:
invalid operation: emptyInterfaceVar * 3 (mismatched types interface{}
and int)
We need to extract the dynamic value by type assertion first.
Ex:
var emptyInterfaceVar interface{}
// int
emptyInterfaceVar = 3
// The dynamic type of 'emptyInterfaceVar' is int
// The dynamic value of 'emptyInterfaceVar' is 3
// We need to extract the dynamic value by type assertion before using it.
fmt.Println(emptyInterfaceVar.(int) * 3)
Result:
Take the following as an example.
dishwasher.go
type dishwasher struct {
brand string
model string
numOfWashCycles int
price float64
}
func newDishWasher(brand string, model string, numOfWashCycles int,
price float64) *dishwasher {
return &dishwasher{
brand: brand,
model: model,
numOfWashCycles: numOfWashCycles,
price: price,
}
}
func (dw *dishwasher) consume() {
fmt.Printf(" -> The Dishwasher (%s - %s) is powered\n", dw.brand,
dw.model)
}
func (dw *dishwasher) print() {
fmt.Println("Dishwasher - Specifications")
fmt.Printf("\tBrand: %s\n", dw.brand)
fmt.Printf("\tModel: %s\n", dw.model)
fmt.Printf("\tNumber of Wash Cycles: %d\n", dw.numOfWashCycles)
fmt.Printf("\tPrice: %.2f\n", dw.price)
pd := format(dw.purchaseDate)
fmt.Printf("\tPurchase Date: %s\n", pd)
}
func format(v interface{}) string {
if v == nil {
return "unknown"
}
var t int
if v, ok := v.(int); ok {
t = v
}
if v, ok := v.(string); ok {
t, _ = strconv.Atoi(v)
}
u := time.Unix(int64(t), 0)
return u.String()
}
hairdryer.go
type hairDryer struct {
brand string
model string
numOfHeatSetting int
price float64
}
func newHairDryer(brand string, model string, numOfHeatSetting
int, price float64) *hairDryer {
return &hairDryer{
brand: brand,
model: model,
numOfHeatSetting: numOfHeatSetting,
price: price,
}
}
func (hd *hairDryer) consume() {
fmt.Printf(" -> The Hair Dryer (%s - %s) is powered\n", hd.brand,
hd.model)
}
walloutlet.go
// Abstract type
type powerConsumer interface {
consume()
}
type wallOutlet struct {
// Connected Devices
powerConsumers []powerConsumer
}
func newWallOutlet() *wallOutlet {
return &wallOutlet{
powerConsumers: []powerConsumer{},
}
}
func (w *wallOutlet) plug(pc powerConsumer) {
w.powerConsumers = append(w.powerConsumers, pc)
}
func (w *wallOutlet) sendPower() {
fmt.Println("Wall Outlet")
for _, pc := range w.powerConsumers {
pc.consume()
}
}
func (w *wallOutlet) print() {
fmt.Println("Wall Outlet Connected Devices")
for _, pc := range w.powerConsumers {
// Extract the dynamic value
// Only dishwasher type support print method
dw, isDishwasher := pc.(*dishwasher)
if !isDishwasher {
continue
}
dw.print()
}
}
main.go
func main() {
wallOutlet := newWallOutlet()
hairDryer := newHairDryer("Dyson", "Supersonic", 4, 579.99)
dishwasher := newDishWasher("Bosch", "SHE3AEM2N", 2, 794.99)
// Plug in devices
wallOutlet.plug(hairDryer)
wallOutlet.plug(dishwasher)
wallOutlet.sendPower()
}
In summary, Go can let you write type safety code, but empty interface can break the rule and make the code hard to maintain.
Therefore, try not use it only if really necessary.
Type Switch
Detects and extracts the dynamic value from an interface value.
Format
switch e := v.(type) {
// e is an int here
case string:
// e is a string here
default:
// e's type equals to v's type
}
* v is an interface value.
* v.(type) will extract the type from the interface value v.
* e is the extracted value.
* The type of e will be changed depending on the extracted value.
* Type switch compares types instead of values.
dishwasher.go
type dishwasher struct {
brand string
model string
numOfWashCycles int
price float64
}
func newDishWasher(brand string, model string, numOfWashCycles int,
price float64) *dishwasher {
return &dishwasher{
brand: brand,
model: model,
numOfWashCycles: numOfWashCycles,
price: price,
}
}
func (dw *dishwasher) consume() {
fmt.Printf(" -> The Dishwasher (%s - %s) is powered\n", dw.brand,
dw.model)
}
func (dw *dishwasher) print() {
fmt.Println("Dishwasher - Specifications")
fmt.Printf("\tBrand: %s\n", dw.brand)
fmt.Printf("\tModel: %s\n", dw.model)
fmt.Printf("\tNumber of Wash Cycles: %d\n", dw.numOfWashCycles)
fmt.Printf("\tPrice: %.2f\n", dw.price)
pd := format(dw.purchaseDate)
fmt.Printf("\tPurchase Date: %s\n", pd)
}
func format(v interface{}) string {
var t int
switch e := v.(type) {
case int:
t = e
case string:
t, _ = strconv.Atoi(e)
default:
return "unknown"
}
const layout = "1973, Oct"
u := time.Unix(int64(t), 0)
return u.Format(layout)
}
turn u.String()
}
No comments:
Post a Comment