Tuesday, June 4, 2024

Golang - OOP - Attach methods


Before we go into the 'attach methods', consider the following example.


book.go
type book struct {
    title string
    price int
}

func printBook(b book) {
    fmt.Printf("Book: title=%q, price=%d\n", b.title, b.price)
}


game.go
type game struct {
    title string
    price int
}

func printGame(g game) {
    fmt.Printf("Game: title=%q, price=%d\n", g.title, g.price)
}


main.go
func main() {
    book1 := book{
      title: "book1",
      price: 10,
    }
    game1 := game{
      title: "game1",
      price: 50,
    }

    printBook(book1)
    printGame(game1)
}


Both the 'book' and 'game' types contain merely data and have no behaviors.

The separated package level functions 'printBook' and 'printGame' mainly use the relevant types, such as 'book' and 'game'.

Also, because Go does not support function overloading, we must declare various function names even if the functions have similar behavior. (Refer this article)


Can we link data (book type) and behaviors (printBook) together in Go, like with other OOP languages? (class + methods)?


Attach functions to types


We can change those functions into types to become behaviors (or we named them 'Method Set').

Format:
  func (receiver_name receiver_type) func_name(input_params) (result_params) {
// your code here
  }


'receiver' is 'syntactic sugar', and it is simply an input parameter that Go will pass automatically for us.

As a result, if we call the methods specified in the following format, Go will automatically send a copy of the type value to those type method sets.

Format:
type_value.func_name(input_params)


We can modify the previous example as shown below.

book.go
type book struct {
    title string
    price int
}

func (b book) print() {
    fmt.Printf("Book: title=%q, price=%d\n", b.title, b.price)
}


game.go
type game struct {
    title string
    price int
}

func (g game) print() {
    fmt.Printf("Game: title=%q, price=%d\n", g.title, g.price)
}


main.go
func main() {
    book1 := book{
      title: "book1",
      price: 10,
    }
    game1 := game{
      title: "game1",
      price: 50,
    }

    book1.print()
    game1.print()
}


We may use the same function name because they are now at the type level (different namespace) rather than the package level.

Also, we can invoke the method defined by the type directly without difficulty.

main.go
func main() {
    book1 := book{
      title: "book1",
      price: 10,
    }
    game1 := game{
      title: "game1",
      price: 50,
    }

    book.print(book1)
    game.print(game1)
}



To sum up, behind the scenes, a method is a function with a receiver as the first parameter.

    * We can use type to call the method set directly by passing the type value manually by us. 

    * Alternatively, we can call the method set by type value, and Go will assist in passing the type value as the first parameter to the method set.


Pointer Receiver



We have some troubles using Receiver to update type values.

book.go
type book struct {
    title string
    price int
}

func (b book) print() {
    fmt.Printf("Book: title=%q, price=%d\n", b.title, b.price)
}

func (b book) discount(amout int) {
    b.price -= amout
}


main.go
func main() {
    book1 := book{
      title: "book1",
      price: 10,
    }

    book1.print()
    book1.discount(5)

    book1.print()
}


Result:
  Book: title="book1", price=10
  Book: title="book1", price=10


Ideally, the price of book1 after adjustment should be 5, not 10.

The underlying cause is that the receiver is just a copy of the type value.

Any modifications to the copied value will not impact the original type value.

To overcome this issue, Go introduces a Pointer Receiver, which permits modifications to the original type value via memory addresses.

Format:
  func (receiver_name *receiver_type) func_name(input_params) (result_params) {
// your code here
  }


When invoking type methods set, we can use those two forms indicated below.

Format:
    // Option 1
    (&type_value).func_name(input_params)

    // Option 2 (Recommended. Go will take its address automatically
// if the method has a pointer receiver)
    type_value.func_name(input_params)


See the changed example below.

book.go
type book struct {
    title string
    price int
}

func (b book) print() {
    fmt.Printf("Book: title=%q, price=%d\n", b.title, b.price)
}

