DEV Community

Weerasak Chongnguluam
Weerasak Chongnguluam

Posted on • Updated on

ทำไม Go ต้องมี pointer และเราใช้ pointer ใน Go ตอนไหนบ้าง

#go

เวลาเราอ่านเจอเรื่อง pointer ก็จะเห็นว่าเป็น type ที่เอาไว้เก็บ address แต่ถ้าจะให้เข้าใจต้องถามต่อไปว่า แล้วจะเก็บ address กันไปทำไม

Variable

การทำงานของคอมพิวเตอร์จะมีส่วนที่เป็น memory และใน machine code การอ้างอิงหน่วยเก็บข้อมูลใน memory จะใช้หมายเลข address แต่อย่างไรก็ตามภาษาคอมพิวเตอร์ที่สร้างขึ้นมาเพื่อให้เราไม่ต้องเขียน machine code เองอย่างภาษาที่เราเห็นทั่วๆไปหรือแม้แต่ Go เองเนี่ย เราไม่จำเป็นต้องมาหาตำแหน่ง address กันแล้ว ภาษามีสิ่งที่เรียกว่าตัวแปร หรือก็คือ variable เนี่ยให้เราใช้ ซึ่งเราก็ใช้ชื่อตัวแปร แทนที่ตำแหน่งเก็บข้อมูล เช่น

package main

import (
    "fmt"
)

func main() {
    a := 10
    b := 20
    c := a + b
    fmt.Println(c)
}
Enter fullscreen mode Exit fullscreen mode

เราก็มีที่เก็บข้อมูลชื่อ a เก็บ 10 ชื่อ b เก็บ 20 และ c เก็บผลลัพธ์ที่ได้ของ a + b คือ 30

จากตรงนี้ ไม่เห็นมีความจำเป็นจะต้องใช้ pointer เลยถูกมั้ยครับ เราต้องไปทำความเข้าใจเรื่อง variable scope ,กฎเกณฑ์ในการส่งค่าไปให้ฟังก์ชัน และ การ return ค่าจากฟังก์ชัน ของ Go ก่อนถึงจะเข้าใจว่า pointer ใน Go ออกแบบเอาไว้ทำอะไร

Variable scope

ตัวแปรใช้ชื่อในการอ้างอิง แต่ว่าก็มีขอบเขตในการอ้างอิงชื่อนั้นเช่นกัน สำหรับ scope ของตัวแปรใน Go มีหลายระดับ แต่ระดับที่เกี่ยวโยงกับ pointer ที่สุดคือตัวแปรที่ถูกสร้างใน scope ของ function

ตัวแปรที่เราสร้างใน function จะถูกอ้างอิงได้แค่ภายในฟังก์ชันนั้นๆเท่านั้น ฟังก์ชันอื่นๆอาจจะมีชื่อตัวแปรเหมือนกันได้ แต่ก็ถือว่าเป็นคนละตัวกัน เช่น

package main

import (
    "fmt"
)

func sayHello(name string) {
    msg := "Hello " + name
    fmt.Println(msg)
}

func main() {
    var msg string
    sayHello("Por")
    fmt.Println("Main => ", msg)
}
Enter fullscreen mode Exit fullscreen mode

เมื่อรันโปรแกรมนี้จะได้

Hello Por
Main => 
Enter fullscreen mode Exit fullscreen mode

จะเห็นว่า ตัวแปร msg ใน main function ไม่ใช่ตัวเดียวกันกับ msg ใน sayHello แม้จะชื่อเหมือนกันก็ตาม

การส่งค่าไปให้ function

function ที่ประกาศ parameter เอาไว้รับค่า เวลาเราเรียกใช้งานเราก็ต้องส่งค่าไปด้วย ซึ่ง Go นั้นเวลาส่งค่าสิ่งที่เกิดขึ้นคือจะ copy ค่าไปให้ตัวแปร parameter เสมอ เช่น

package main

import (
    "fmt"
)

