15/02 Study Daily record

손진성·2022년 2월 16일
0

Closer Look at Interfaces and Methods in Go

Type node interface

type Node interface {
	SetValue(v int) error
	GetValue() int
}
  • This Node interface contained only two methods.
  • SetValue which takes a value as an argument that sets in the node.
  • GetValue which gets the value of the node.
  • The interface served as the parent interface for two struct types.
  • The SLLNode struct and the PowerNode struct.

Type SLLNode

//type SLLNode
type SLLNode struct {
	next         *SLLNode
	value        int
	SNodeMessage string
  • The SLLNode type acted as a normal node.
  • In a single linked list where each node stores a value as well as a pointer to the next node.
  • In the SLLNode struct, you'll add a new string field called SNodeMessage to represent the unique field that only exists in the SLLNode type.

Change NewSLLNode() *SLLNode

Before

func NewSLLNode()*SLLNode{
return new(SLLNode)
}

After

func NewSLLNode() *SLLNode {
	return &SLLNode{SNodeMessage: "This is a message from the normal Node"}
}
  • In the constructor for SLLNode, We'll change the previous code to use a struct liteal to initialize struct The SLLNode with a string message the SNodeMessage field.

    A struct literal is a way to allocate a new struct by listing its fields and values.

    var (
    	v1 = Vertex{1, 2}  // has type Vertex
    	v2 = Vertex{X: 1}  // Y:0 is implicit
    	v3 = Vertex{}      // X:0 and Y:0
    	p  = &Vertex{1, 2} // has type *Vertex
    )

Receiver Method

  • As a reminder, we call the argument
    the receiver between the func keyword and the function name.
  • A receiver represents the type which implements a method.
func (sNode *PowerNode) SetValue(v int) error {
	if sNode == nil {
		return ErrInvalidNode
	}
	sNode.value = v * 10
	return nil
}

func (sNode *PowerNode) GetValue() int {
	return sNode.value
}
  • So if we think from an object-oriented pointed of view, (sNode *PowerNode) is our object type or our class type.
  • GetValue() is a method that our object implements.

Type PowerNode

//type PowerNode
type PowerNode struct {
	next         *PowerNode
	value        int
	PNodeMessage string
}
  • The PowerNode type is similar to the SLLNode type except that it multiplies any value it receives by 10.
  • The PowerNode struct we'll add a new string field called PNodeMessage that is unique to the struct type.

Constructor type PowerNode

func NewPowerNode() *PowerNode {
	return &PowerNode{PNodeMessage: "This is a message from the power Node"}
}
  • In the constructor for the PowerNode we'll assign a string message to the PNodeMessage field Using the struct literal notion as before.

createNode

func createNode(v int) Node {
	sn := NewSLLNode()
	sn.SetValue(v)
	return sn
}
  • Let's write a function that would simulate a node getting created.
  • For starters, We'll create a NewSLLNode then We'll set the value as a value as a given to the createNode function.
  • Because SLLNode implements a Node interface.
  • We can simply return it as a node inferface as shown so this becomes valid code.

Type Switch

switch concreten := n.(type) {
case *SLLNode:
	fmt.Println("Type is SLLNode, message:", concreten.SNodeMessage)
case *PowerNode:
	fmt.Println("Type is PowerNode, message:", concreten.PNodeMessage)
	}
  • Let's explore a feature of Go code type switch.
  • By using a type switch, We can test the type of a certain value from multiple choices.
  • The syntax is to use dot with the word type between round brackets next to the interface value that would like to inspect in a switch statement.
  • We assigned the concrete result of the conversion to another variable.
    • A concrete type defines what a value is, as well as what a value can do. That is, in the concrete type, the basic type in which the data of the value is stored is stored.
  • We use a variable called concreten to store the concrete value produced from the conversion.
  • The two cases here in the switch statements represent concrete types that we check against since n is of type node. Since It's a result of createNode
  • That means that n could either be a pointer to SLLNode or a pointer to PowerNode.
  • That is because those the two types that implement the node interface which is why the two cases of our switch statment check against those types.
  • Because createNode in this case created node type of SLLNode, This case will be the one invoked with this code.
  • We verify that the concrete type is what we're retrived from this statement by calling a field that only exists in this particular concrete type.
  • So as we have done SNodeMessage only exists in SLLNode
  • It does not exist in the PowerNode type.

What is Concrete Type?

  • The value of the interface type has a concrete type and a value of the corresponding type.
  • A concrete type is any type that can have methods. Only user-defined types can have methods.
  • When developing, do not create interfaces first. An interface with one implementation is unnecessary. When two or more concrete types need to be handled in the same way, it is desirable to use an interface.
  • A branch can be configured according to the concrete type that the interface contains, and this is called a Type Switch. Used like match with in Ocaml, which is actually a very nice feature.
  • Interfaces in Go have an abstract form unlike other types. In other words, types such as structures and variables accurately express values, so additional actions can be provided through methods. A type clearly expressed in this way is called a concrete type. However, for abstract types such as interfaces, the expression of values is not clear. Because the interface exposes only a subset of the methods, it only knows what this value does or what behavior the method on it provides.
  • riverandeye.tistory.com/entry/1-Golang-%EA%B0%9C%EB%85%90-%EB%B0%8F-%EA%B8%B0%EC%B4%88
  • oyongs7.tistory.com/9

Before(New SLL Node)

func createNode(v int) Node {
	sn := NewSLLNode()
	sn.SetValue(v)
	return sn
}
Type is SLLNode, message: This is a message from the normal Node
  • We got that Type is SLLNode message and also We got a special message we stored in the unique field SNodeMessage field That only exists in the SLLNode type.
  • That proves that using the switch types we were able to treat the full concrete type that was being represented by the high-level interface.

After(NewPowerNode)

func createNode(v int) Node {
	sn := NewPowerNode()
	sn.SetValue(v)
	return sn
}
Type is NewPowerNode, message: This is a message from the normal Node
  • If we create our PowerNode that will trigger the second case as shown notice how returning the PowerNode still satisfy the createNode Function.
  • The PowerNode implements Node interface.

Important gotcha 1

  • If *T implements methods of interface I, then a value of only type *T can access the methods.
  • Let's cover some important gotchas when working with interfaces.
  • Gotchas represent tricks and common mistakes when dealing with interfaces.
  • In Go, when the type implementing the interface, Method is a pointer. Only a value of a pointer type can access these methods.
sNode := SLLNode{}
n = sNode
  • For example, If you create a node that is a just straight SLLNode struct not a pointer to that struct, We would not be able to assign this value to the interface.
  • Because n is of type Node. and Node is only implemented by pointer to SLLNode not as straight value of SLLNode.
func (sNode *SLLNode) SetValue(v int) error {
	if sNode == nil {
		return ErrInvalidNode
	}
	sNode.value = v
	return nil
}
func main() {
	n := createNode(5)

	switch concreten := n.(type) {
	case *SLLNode:
		fmt.Println("Type is SLLNode, message:", concreten.SNodeMessage)
	case *PowerNode:
		fmt.Println("Type is PowerNode, message:", concreten.PNodeMessage)
	}
	sNode := SLLNode{}
	n = sNode
}
func createNode(v int) Node {
	sn := NewSLLNode()
	sn.SetValue(v)
	return sn
}
.\main.go:93:4: cannot use sNode (type SLLNode) as type Node in assignment:
        SLLNode does not implement Node (GetValue method has pointer receiver)
  • As seen here the type implements the Node interface mothods is a pointer to SLLNode which is why this code produced this error.
  • It states clearly the reason for the error.
  • To fix the code, We will make sNode a pointer to SLLNode By using & symbol which would indicate that this is an address for SLLNode Struct hands a pointer.

after(main)

sNode := &SLLNode{}
	n = sNode
}
Type is SLLNode, message: This is a message from the normal Node
  • This functionality is by design in order to protect the corder from invoking a pointer receiver without needing.
  • Pointer receiver can change the values that it point to.
  • So in other words, sNode value equals v. We actually change the value sNode.
  • If that was not a pointer, This would have been a copy of sNode and it would not have changed the value the original SNode.
  • So Go make sure you are certain in using a pointer to make your calls.

