Skip to content

Dashboard

Truyền giá trị và truyền tham chiếu trong Go

Created by Admin

Truyền giá trị (pass by value) và truyền tham chiếu (pass by reference) là điều quan trọng cần phải cẩn thận khi chúng ta làm việc với các ngôn ngữ lập trình hỗ trợ "pointer" như Java, C#, C/C++, Go,...

Khi bạn tạo một method hay một function với tham số, kiểu dữ liệu của tham số có thể là kiểu dữ liệu bình thường hoặc pointer. Điều này sẽ tạo ra sự khác biệt với đối số được truyền vào method:

  • Truyền giá trị (pass by value) sẽ truyền giá trị của biến vào trong methed hoặc chúng ta có thể hiểu rằng giá trị của biến ban đầu được copy ra một giá trị khác và lưu trong địa chỉ ô nhớ khác và truyền giá trị mới được tạo vào trong method
  • Truyền tham chiếu (pass by reference) sẽ truyền địa chỉ ô nhớ thay vì giá trị của biến. Nói cách khác, nó truyền ‘ container’ của biến cho method, vì vậy bất cứ điều gì xảy ra với biến bên trong method sẽ ảnh hưởng đến biến ban đầu.

Tóm lại : Pass by value sẽ copy giá trị, pass by reference truyền địa chỉ ô nhớ

Hình minh họa

golang compare

Pass by Value và Pass by Reference trong Go

Trong ngôn ngữ lập trình Golang, chúng ta có thể truy cập để làm việc với pointer, điều này có nghĩa là chúng ta cần phải hiểu cách làm việc của Pass by Value và Pass by Reference trong go. Ở đây chúng ta sẽ phân chia làm 3 sections chính : Basic , Referenced , Function.

Basic Data Type

package main

import "fmt"

func main() {
    // Khởi tạo giá trị
    a, b := 0, 0

    fmt.Printf("## INIT\n")
    fmt.Printf("Memory Location a: %p, b: %p\n", &a, &b)
    fmt.Printf("Value a: %d, b: %d\n", a, b) // 0 0

    // Passing By Value a(int)
    Add(a) // Go sẽ copy giá trị của a và truyền vào trong function

    // Passing By Reference b(int), &b(*int) => Với toán tử '&' chúng ta có thể lấy địa chỉ của 'b'
    AddPtr(&b) // Truyền địa chỉ của 'b' vào trong funtion

    fmt.Printf("\n## FINAL\n")
    fmt.Printf("Memory Location a: %p, b: %p\n", &a, &b)
    fmt.Printf("Value a: %d, b: %d\n", a, b) // 0 1
}

// Pass By Value
func Add(x int) {
    fmt.Printf("\n## 'Add' Function\n")
    fmt.Printf("Before Add, Memory Location: %p, Value: %d\n", &x, x)
    x++
    fmt.Printf("After Add, Memory Location: %p, Value: %d\n", &x, x)
}

// Pass By Reference
func AddPtr(x *int) {
    fmt.Printf("\n## 'AddPtr' Function\n")
    fmt.Printf("Before AddPtr, Memory Location: %p, Value: %d\n", x, *x)
    *x++     // Chúng ta thêm toán tử * vào đằng trước biến, * sẽ trả về giá trị của pointer
    fmt.Printf("After AddPtr, Memory Location: %p, Value: %d\n", x, *x)
}

Kết quả

## INIT
Memory Location a: 0xc0000120a8, b: 0xc0000120c0
Value a: 0, b: 0

## 'Add' Function
Before Add, Memory Location: 0xc0000120f0, Value: 0
After Add, Memory Location: 0xc0000120f0, Value: 1

## 'AddPtr' Function
Before AddPtr, Memory Location: 0xc0000120c0, Value: 0
After AddPtr, Memory Location: 0xc0000120c0, Value: 1

## FINAL
Memory Location a: 0xc0000120a8, b: 0xc0000120c0
Value a: 0, b: 1

Trong ví dụ đầu tiên, chúng ta sẽ làm việc với kiểu dữ liệu cơ bản. Ở đây chúng ta có 2 function

  • Add(x int) với tham số truyền vào là int
  • AddPtr(x *int) với tham số truyền vào là pointer kiểu int

Chúng ta không thể dự đoán được địa chỉ ô nhớ của a và b nhưng chúng ta có thể xem bằng cách in địa chỉ ô nhớ của chúng.

Trong method Add, địa chỉ ô nhớ của giá trị truyền vào không cùng với a trong main() bởi Go copy giá trị của a và khởi tạo một địa chỉ ô nhớ khác, nên khi chúng ta thay đổi giá trị x++, thì a vẫn là 0. Kết quả cuối cùng của a là 0 bởi vì đây là truyền giá trị (pass by value)

Trong method AddPtr, địa chỉ ô nhớ của giá trị truyền vào giống với b, vì thế mọi thay đổi mà chúng ta thực hiện với x bên trong AddPtr sẽ ảnh hưởng đến giá trị của b. Ở đây chúng ta thực hiện tăng giá trị của x bằng câu lệnh *x++. Kết quả cuối cùng của b là 1 bởi vì đây là truyền tham chiếu (pass by reference)

Những kiểu dữ liệu cơ bản khác như cũng tương tự nhau : int, int8, int16, int32, int64, uint, uint8, uint16, uint32, uint64, uintptr, float32, float64, string, bool, byte, rune, Array, Structs

Referenced Data Type

package main

import (
    "fmt"
)