func add(a, b int) int {
    return a + b
}

func main() {
    fmt.Println(add(10, 20))
}
Enter fullscreen mode Exit fullscreen mode

เมื่อเราเรียก add(10, 20)ตัวแปร a และ b ที่เป็น parameter ของ function add ก็จะถูกสร้างขึ้นแล้วก็ถูก copy ค่า 10 และ 20 ให้ตามลำดับ

ถ้าเรียกแบบนี้

func main() {
    a := 10
    b := 20
    fmt.Println(add(a, b))
}
Enter fullscreen mode Exit fullscreen mode

นั่นคือมีตัวแปร a และ b ใน main อยู่แล้วแต่ตอนเรียก add(a, b) ก็คือ copy ค่า a และ b ของ main ไปให้ a และ b ที่เป็น parameter ของ add function อย่างที่บอกแล้วว่าตัวแปรเป็นคนละ scope แม้ชื่อเหมือนกันแต่เป็นคนละตัว

ค่าที่ return ออกมาจาก function

ส่วนค่าที่ return ออกจาก function ก็เช่นกันกับค่าที่ส่งไปให้นั่นคือ ใช้การ copy เช่นกัน เช่นถ้าเราเขียน function add ด้านบนใหม่แบบนี้

package main

import (
    "fmt"
)

func add(a, b int) int {
    c := a + b
    return c
}

func main() {
    a := 10
    b := 20
    c := add(a, b)
    fmt.Println(c)
}
Enter fullscreen mode Exit fullscreen mode

ตัวแปร c ใน add ก็มีขอบเขตแค่ในฟังก์ชัน add เท่านั้นแล้วเมื่อเรา return c ก็คือการ copy ค่าของ c ออกไป เมื่อเรากำหนดผลลัพธ์ของ add(a, b) ให้ c ใน main function ก็จะเกิดตัวแปร c อีกตัวที่มี scope ใน main function โดย copy ค่าที่ return ได้กลับมาเก็บไว้ในตัวแปร c

พฤติกรรมของ Go ในการส่งค่าและรีเทิร์นค่ากลับมาเป็นแบบนี้เสมอ นั่นคือ copy จากตัวแปรที่อยู่ใน scope ของอีกฟังก์ชันให้กับอีกฟังก์ชัน ทั้งขาส่งค่า parameter และขา return ค่ากลับออกมา ไม่ว่าตัวแปรหรือข้อมูลนั้นจะเป็น type อะไรก็ตาม

ทุกอย่างที่เป็นการกำหนดค่า คือการ copy

จริงๆแล้วทุกๆการกำหนดค่าของ Go คือการ copy ค่าจากที่หนึ่งไปอีกที่หนึ่งทั้งหมด เช่น

a := 10
b := a
c := b
Enter fullscreen mode Exit fullscreen mode

ก็คือ a, b และ c เป็นคนละตัวแปร และการกำหนดค่า ไม่ว่าจะเป็น := หรือ = คือการ copy ค่าจากด้านขวาไปทางด้านซ้ายทั้งหมด

และที่การส่งค่าของ function เป็นการ copy ด้วยเพราะมันก็คือการกำหนดค่าเหมือนกันคือกำหนดค่าให้กับตัวแปร parameter ของฟังก์ชันนั่นเอง

ทำไมต้องมี pointer

ทีนี้ก็พร้อมจะอธิบายล่ะ ว่าทำไมต้องมี pointer พอเรารู้ว่าทุกอย่างในการส่งค่าไปให้ฟังก์ชันเป็นการ copy เสมอ ดังนั้นเราก็ไม่มีทางเลยจะออกแบบ function แบบนี้ได้ เช่น

package main

import (
    "fmt"
    "strings"
)

func upperAllLetter(str string) {
    str = strings.ToUpper(str)
}

func main() {
    name := "Weerasak"
    upperAllLetter(name)
    fmt.Println(name)
}
Enter fullscreen mode Exit fullscreen mode