Important gotcha 2

  • If T implements methods of interface I, then a value of either type T or type *T can access the methods.
  • Let's see what that means by writing some code.

Before

func (sNode *SLLNode) SetValue(v int) error {
	sNode.value = v
	return nil
}

func NewSLLNode() *SLLNode {
	return &SLLNode{SNodeMessage: "This is a message from the normal Node"}
}

After

func (sNode SLLNode) SetValue(v int) error {
	sNode.value = v
	return nil
}

func NewSLLNode() SLLNode {
	return SLLNode{SNodeMessage: "This is a message from the normal Node"}
}
  • Instead of having the method receiver as type pointer to SLLNode(*SLLNode), We'll have it as type SLLNode directly.
  • So we'll remove the pointer references and the address reference here.
  • SLLNode type is the one that implements the node interface.
func main() {
	sNode := &SLLNode{value: 15}
	fmt.Println(sNode.GetValue())
}
  • In the createNode I put back NewSLLNode and doesn't return a pointer anymore.
  • So if you look at this code here. Even though sNode of type pointer to SLLNode was value 15, We can still access the method of type SLLNode which is not a pointer as shown.
  • This is permitted and Go will take care of the indirection for you.
Type is SLLNode, message: This is a message from the normal Node
15
  • You can see here that the sNode.Getvalue printed the value of 15.
  • So we accessed the method.
  • go.dev/play/p/JAIJYhJAUMJ