func (b *book) discount(amout int) {
    b.price -= amout
}


main.go
func main() {
    book1 := book{
      title: "book1",
      price: 10,
    }

    book1.print()

// Option 1
    (&book1).discount(5)

// Option 2
    // Can use this simple form since Go will help to transform
    // book1.discount(5)

    book1.print()
}


Result:
  Book: title="book1", price=10
  Book: title="book1", price=5



Receiver V.S. Pointer Receiver



If we are dealing with vast amounts of data, we should use a pointer receiver since it will refer to the actual data rather than creating a copy.

Take the following example: using pointer receivers is much faster than using regular receivers.

huge.go
type huge struct {
    data [1000000]string
}

func (h huge) print() {
    fmt.Printf("%p\n", &h)
}

func (h *huge) pointerPrint() {
    fmt.Printf("%p\n", h)
}


main.go
func main() {
    var huge huge

    for i := 0; i < 10; i++ {
      // huge.print()
      huge.pointerPrint()
    }
}


Result (Receiver):
  real    0m0.429s
  user    0m0.481s
  sys     0m0.211s


Result (Pointer Receiver):
  real    0m0.186s
  user    0m0.191s
  sys     0m0.143s  



Attach methods to almost all types



In the last part, we learnt how to attach methods to struct types.

Not only for that, but also Go allows you to attach methods to almost any type.

However, you should not use a pointer receiver for the following types because the passed-by value is already a memory address:

    slice, map, chan, and func

Let's try modifying the following example to attach methods to slice types.

game.go
type game struct {
    title string
    price int
}

// Add method to struct
func (g game) print() {
    fmt.Printf("Game: title=%q, price=%d\n", g.title, g.price)
}


main.go
func main() {
    game1 := game{
      title: "game1",
      price: 50,
    }

    game2 := game{
      title: "game2",
      price: 30,
    }

    var games []game
    games = append(games, game1, game2)

    for _, g := range games {
      // Call attached methods
      g.print()
    }
}



To begin, we may try attaching a method to []game as shown below, but this will result in a compile error because []game is an undefined type.

Ex:
func (l []game) print() {
for _, g := range l {
g.print()
}
}


Error:
  invalid receiver type []game


A type and its attached methods must be in the same package, and the unnamed(undefined) type does not belong to any package.

Then, let us use the defined type 'list' to achieve this.

Idea:
type list []game

func (l list) print() {
    for _, g := range l {
      g.print()
    }
}


We can also easily convert types if the underlying types are the same.

Idea:
// Convert []game to the list type
  list := list(games)

  // Call an attached method of the defined type ([]game)
  list.print()


The example with the final adjustments.

game.go

type game struct {
    title string
    price int
}

func (g game) print() {
    fmt.Printf("Game: title=%q, price=%d\n", g.title, g.price)
}


list.go

type list []game

func (l list) print() {
    for _, g := range l {
      g.print()
    }
}


main.go

func main() {
    game1 := game{
      title: "game1",
      price: 50,
    }

    game2 := game{
      title: "game2",
      price: 30,
    }

    var games []game
    games = append(games, game1, game2)

    // Convert []game to the list type
    list := list(games)

    // Call an attached method of the defined type ([]game)
    list.print()
}


The following is an example of attaching methods to an integer type.

The attached int method will return a string containing the $ symbol.

money.go:

type money int

func (m money) string() string {
    return fmt.Sprintf("$%d", m)
}


Monday, June 3, 2024

Golang - Pointers

 

Pointers



A pointer stores the memory address of a value located in computer memory.

Therefore, we can retrieve and update a value through its memory address.


Operators



& (ampersand) symbol finds the address of the variable.

*Type denotes a pointer type (A pointer to a 'Type' value).

    You only can store the memory address of the value which type is the same type

* indirection operator symbol finds the value of pointed memory address.


Experiment



1. Initial:
  var p *byte
  var counter byte = 100