ซึ่งเราคาดว่าเรียก upperAllLetter(name) ค่าใน name ต้องเปลี่ยนเป็น upper ทั้งหมดแต่ปรากฎว่าได้ค่าเดิมเพราะเพื่อเรียก function มันคือการ copy ค่า name ใน main ให้กับ paramter str ของ function แม้ str เปลี่ยนก็ไม่มีผลอะไรกับ name ใน main

ถ้าเราอยากให้ฟังก์ชันที่เราเรียกเปลี่ยนค่าของตัวแปรที่อยู่คนละ scope ได้จริงๆต้องทำอย่างไร

ใน Go เลือกที่จะมีข้อมูลประเภทใหม่ที่เรียกว่า pointer โดยสิ่งที่ pointer เก็บคือ address ของตัวแปรอื่นหรือตำแหน่งหน่วยความจำอื่นๆ ซึ่งก็ไม่ต่างกับตัวแปรอื่นๆคือเกิดขึ้นมาเพื่อเก็บข้อมูล มีชื่ออ้างอิง แค่ในกรณีนี้เก็บตัวเลข address การได้มาซึ่งเลข address ใน Go ก็ไม่ใช่การกำหนดเลขมั่วๆ สิ่งที่เตรียมให้คือ operator & ในการเอาค่า address ออกมา (หรือใช้ builtin function new สำหรับจองพื้นที่หน่วยความจำพร้อมเอา address ออกมาโดยไม่ต้องใช้ตัวแปร ซึ่งก็ไม่ค่อยได้ใช้เท่าไหร่ นานๆใช้ที)
เราไม่ได้ทำอะไรกับเลข address นี้โดยตรงหรอก แต่สิ่งที่ pointer ทำได้คือภาษา เพิ่มความสามารถให้ตัวแปร pointer นั้นแก้ไขค่าใน address นั้นได้ ผ่าน operator *

ตัวอย่างเช่น

package main

import (
    "fmt"
)

func main() {
    name := "Por"

    // ถ้าประกาศโดยใช้ data type ด้วยก็คือใช้ 
    // var pointerToName *string = &name
    pointerToName := &name 

    *pointerToName = "Weerasak"
    fmt.Println(name)
}
Enter fullscreen mode Exit fullscreen mode

ทีนี้เราไม่ค่อยเห็นการสร้าง pointer มาก็เพื่อจะแชร์ตัวแปรกันเองใน scope เดียวกันหรอก จะทำทำไมเล่า ก็เมื่อเราก็ใช้ name ได้อยู่แล้วใน scope เดียวกัน

ดังนั้นสิ่งที่เราจะเห็นการเอา pointer มาใช้บ่อยที่สุดคือ เอามาใช้เวลาเราต้องการให้ฟังก์ชันแก้ไขค่าของตัวแปรอื่นที่ส่งเข้ามา เพราะในเมื่อแก้ตรงๆไม่ได้ แต่เราส่ง pointer มา เราจะใช้ operator * แก้อ้อมๆ (indirect) ได้นั่นเอง

ที่นี้มาเขียนฟังก์ชัน upperAllLetter กันใหม่โดยออกแบบให้รับ parameter เป็น type pointer ของ string แทนแบบนี้

package main

import (
    "fmt"
    "strings"
)

func upperAllLetter(str *string) {
    *str = strings.ToUpper(*str)
}

func main() {
    name := "Weerasak"
    upperAllLetter(&name)
    fmt.Println(name)
}
Enter fullscreen mode Exit fullscreen mode

ก็จะเห็นว่าพอ type ของ parameter เปลี่ยน แน่นอนเราส่ง name เฉยๆไม่ได้แล้วเพราะมันจะผิด type ของ parameter ที่เขียนเอาไว้ว่าอยากได้ pointer ของ string

สิ่งที่เราทำได้ก็คือใช้ operator & เพื่อส่ง address ของ name ไปให้แทน