Important gotcha 3

  • The receiver of a method is allowed to be nil
  • Without craching the application which is unlike other languages.
  • We'll handle the case if receives nil and return an error to the caller node needs to initialize the receiver.
  • So let's create an error called errInvalidNode which is an error presentation of the invalid code.
var ErrInvalidNode = errors.New("Node is not valid")
  • In the SetValue method, we can also put those back as pointers.
  • So now we're back to having the SLLNode pointer type implementing the node interface.

Back to having the SLLNode Pointer

Before

func (sNode SLLNode) SetValue(v int) error {
	sNode.value = v
	return nil
}

func (sNode SLLNode) GetValue() int {
	return sNode.value
}

After

func (sNode *SLLNode) SetValue(v int) error {
	sNode.value = v
	return nil
}

func (sNode *SLLNode) GetValue() int {
	return sNode.value
}

Add Nil check

Before

func (sNode *SLLNode) SetValue(v int) error {
	sNode.value = v
	return nil
}

After

func (sNode *SLLNode) SetValue(v int) error {
	if sNode == nil {
		return ErrInvalidNode
	}
	sNode.value = v
	return nil
}
  • In the SetValue method, We can actually add a Nil check.

main

func main() {
	var sllnode *SLLNode
	fmt.Println(sllnode.SetValue(4))
}
  • Let's change our main function code to test out this.
  • We'll create a variable called SLLNode of type pointer to the SLLNode struct.
  • This pointer is not initialized(*SLLNode).
  • Because we do not assign any value to it.
  • When a pointer is not initialized, its zero value is nil which is equivalent to null in other languages.
  • so that means when we call a method straight from that nil pointer, This code path will be followed(ErrInvalidNode).
  • Instead of a panic, Go will still excute the method and it will get to this point.
  • In this case, Our code detects that this is nil and it returns an error.
Node is not valid
  • Instead of checking whether this value is nil outside, whenever we need to call a method on it, we can simply put the check inside the method itself and that take care of the validation.
  • go.dev/play/p/nkJHFhkD1r8

main change

func main() {
	var n Node
	fmt.Println(n.SetValue(4))
}
  • An important note is that if we try on the other hand to do something like this where we call a method directly from the interface type.
panic: runtime error: invalid memory address or nil pointer dereference
[signal 0xc0000005 code=0x0 addr=0x0 pc=0x50c576]

goroutine 1 [running]:
main.main()
        C:/Go/Learngo/Udemy/single linked list/main.go:63 +0x16
exit status 2
  • As shown, A panic will occur.
  • Because the interface value has to be assigned.
  • go.dev/play/p/xWZ5TWpEgp6

main change(Pointer node)

func main() {
	var n Node
	n = &SLLNode{}
	fmt.Println(n.SetValue(4))
}
  • If we change the code to look like this
<nil>
  • We assign an address of the SLLNode struct to n.
  • Meaning that n will host a pointer to the SLLNode type
  • The reason why it outputs nil is because SetValue returns an error and there was no error.
    -So hence that's why the return result was nil meaning that it was successful, No crash.

Code

func main() {
	printType("text")
	printType(3)
	printType(4.0)
}

func printType(i interface{}) {
	switch i := i.(type) {
	case string:
		fmt.Println("This is a string type", i)
	case int:
		fmt.Println("This is an int type", i)
	case float32:
		fmt.Println("This is a float type", i)
	}
}
  • Let's write new code to showcase the power of the empty interface.
  • We will write a function called printType that takes an empty interface argument and that is the syntax for that empty interface.
  • The word interface with two curly brackets.
  • So argument is called i.
  • We inspect the types of that interface using the switch type statement that we have learned earlier.
  • And then we print the type that we find.
  • So if we use this funciton with different argument types.
  • So this is a string("text")
  • This is an integer(3)
  • This is a float(4.0)
This is a string type text
This is an int type 3
  • It works as you can see we can't push any type to this function and Go didn't complain.
  • That is because any type out there is a child of the empty interface.
  • We wear also able to control the behavior of our code based on the data type that was received using the switch type statement.
  • This shows a very powerful side in go which makes your code very flexible.
  • This is a key with designing generic APIs.
  • How come we printed three cases and int case shown here but not the float case.
  • Because the default floating in Go is a float64 type.
  • go.dev/play/p/jbMvnMW68G3

Change code (float32 => float64)

func main() {
	printType("text")
	printType(3)
	printType(4.0)
}

func printType(i interface{}) {
	switch i := i.(type) {
	case string:
		fmt.Println("This is a string type", i)
	case int:
		fmt.Println("This is an int type", i)
	case float64:
		fmt.Println("This is a float type", i)
	}
}
This is a string type text
This is an int type 3
This is a float type 4
  • go.dev/play/p/pgk3cMwGhcW