Address:

                     byte
                    counter
                       ↓

                      100
        | ___ | ___ | ___ | ___ | ___  | ___ | ___ | ___ |

        0X 1     2     3     4     5      6     7     8



2. Get the memory address of the counter variable and assign to pointer variable:
var p *byte
var counter byte = 100
p = &counter


Address:

                     byte        *byte
                    counter        p
                       ↓           ↓

                      100          3
        | ___ | ___ | ___ | ___ | ___  | ___ | ___ | ___ |

        0x 1     2     3     4     5      6     7     8

                      ☝          
                       |           |
                       |___________|


    NOTE:

    * '&' operator will get the memory address 0x3 of the counter variable
    * Go will allocate memory address 0x5 to the pointer variable
    * Assign 0x3 to the p pointer variable


3. Create a new variable to store the value of the pointed memory address:
  var p *byte
  var counter byte = 100
  p = &counter
  v = *p


Address:

                     byte        *byte         byte
                    counter        p            v
                       ↓           ↓            ↓

                      100          3           100
        | ___ | ___ | ___ | ___ | ___  | ___ | ___ | ___ |

        0x 1     2     3     4     5      6     7     8

                      ☝          
                       |           |
                       |___________|


    NOTE:

    * Go will allocate memory address 0x7 to the v variable.
    * '*' indirection operator will find the value of the pointed memory address 0x3 which is stored in pointer variable p.
    * 100 will be stored in the memory address 0x7 of v variable.


4. Change the value of variable v to see if variable counter got changed or not:
  var p *byte
  var counter byte = 100
  p = &counter
  v =*p
  v = 200


Address:

                     byte        *byte         byte
                    counter        p            v
                       ↓           ↓            ↓

                      100          3           200
        | ___ | ___ | ___ | ___ | ___  | ___ | ___ | ___ |

        0x 1     2     3     4     5      6     7     8

                      ☝          
                       |           |
                       |___________|


    NOTE:

    * counter variable and v variable are independent. (*p only return value only)


5. Change the value of variable counter to see what happened.:
  var p *byte
  var counter byte = 100
  p = &counter
  v = *p
  v = 200
  counter++
  v2 = *p


Address:

                     byte        *byte         byte  byte
                    counter        p            v     v2
                       ↓           ↓            ↓     ↓

                      101          3           200   101
        | ___ | ___ | ___ | ___ | ___  | ___ | ___ | ___ |

        0x 1     2     3     4     5      6     7     8

                      ☝          
                       |           |
                       |___________|


    NOTE:

    * The value of counter is 101 now
    * '*p' will get the value of pointed memory which is stored in the pointer variable p


6. Update the value of the counter variable through *p:
  var p *byte
  var counter byte = 100
  p = &counter
  v = *p
  v = 200
  counter++
  v2 = *p
  *p = 25


Address:

                     byte        *byte         byte  byte
                    counter        p            v     v2
                       ↓           ↓            ↓     ↓

                      25           3           200   101
        | ___ | ___ | ___ | ___ | ___  | ___ | ___ | ___ |

        0x 1     2     3     4     5      6     7     8

                      ☝          
                       |           |
                       |___________|


    NOTE:
    * Using '*p', we can update the value of the counter variable indirectly.


More Examples



1. Empty pointer is nil. 
    And  we can use fmt.Printf and %p verb to show pointer value.

Ex:

var p *int

  fmt.Printf("pointer p is %v\n", p)
  fmt.Printf("pointer p its address %p\n", p)


Result:

  pointer p is <nil>
  pointer p its address 0x0



2. Pointer variable has its own memory address. 
    Pointer variable store the memory address of other variable. 
    Using indirect operator '*', we can get the value of pointed memory address..

Ex:

var (
      counter int
      p       *int
  )

  counter = 100
  p = &counter

  fmt.Printf("counter: value is %d\n", counter)
  fmt.Printf("counter: memory address is %p\n", &counter)
  fmt.Printf("pointer p: value is %p\n", p)
  fmt.Printf("pointer p: memory address is %p\n", &p)
  fmt.Printf("pointer p: value of pointed memory address is %d\n", *p)