ผลการทำงานก็จะเห็นว่า name ของ main โดนเปลี่ยนไปด้วยตามที่ต้องการแล้ว โดยที่กฎเกณฑ์เรื่องการส่งค่ายังคงเดิมคือ COPY ค่าเหมือนเดิม แค่ในเคสนี้ค่าที่ถูก copy คือข้อมูลประเภท pointer ที่เป็นเลข address นั่นเอง

Pointer ของ Struct

Struct ต่างกับ int, bool, string ที่เป็น type พื้นฐานตรงที่มันเป็น type ที่ประกอบจาก type อื่นๆหลาย type โดยเอามาสร้างเป็น field ของ struct

โดยที่เราสามารถเข้าถึงข้อมูลในแต่ละ field หรือเปลี่ยนค่าบาง field ได้ผ่านทาง . (dot) เช่น

package main

import (
    "fmt"
)

type Profile struct {
    Name string
    Age  int
}

func main() {
    p := Profile{
        Name: "Por",
        Age:  35,
    }
    fmt.Println(p.Name)
    fmt.Println(p.Age)

    p.Name = "Weerasak"
    fmt.Println(p.Name)
}
Enter fullscreen mode Exit fullscreen mode

แน่นอนว่าถ้าเราต้องส่งสร้าง function เพื่อแก้ไขค่าของ struct ถ้าเราออกแบบ function แบบนี้

package main

import (
    "fmt"
    "strings"
)

type Profile struct {
    Name string
    Age  int
}

func upperProfileName(p Profile) {
    p.Name = strings.ToUpper(p.Name)
}

func main() {
    p := Profile{
        Name: "Por",
        Age:  35,
    }
    upperProfileName(p)
    fmt.Println(p.Name)
}
Enter fullscreen mode Exit fullscreen mode

เวลาเราส่ง p ไป ก็คือการ copy p ให้กับ p อีกตัวนึงนั่นเอง ซึ่งก็จะ copy ค่าใน field ทีละ field ไปด้วยทำให้เราเปลี่ยน Name ใน function ก็ไม่กระทบกับ Name ใน p ที่อยู่ใน main function

ถ้าอยากให้เปลี่ยนได้ ก็ใช้ pointer ช่วยได้เหมือนเดิมโดยเปลี่ยนโค้ดเป็นแบบนี้

package main

import (
    "fmt"
    "strings"
)

type Profile struct {
    Name string
    Age  int
}

func upperProfileName(p *Profile) {
    p.Name = strings.ToUpper(p.Name)
}

func main() {
    p := Profile{
        Name: "Por",
        Age:  35,
    }
    upperProfileName(&p)
    fmt.Println(p.Name)
}
Enter fullscreen mode Exit fullscreen mode

แต่สิ่งที่พิเศษสำหรับ pointer ของ struct คือเราสามารถใช้ . dot ได้เลย ไม่ต้องใช้ operator * ในการแก้ไขค่าของ field ผ่านทาง pointer เราจะใช้ * สำหรับ struct ก็ตอนที่เราต้องการเปลี่ยนทั้งก้อน ซึ่งก็ไม่บ่อยที่จะทำแบบนั้น

Slice และ Map

slice และ map ของ Go จะพิเศษจากตัวแปรธรรมดาหน่อยเพราะมันถูกออกแบบมาให้เก็บข้อมูลแบบ dynamic size คือเราจะ append element ใส่ให้ slice หรือเอาออกก็ได้ สำหรับ map ก็เพิ่ม key value หรือลบออกก็ได้เช่นกัน

การจัดการภายในของ slice และ map คือจะมีการเก็บ address แบบ pointer นั่นแหละ ของ element เอาไว้ ตัว slice และ map ไม่ใช่ก้อน element เองโดยตรง

การส่งค่า parameter ของ slice และ map ยังคงเป็น copy เหมือนเดิมกับเหมือนตัวแปรอื่น อย่างที่เขียนไปแล้วว่าทุก type ใน Go ส่งค่ายังไงก็ copy เสมอ

