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


No comments:

Post a Comment