Result:

  counter: value is 100
  counter: memory address is 0xc0000120b0
  pointer p: value is 0xc0000120b0
  pointer p: memory address is 0xc000054020
  pointer p: value of pointed memory address is 100


3. Comparing pontoniers will compare the value they pointed.

Ex:

company := "apple"
  p1 := &company
  p2 := p1

  fmt.Printf("(p1 == p2) => %v\n", p1 == p2)
  fmt.Printf("p1=> address: %p, pointed address: %p,
pointed value: %s\n", &p1, p1, *p1)
  fmt.Printf("p2=> address: %p, pointed address: %p,
pointed value: %s\n", &p2, p2, *p2)


Result:

  p1 == p2 => true
  p1=> address: 0xc00009c028, pointed address: 0xc000096020,
pointed value: apple
  p2=> address: 0xc00009c030, pointed address: 0xc000096020,
pointed value: apple



Work with functions



From the previous topic 'Functions', we have discussed how to confine variables in function to update variables instead of defining variables in global level.

Ex:

func main() {
    var counter int
    counter = 100

    // Use the returned value to update the counter variable
    counter = passValue(counter)

    fmt.Printf("counter: %d\n", counter)
}

func passValue(counter int) int {
    counter++

    // Need to return the value
    return counter
}


Result:

  counter: 101


But after learning pointers, we can adjust the example as shown below.

Ex:

func main() {
    var counter int
    counter = 100

    // Passing memory address of the counter variable
    passPointerValue(&counter)

    fmt.Printf("counter: %d\n", counter)
}

// The input is the pointer variable
func passPointerValue(counter *int) {
    // We can use the indirection operator to update the value of
// the pointed memory address
    *counter++
}


Result:

  counter: 101



Work with Composite type



1. Array


As arrays are plain values, Go will duplicate the values and pass them to the function variables.

As a result, any changes to the values within the functions will have no effect on the function callers' values.

Consider the following as an example.

Ex:

func main() {
    arr := [...]int{1, 2, 3}

    fmt.Printf("main -> arr.memory: %p\n", &arr)
    fmt.Printf("main -> arr: %v\n", arr)

    increaseArrElement(arr)

    fmt.Printf("main -> arr: %v\n", arr)
}

func increaseArrElement(arr [3]int) {
    for i := range arr {
        arr[i] += 1
    }

    fmt.Printf("func -> arr.memory: %p\n", &arr)

    fmt.Printf("func -> arr: %v\n", arr)
}


Result:

  main -> arr.memory: 0xc0000180d8
  main -> arr: [1 2 3]

  func -> arr.memory: 0xc000018120
  func -> arr: [2 3 4]
       
  main -> arr: [1 2 3]


Variables within and outside the function have distinct memory addresses.
And the changes will be limited to the copied arr.

We can use pointers to link them.

Ex:

func main() {
    arr := [...]int{1, 2, 3}

    fmt.Printf("main -> arr.memory: %p\n", &arr)
    fmt.Printf("main -> arr: %v\n", arr)

    increaseArrElement(&arr)

    fmt.Printf("main -> arr: %v\n", arr)
}

func increaseArrElement(arr *[3]int) {
    for i := range arr {
        arr[i] += 1
    }

    fmt.Printf("func -> arr.memory: %p\n", arr)
    fmt.Printf("func -> arr: %v\n", *arr)
}


Result:

  main -> arr.memory: 0xc0000a4000
  main -> arr: [1 2 3]

  func -> arr.memory: 0xc0000a4000
  func -> arr: [2 3 4]

  main -> arr: [2 3 4]


For the best practice, try to use Slice rather than Pointers with Array.


2. Slice


Slice carries the slice header, which contains the memory address of the backing array.

Go will copy the slice value and assign it to the function variable.

As a result, even if two variables have distinct memory addresses, if we alter the slice variable within the function, the change will be reflected outside the variable because they belong to the same backing array.

Ex:

func main() {
    slice := []string{"h", "e", "l", "l", "o"}

    fmt.Printf("main -> slice.memory: %p\n", &slice)
    fmt.Printf("main -> slice.header:  (%+v)\n",
(*reflect.SliceHeader)(unsafe.Pointer(&slice)))
    fmt.Printf("main -> slice: %v\n", slice)

    capitalSlice(slice)

    fmt.Printf("main -> slice: %v\n", slice)
}

func capitalSlice(slice []string) {
    for i, _ := range slice {
        slice[i] = strings.ToUpper(slice[i])
    }

    fmt.Printf("func -> slice.memory: %p\n", &slice)
    fmt.Printf("func -> slice.header:  (%+v)\n",
(*reflect.SliceHeader)(unsafe.Pointer(&slice)))
    fmt.Printf("func -> slice: %v\n", slice)
}


Result:

  main -> slice.memory: 0xc0000a4000
  main -> slice.header:  (&{Data:824634400768 Len:5 Cap:5})
  main -> slice: [h e l l o]

  func -> slice.memory: 0xc0000a4030
  func -> slice.header:  (&{Data:824634400768 Len:5 Cap:5})
  func -> slice: [H E L L O]

  main -> slice: [H E L L O]


But it does not work if we append or remove slice since the Len and Cap are bare values.

Ex:

func main() {
    slice := []string{"Hello"}

    fmt.Printf("main -> slice.memory: %p\n", &slice)
    fmt.Printf("main -> slice.header:  (%+v)\n",
(*reflect.SliceHeader)(unsafe.Pointer(&slice)))
    fmt.Printf("main -> slice: %v\n", slice)

    appendSlice(slice)

    fmt.Printf("main -> slice: %v\n", slice)
}

func appendSlice(slice []string) {
    slice = append(slice, "Frank")

    fmt.Printf("func -> slice.memory: %p\n", &slice)
    fmt.Printf("func -> slice.header:  (%+v)\n",
(*reflect.SliceHeader)(unsafe.Pointer(&slice)))
    fmt.Printf("func -> slice: %v\n", slice)
}


Result:

  main -> slice.memory: 0xc0000a4000
  main -> slice.header:  (&{Data:824634335264 Len:1 Cap:1})
  main -> slice: [Hello]

  func -> slice.memory: 0xc0000a4030
  func -> slice.header:  (&{Data:824634499075 Len:2 Cap:2})

  func -> slice: [Hello Frank]
  main -> slice: [Hello]


If the backing array's maximum capacity is exceeded, Go will assign a new one.

As a result, two variables will connect to distinct backing arrays, and the changes will not be synchronized.

We can use pointers to resolve it, but it is not that common.

Ex:

func main() {
    slice := []string{"Hello"}

    fmt.Printf("main -> slice.memory: %p\n", &slice)
    fmt.Printf("main -> slice.header:  (%+v)\n",
(*reflect.SliceHeader)(unsafe.Pointer(&slice)))
    fmt.Printf("main -> slice: %v\n", slice)

    appendSlice(&slice)

    fmt.Printf("main -> slice.memory: %p\n", &slice)
    fmt.Printf("main -> slice.header:  (%+v)\n",
(*reflect.SliceHeader)(unsafe.Pointer(&slice)))
    fmt.Printf("main -> slice: %v\n", slice)
}

func appendSlice(slice *[]string) {
    data := *slice

    *slice = append(data, "Frank")

    fmt.Printf("func -> slice.memory: %p\n", slice)
    fmt.Printf("func -> slice.header:  (%+v)\n",
(*reflect.SliceHeader)(unsafe.Pointer(slice)))
    fmt.Printf("func -> slice: %v\n", *slice)
}


Result:

  main -> slice.memory: 0xc000010018
  main -> slice.header:  (&{Data:824633802864 Len:1 Cap:1})
  main -> slice: [Hello]

  func -> slice.memory: 0xc000010018
  func -> slice.header:  (&{Data:824634179616 Len:2 Cap:2})
  func -> slice: [Hello Frank]

  main -> slice.memory: 0xc000010018
  main -> slice.header:  (&{Data:824634179616 Len:2 Cap:2})
  main -> slice: [Hello Frank]



3. Map


Map value is a pointer.

As a result, the changes will be synchronized because they point to the same memory address.

Ex:

func main() {
    dict := map[string]string{
        "Hi":      "嗨",
        "Welcome": "歡迎",
    }

    fmt.Printf("main -> dict.memory: %p\n", &dict)
    fmt.Printf("main -> dict: %v\n", dict)

    changeMap(dict)

    fmt.Printf("main -> dict: %v\n", dict)
}

func changeMap(dict map[string]string) {
    dict["Hi"] = "嗨嗨"

    fmt.Printf("func -> dict.memory: %p\n", &dict)
    fmt.Printf("func -> dict: %v\n", dict)
}


Result:

  main -> dict.memory: 0xc000054020
  main -> dict: map[Hi:嗨 Welcome:歡迎]

  func -> dict.memory: 0xc000054030
  func -> dict: map[Hi:嗨嗨 Welcome:歡迎]

  main -> dict: map[Hi:嗨嗨 Welcome:歡迎]



4. Struct


The behavior of passing Struct value to functions varies according to the type of struct fields.

If it's a basic type or an array, changes within the functions have no effect on the variable outside.

Ex:

func main() {
    p1 := person{
        name: "Fank",
        age:  18,
    }

    fmt.Printf("main -> struct.memory: %p\n", &p1)
    fmt.Printf("main -> struct: %v\n", p1)

    incr(p1)

    fmt.Printf("main -> struct: %v\n", p1)
}

func incr(p person) {
    p.age++

    fmt.Printf("func -> struct.memory: %p\n", &p)
    fmt.Printf("func -> struct: %v\n", p)
}


Result:

  main -> struct.memory: 0xc000010018
  main -> struct: {Fank 18}

  func -> struct.memory: 0xc000010048
  func -> struct: {Fank 19}

  main -> struct: {Fank 18}


We can use pointers to adjust previous example.

Ex:

func main() {
    p1 := person{
        name: "Fank",
        age:  18,
    }

    fmt.Printf("main -> struct.memory: %p\n", &p1)
    fmt.Printf("main -> struct: %v\n", p1)

    incr(&p1)

    fmt.Printf("main -> struct: %v\n", p1)
}

func incr(p *person) {
    p.age++

    fmt.Printf("func -> struct.memory: %p\n", p)
    fmt.Printf("func -> struct: %v\n", *p)
}


Result:

  main -> struct.memory: 0xc0000a4000
  main -> struct: {Fank 18}

  func -> struct.memory: 0xc0000a4000
  func -> struct: {Fank 19}

  main -> struct: {Fank 19}


If the field we want to change is slice or map, then the changes will be synced.

Ex:

func main() {
    p1 := person{
        name:  "Fank",
        age:   18,
        title: []rune{'m', 'a', 's', 't', 'e', 'r'},
    }

    fmt.Printf("main -> struct.memory: %p\n", &p1)
    fmt.Printf("main -> struct.title: %v\n", string(p1.title))

    upper(p1)

    fmt.Printf("main -> struct.title: %s\n", string(p1.title))
}

func upper(p person) {
    for i, _ := range p.title {
        p.title[i] = unicode.ToUpper(p.title[i])
    }

    fmt.Printf("func -> struct.memory: %p\n", &p)
    fmt.Printf("func -> struct.title: %s\n", string(p.title))
}


Result:

  main -> struct.memory: 0xc000108090
  main -> struct.title: master

  func -> struct.memory: 0xc00007e0f0
  func -> struct.title: MASTER

  main -> struct.title: MASTER