แต่พอเป็น slice และ map สิ่งที่ copy ก็เหมือน pointer คือ copy address ที่อ้างอิงถึง element ข้างใน ไม่ได้ copy element ดังนั้นถ้าเกิดการเปลี่ยนแปลงของ element ผ่าน parameter ของ slice และ map ก็จะกระทบกับ slice และ map ของฝั่งที่เรียกใช้ด้วยนั่นเอง เช่น

package main

import (
    "fmt"
)

func doubleAllElement(nums []int) {
    for i := range nums {
        nums[i] *= 2
    }
}

func main() {
    nums := []int{1, 2, 3}
    doubleAllElement(nums)
    fmt.Println(nums)
}
Enter fullscreen mode Exit fullscreen mode

เวลาเราเรียก doubleAllElements(nums) มันคือการ copy nums ใน main ให้ nums ใน doubleAllElements แต่ว่าสิ่งที่ copy ไปคือ address ของ element ด้วย ดังนั้นการเปลี่ยนแปลงของ element ภายใน doubleAllElements ก็จะกระทบกับ element ของ nums ใน main ด้วย

สำหรับ map ก็ไม่ต่างกันเช่น

package main

import (
    "fmt"
)

func doubleAllElement(nums map[string]int) {
    for key := range nums {
        nums[key] *= 2
    }
}

func main() {
    nums := map[string]int{
        "one":   1,
        "two":   2,
        "three": 3,
    }
    doubleAllElement(nums)
    fmt.Println(nums)
}
Enter fullscreen mode Exit fullscreen mode

เน้นย้ำอีกครั้ง มันยังคงเป็นการ copy แต่สิ่งที่ copy คือ address ของ element เลยทำให้ element เปลี่ยนไปด้วย

แต่ถ้าเราอยากให้เกิดการเปลี่ยนแปลงกับตัวแปรทั้งก้อนจริงๆ ก็ยังต้องใช้ pointer type ช่วยอยู่ดี เราจะพบกรณีแบบนี้เช่น function ที่ใช้ในการ unmarshaling ข้อมูลอย่าง JSON ที่เราต้องการเอามาเก็บในตัวแปรของ Go จะออกแบบให้เราต้องส่ง pointer เสมอแม้ว่าตัวแปรจะเป็น slice หรือ map ก็ตามเพื่อจะได้เปลี่ยนแปลงข้อมูลทั้งก้อนของตัวแปรได้ผ่าน pointer เช่นถ้าทำแบบนี้

package main

import (
    "encoding/json"
    "fmt"
)

func main() {
    jsonText := []byte(`[1, 2, 3]`)
    var nums []int
    json.Unmarshal(jsonText, nums) 
    // ที่ถูกต้องเป็นแบบนี้ json.Unmarshal(jsonText, &nums)
    fmt.Println(nums)
}
Enter fullscreen mode Exit fullscreen mode

ตัวแปร nums จะไม่ได้ค่า [1, 2, 3] แน่นอน ที่ถูกต้องคือต้องเรียก json.Unmarshal(jsonText, &nums) ส่ง address (pointer ของ slice []int) ไปให้แทน

การ return pointer

ตัวแปรที่อยู่ในขอบเขตของ function เพื่อฟังก์ชันทำงานจบ มันก็จะถูกคืนหน่วยความจำไป เอาไป reuse ใช้ต่อ เพราะการ return ก็แค่คือการ copy เช่นกันตัวแปรเกิดใหม่ที่อีกฟังก์ชันที่เอามาเก็บค่า ก็ถือเป็นคนละตัวกัน