func main() {
    // Slices
    fmt.Println("======================")
    fmt.Println("SLICES")
    fmt.Println("======================")
    var arrInt []int = []int{1, 2, 3, 4, 5}
    var sliceInt = arrInt[3:]

    fmt.Printf("Init\n")
    fmt.Printf("ArrInt: %+v, SliceInt: %+v\n\n", arrInt, sliceInt)

    sliceInt[0] = 10
    fmt.Printf("After\n")
    fmt.Printf("ArrInt: %+v, SliceInt: %+v\n", arrInt, sliceInt)

    // MAP
    fmt.Println("======================")
    fmt.Println("MAP")
    fmt.Println("======================")
    var emptyMap = make(map[string]interface{})
    fmt.Println("Init")
    fmt.Printf("emptyMap : %+v\n", emptyMap)

    MapFunc(emptyMap)
    fmt.Println("After")
    fmt.Printf("emptyMap : %+v\n", emptyMap)
}

func MapFunc(val map[string]interface{}) {
    val["this is a new value"] = 100
}

Kết quả

======================
SLICES
======================
Init
ArrInt: [1 2 3 4 5], SliceInt: [4 5]
After
ArrInt: [1 2 3 10 5], SliceInt: [10 5]

======================
MAP
======================
Init
emptyMap : map[]
After
emptyMap : map[this is a new value:100]

Trong ví dụ này, Slices về bản chất có cùng địa chỉ ô nhớ với array, nên nếu chúng ta thay đổi giá trị của Slices nó sẽ ảnh hưởng đến giá trị của array

Với kiểu dữ liệu map thì mặc định là pass by reference, vì vậy bất kỳ điều gì thay đổi bên trong function sẽ làm thay đổi giá trị của map ban đầu. Nếu chúng ta muốn thực hiện pass by value cho map, chúng ta có thể sử dụng bản copy giá trị của map để truyền vào function, điều này sẽ không ảnh hưởng gì đến giá trị map ban đầu. chan hoặc channel cũng là kiểu dữ liệu tham chiếu.

package main

import "fmt"

type StructVal struct {
    IntVal int
}

func (s StructVal) Add() {
    fmt.Printf("====================\n")
    fmt.Printf("Func Add\n")
    fmt.Printf("====================\n")
    fmt.Printf("Memory Location %p\n", &s)
    fmt.Printf("Value Before: %+v\n", s)
    s.IntVal++
    fmt.Printf("Value After: %+v\n\n", s)
}

func (s *StructVal) AddPtr() {
    fmt.Printf("====================\n")
    fmt.Printf("Func AddPtr\n")
    fmt.Printf("====================\n")
    fmt.Printf("Memory Location %p\n", s)
    fmt.Printf("Value Before: %+v\n", s)
    s.IntVal++
    fmt.Printf("Value After: %+v\n\n", s)
}

func main() {
    init := StructVal{
        IntVal: 0,
    }

    fmt.Printf("================\n")
    fmt.Printf("MAIN\n")
    fmt.Printf("================\n")
    fmt.Printf("Memory Location: %p\n", &init)
    fmt.Printf("Value: %+v\n\n", init)

    init.Add()

    fmt.Printf("================\n")
    fmt.Printf("AFTER Func Add()\n")
    fmt.Printf("================\n")
    fmt.Printf("Value: %+v\n\n", init)

    init.AddPtr()
    fmt.Printf("================\n")
    fmt.Printf("AFTER Func AddPtr()\n")
    fmt.Printf("================\n")
    fmt.Printf("Value: %+v\n\n", init)

    fmt.Printf("================\n")
    fmt.Printf("FINAL\n")
    fmt.Printf("================\n")
    fmt.Printf("Value: %+v\n", init)
}

Kết quả

================
MAIN
================
Memory Location: 0xc000128058
Value: {IntVal:0}

====================
Func Add
====================
Memory Location 0xc000128098
Value Before: {IntVal:0}
Value After: {IntVal:1}

================
AFTER Func Add()
================
Value: {IntVal:0}
====================
Func AddPtr
====================
Memory Location 0xc000128058
Value Before: &{IntVal:0}
Value After: &{IntVal:1}

================
AFTER Func AddPtr()
================
Value: {IntVal:1}

================
FINAL
================
Value: {IntVal:1}

Trong ví dụ cuối này, chúng ta làm việc với Struct Function. Sự khác biệt nằm ở chính bản thân struct thay vì biến nằm bên trong struct. Khi chúng ta tạo struct function, chúng ta cần khai báo struct receive đằng trước lên của method ví dụ func (s struct) FuncName() hay func (s *struct) FuncName()

Sự khác biệt ở đây là struct được sử dụng trong function. Nếu chúng ta sử dụng pointer, nó sẽ cung cấp struct ban đầu, vì vậy bất kỳ điều gì xảy ra làm thay đổi struct sẽ làm ảnh hưởng đến giá trị của struct ban đầu. Còn nếu chúng ta không sử dụng pointer thì một giá trị copy của struct sẽ được truyền vào trong function, nếu có bất kỳ thay đổi gì tác động lên struct sẽ không ảnh hưởng gì đến struct ban đầu

Kết thúc bài viết này, hi vong nó sẽ giúp các bạn hiểu hơn về Pass by Value và Pass by Reference trong ngôn ngữ lập trình Golang

Source: https://viblo.asia/p/truyen-gia-tri-va-truyen-tham-chieu-trong-go-V3m5WkX75O7