What is .(type)?

  • First, I need to explain what the syntax of a form like .(type) means. This is called a type assertion, which simply means to tear off the interface wrapper and restore the original type.
  • .(type) is a syntax used in the special case of type switch, and can be executed only in the switch statement. This is used to define the syntax for each type the interface has inside, and it can be used as follows.
  • go.dev/play/p/GKEFx-5eg
  • go.dev/play/p/614ONhdfFeG
  • Since the types of parameters and return values must be the same for each method, newint or newfloat64, which are different result value types, are bound to the allnum interface and returned.
  • The difference is that the allnum interface in the previous example could contain all types, but the interface in the current example only has types with the Square() method (Square() allnum) returning allnum, that is, newint and newfloat64 types. that it includes This is because an interface contains only types that can implement the methods of the interface.

Why was the method made the standard?

  • The reason can be understood by looking at what a type is from a functional point of view.
  • Rather than classifying it using a predetermined type, all things that can perform a specific function will be classified as a specific type.
  • This way of thinking is commonly referred to as duck typing.

What is duck typing?

"In other words, don't check whether it IS-a duck: check whether it QUACKS-like-a duck, WALKS-like-a duck, etc, etc, depending on exactly what subset of duck-like behaviour you need to play your language-games with."

  • In other words, if a person quacks like a duck and walks back and forth like a duck, it will be judged as a duck and not a person.
  • Go is a compilation-based static type language. However, dynamic language style coding is also possible by accommodating the characteristics of dynamic languages. In other words, it is possible to use the advantages of a dynamic language while being guaranteed by the compiler. What makes this possible is Go's interface that works by duck typing.
  • That is, "If a bird walks, swims, and quacks like a duck, I will call it a duck." with this concept.
  • In Go, a function and a method are completely different concepts. It should not be misunderstood as the concept of a function that we generally know is called a function, and a method is a concept that leads to an action to be added to a structure.
  • popit.kr/golang으로-만나보는-duck-typing/

Set the syntax for each type

switch Arbitrary variable name:=variable name.(type){ 
case Possible type name 1: 
	Syntax 1 
case Possible type name 2: 
	Syntax 2 ... 
}
  • In this way, you can set the syntax for each possible type.
  • The core of the interface is this. It is a packaging type for grouping several types as if they are one type, and when unpacking, a type assertion (.(type name)) is used to restore the original type.
  • gall.dcinside.com/mgallery/board/view/?id=gophers&no=325

Empty Interface

  • Let's showcase another example of the power of the empty interface will create a struct called magicStore which can store a value of any type by having a field called value of type empty interface.
type magicStore struct {
	value interface{}
	name  string
}
  • we'll add another filed called name in case the user of the struct would like to name the instance they are using.
func (ms *magicStore) SetValue(v interface{}) {
	ms.value = v
}

func (ms *magicStore) GetValue() interface{} {
	return ms.value
}
  • We will get the magicStore pointer to implement two functions SetValue and GetValue.
  • We create a constructor called NewMagicStore which will take a string and It will return a pointer to the magicStore struct.
  • Inside our constructor, we will create a new pointer to magicStore and we will assign the name property of the struct with a value that is passed as an argument of a constructor which is the name would like to give to the magicStore.
func main() {
	istore := NewMagicStore("Integer Store")
	istore.SetValue(4)
	if v, ok := istore.GetValue().int; ok {
		v *= 4
		fmt.Println(v)
	}
}
  • Let's write some code in the main function to store and retrieve the value from the magicStore struct.
  • We'll start by creating a NewMagicStore.
  • we will call it integer store.
  • we will store that value in a variable called istore and set the value 4 on that store.
  • so far as an integer. So we store the integer value.
  • We will then use that type assertion from the value stored in our MagicStore.
  • To retrive the values stored inside the MagicStore to prove that this is a normal integer will multiply it by 4 and then we'll print the results.
sstore := NewMagicStore("String store")
sstore.SetValue("Hello")
if v, ok := istore.GetValue().(string); ok {
	v += " World"
	fmt.Println(v)
}
  • Next we'll create another MagicStore. This time we'll call it the String store. We will store it in a variable called sstore. We'll set the value of "Hello" and then we'll use the assertion to get the value stored in that store.
  • And to prove it's a normal string we'll concatenate it by another string which would be space world and we'll print the results.
16
Hello World
  • It was able to store an integer and a string And we were also able to choose those values and use them natively.
  • This shows us another example into how much go can be flexible when writing code using the empty interface.
  • However like any other powerful feature, we need to be careful not to over use the empty interface
  • Sometimes it will make more sense to tie that code to the expected two types in other times like building a generic API, We will need the empty interface.
  • go.dev/play/p/SNA6z2fsngT
profile
Gopyther