แต่บางครั้งเราต้องการสร้างฟังก์ชันที่ทำหน้าที่ setup ค่าต่างๆของตัวแปรที่โครงสร้างซับซ้อน หรือใช้หน่วยความจำเยอะ การที่เขียนโค้ด setup ซับซ้อนแล้วต้องใช้หลายๆที่ เราก็ต้องการแยกเป็น function เพื่อให้เรียกใช้ง่ายๆ แต่เราก็รู้ว่าพอเรา return มันจะ copy ค่าทั้งก้อนใหญ่ๆอีก

ทางแก้ตรงนี้ก็คือใช้ pointer มาช่วยเช่นกัน แทนที่เราจะ return ตัว data ที่ซับซ้อนทั้งก้อน เราจะ return address หรือ pointer ของมันแทน ที่เราจะเห็นบ่อยสุดคือการสร้าง function เพื่อสร้างค่าของ type อย่าง struct เช่น

package main

import (
    "fmt"
)

type App struct {
    db *DB
    conf *Config
}

func NewApp() (*App, error) {
    db, err := NewDB()
    if err != nil {
        return nil, err
    }
    conf, err := NewConf()
    if err != nil {
        return nil, err
    }   

    return &App {
        db: db,
        conf: conf,
    }, nil
}

func main() {
    app, err := NewApp()
    if err != nil {
        panic(err)
    }
    app.Run()
}
Enter fullscreen mode Exit fullscreen mode

สรุป

สำหรับ Go การกำหนดค่าให้ตัวแปร หรือการส่งค่าและรับค่าจาก function ทุกอย่างคือการ Copy ค่าเสมอ

ส่วนถ้าต้องการแชร์ข้อมูลข้ามขอบเขตของ function จะใช้ Pointer type ช่วยเพราะมี operator * ในการเข้าถึงหรือแก้ไขข้อมูลในตำแหน่งที่ pointer เก็บเอาไว้อยู่นั่นเอง

Buy Me A Coffee

Top comments (8)

Collapse
 
miwtoo profile image
Miwtoo
func doubleAllElement(nums []int) {
    for i := range nums {
        nums[i] *= 2
    }
}

func main() {
    nums := []int{1, 2, 3}
    doubleAllElement(nums)
    fmt.Println(nums)
}

การส่ง arrays ก็เป็นการส่งค่าของ address ไปด้วยหรอครับ แล้วตอนมีการเปลี่ยนค่า ก็ไม่ต้องใช้ * เหมือน struct ใช่ไหมครับ

Collapse
 
iporsut profile image
Weerasak Chongnguluam

slice นะครับ ไม่ใช่ array ตัว slice เองเก็บ 3 อย่าง Len, cap, และ address ของ element

Collapse
 
miwtoo profile image
Miwtoo

ขอบคุณมากครับ

Collapse
 
dtonna profile image
Noppadol Anuroje

Pass by value และ Pass by reference

Collapse
 
iporsut profile image
Weerasak Chongnguluam

ไม่ถูกต้องครับ Go และ C ไม่เคยมี pass by reference

Collapse
 
dtonna profile image
Noppadol Anuroje

ขอบคุณครับ เรื่องนี้ทำให้ผมเข้าใจผิดอยู่นาน
เพราะสาเหตุที่เราส่ง pointer เข้าไปใน function นั้น มันดูเหมือนกับการ pass by reference แต่จริงๆแล้วมันคือ การ copy address ไว้ใน object ใหม่ ไม่ได้มีการส่ง object มาที่ function จริงๆ ที่ถูกควรเป็น pass by value
ที่น่าตลกก็คือ สิ่งที่ทำให้ผมคิดว่าเป็น pass by reference อีกอย่างคือ ตัว pointer มันเป็น reference type ก็เลยคิดว่ามัน pass by reference

Collapse
 
iporsut profile image
Weerasak Chongnguluam

อันนี้ครับเว็บทางการ Go ก็บอกเอาไว้
golang.org/doc/faq#Pointers

Collapse
 
roomday profile image
Chanchai Srithep

มาเขียนอีกบ่อยๆ นะครับไม่ค่อยได้เจอคนไทยเลยครับ