iT邦幫忙

2023 iThome 鐵人賽

DAY 3
0
自我挑戰組

Go in 3o系列 第 3

[Day03] Go in 30 - 變數與算符 Part02 運算子與指標

  • 分享至 

  • xImage
  •  

一、運算子分類

算符又稱為運算子,算數、比較、邏輯運算子使用比來跟其它程式語言都差不多,
以下是Operators的分類 :

  • 算術算符(arithmetic operators)
  • 比較算符(comparison operators)
  • 邏輯算符(logical operators)
    (~後續說明~)
  • 定址算符(address operators) : 專門用來處理指標(pointer)
  • 位元算符(bitwise operators)
  • 受理算符(receive operators) : Go語言特有的通道(channel)寫入或讀取值

二、零值(Zero Values)

所謂零值,是指該型別具有預設或是空值(empty value)。

型別 零值
整數型別 0
浮點數型別 0.0
字符串型別 空字串 ("" 或 "")
布林型別 false
陣列型別 依元素型別而定
切片型別 nil
地圖型別 nil
通道型別 nil
函式型別 nil
接口型別 nil
指標型別 nil
自訂結構型別 依欄位型別而定

三、值 vs 指標(pointers)

3.1 了解指標前

當我們使用 int、string、bool 這類值傳遞給函數時,Go 語言會在函數中複製這些值,建立出新的變數,這意味著你呼叫函數時,若函數對值做出更動,那麼原值也不會受到更動,能減少程式的錯誤。

這種傳值方式叫做值傳遞,在值傳遞中,函數接收的是原始值的一個複本,而不是原始值本身。這意味著在函數內對該值的修改不會影響到原始值。這種方式確保了函數內外的數據獨立性,有助於減少副作用和錯誤。

然而,這種記憶體管理系統,叫做堆疊(stack),每個參數都會在 stack 中獲得自己的記憶體,而越多值在函數之間傳遞,這樣複製動作越多就會消耗越多記憶體。

另一種傳值替代方式就是 Pointer 指標。

指標和值是兩回事,而指標唯一的用途就只是拿來取得值而已,
可以將指標想像成通往值的路標,想要取得值,就需要照的路牌走。

https://ithelp.ithome.com.tw/upload/images/20230918/201626937dsNsm9ZYa.jpg

(資料來源:GeekForGeek-Pointers in Golang)

指標變數存的是一個位址,而位址指向的空間才是值。

如圖所示變數x,儲存的值為100,而此變數的記憶體位址在0x0201,然而此時:

var x int = 100
var ptr *int = &x  // 使用 & 運算符可以取得變數的記憶體地址,並將其分配給指標變數

取值

fmt.Println(ptr) //輸出結果位址 : 0x0201
fmt.Println(*ptr) // 輸出結果是位址指向的值 : 100
// 可以使用 * 運算符來訪問指標指向的值,這稱為解引用。

關於指標變數賦值取值回在後續說明。

3.2 pointers 在 Go 語言中的角色

當談到指標(pointers)時,我們需要考慮到它們在 Go 語言中的角色
以及與 "pass by reference" 之間的區別。

指標的角色:

你可以將指標想像成是通向值的路標。就像在地圖上跟著路牌找到特定的地點一樣,透過指標,我們可以找到存儲在某個位置的值,這些指標本身並不儲存實際的數據,它們只是指向某個變數或數據的位置。換句話說,就是存儲記憶體"位置"的變數。

"pass by reference" 和 Go 的指標:

"Pass by reference" 是一種函數參數傳遞方式,其中函數接收的是變數的引用或記憶體位置,而不是變數的複本。當你通過引用對變數進行更改時,原始變數也會受到影響,因為它們實際上指向相同的記憶體位置。

在 Go 語言中,函數的參數傳遞方式是 "pass by value",這意味著函數接收的是變數的複本,而不是原始變數的引用。然而,Go 語言提供了指標的概念,你可以將指標傳遞給函數,並在函數內部通過指標修改原始變數的值。

這裡的關鍵區別在於,Go 語言不是直接 "pass by reference",而是透過指標實現 "pass by value with a pointer"。也就是說,函數接收的是指標的副本,但這個指標副本仍然指向相同的記憶體位置,因此可以影響原始變數。

總結來說

指標在 Go 中扮演著重要的角色,用於間接訪問值。Go 語言使用 "pass by value with a pointer" 的方式處理函數參數傳遞,這意味著函數接收的是指標的副本,但這些副本仍然指向相同的記憶體位置,因此可以修改原始變數的值。這不同於嚴格的 "pass by reference" 概念,其中函數接收的是原始變數的引用。

3.3 取得指標

取得指標有幾種常見的方式,我們可以使用 * 關鍵字來宣告指標變數,也可以使用 new() 函式和 & 運算符來取得指標。

(1) 在型別前面加上 *

var <變數> *<型別>

上述宣告方式初值為nil。

var ptr *int // 宣告一個指向整數型別的指標變數

(2) Go 語言提供了 new() 函式,可以用來取得特定型別的記憶體並返回其指標。new() 函式的工作方式是分配足夠的記憶體來容納該型別的零值,然後返回這個記憶體的指標。以下是一個使用 new() 函式的範例:

<變數> := new(<型別>)
var <變數> = new(<型別>)
var ptr = new(int) // 使用 new() 取得一個整數的指標

這種方式將創建一個指向指定型別的指標,並為其分配記憶體,初值為該型別的零值(對於整數,初值為 0)。

(3) 直接使用 & 運算符來取得現有變數的指標:

<變數 1> := &<變數 2>

例如 :

package main

import "fmt"

func main() {
    // 宣告一個整數變數 x
    x := 42

    // 取得變數 x 的指標,並存儲在 ptr 中
    ptr := &x

    // 打印變數 x 的值和指標 ptr 的值
    fmt.Println("x 的值:", x)
    fmt.Println("ptr 的值:", *ptr) // 使用 * 運算符來取得指標指向的值
}

這個程式首先宣告一個整數變數 x,然後使用 & 運算符來取得變數 x 的指標,並將它存儲在變數 ptr 中。最後,使用 * 運算符來取得指標 ptr 指向的值,並將其印出。

這些方式都可以用來取得指標,但它們的用途略有不同:

  • 使用 new() 函式時,你可以初始化一個指標並為其分配記憶體,
  • 使用 * 宣告方式時,你需要在後續的程式碼中明確地指定這個指標變數指向的記憶體位置。
  • 使用 & 運算符可以直接取得現有變數的指標,而不需要額外的初始化過程。

3.4 從指標取得值(Dereferencing the Pointer)

從指標取得值的過程稱為解引用(Dereferencing),這是指標的一個重要操作。解引用允許我們取得指標指向的記憶體位置上存儲的值,也就是原始變數的值。要解引用一個指標,我們使用 * 運算符,然後後接上指標變數的名稱。

<值> = *<指標變數>

這個操作會取得指標指向的記憶體位置上的值,並將其賦值給一個新的變數。這樣,我們就可以使用該變數來訪問和操作原始變數的值。

需要注意 常見的錯誤是試圖去解除一個無值指標,也就是nil,Go不會幫你檢查,等實際運行時才會報錯,
Dereferencing之前,最好先檢查使否為nil。

import "fmt"

func main() {
	var p *int
	//試圖解無值指標
	if p != nil {
		fmt.Println(*p)
	} else {
		fmt.Println("p 是 nil 啦")
	}
}

關於指標用程式範例來了解 :

package main

import "fmt"

func main() {

    // 使用 var 關鍵字聲明一個變數 y,並將其初始化為 500
    var y = 500

    // 使用 var 關鍵字聲明一個指標變數 p,並將其初始化為變數 y 的記憶體地址。
    var p = &y

    // 輸出 y 變數的值,這是 500。
    fmt.Println("輸出 y 變數的值 = ", y)

    // 輸出變數 y 的記憶體地址。
    fmt.Println("輸出變數 y 的記憶體地址。", &y)

    // 輸出指標變數 p 的值,這是變數 y 的記憶體地址。
    fmt.Println("輸出指標變數 p 的值,這是變數 y 的記憶體地址 : ", p)

    // 使用 * 運算符對指標變數進行解引用,這將返回指標所指向的記憶體位置上存儲的值,即變數 y 的值。
    fmt.Println("解引用,這將返回指標所指向的記憶體位置上存儲的值,即變數 y 的值 : ", *p)

    // 通過指標 p 修改變數 y 的值,將其設置為 500。
    *p = 1000

    // 再次輸出 y 變數的值,現在它已經更改為 500。
    fmt.Println("再次輸出 y 變數的值,現在它已經更改為 : ", y)

}

輸出結果 :

https://ithelp.ithome.com.tw/upload/images/20230918/20162693pHf4z69qLw.png

四、將指標傳遞給的函式

函式的部分會在後續做介紹。
這邊著重重點在於,如果是以指標形式傳入函式,如果對其值做變動,就真的會改變原始變數,使用這類設計時需要謹慎。

4.1 創建指標並將其傳遞給函式

package main

import "fmt"

// 定義一個帶有整數類型指標參數的函式
func ptf(a *int) {

    // 解引用指針並賦值
    *a = 500
}

func main() {

    // 定義一個普通變數 x
    var x = 100

    fmt.Printf("函式調用前 x 的值為:%d\n", x)

    // 定義一個指標變數 pa 並將 x 的地址分配給它
    var pa *int = &x

    // 通過將指針傳遞給函式來調用函式
    ptf(pa) 

    fmt.Printf("函式調用後 x 的值為:%d\n", x)
}

輸出結果 :
https://ithelp.ithome.com.tw/upload/images/20230918/201626930xfKfBLz58.png

4.2 將變數的地址直接傳遞給函式

package main

import "fmt"

// 定義一個帶有整數類型指針參數的函式
func ptf(a *int) {

    // 解引用指針並賦值
    *a = 500
}

func main() {

    // 定義一個普通變數 x
    var x = 100

    fmt.Printf("函式調用前 x 的值為:%d\n", x)

    // 通過傳遞變數 x 的地址來調用函式
    ptf(&x)

    fmt.Printf("函式調用後 x 的值為:%d\n", x)
}

補充說明fmt.Printf()函式

fmt.Printf() 使用一種格式化樣板語言(template language)
官方文件

樣板 說明
%v 默認格式
%+v 添加字段名
%#v Go 語法表示
%T 顯示值的類型
%t 布林值
%d 十進制整數
%b 二進制表示
%o 八進制表示
%x 十六進制表示(小寫字母)
%X 十六進制表示(大寫字母)
%c 字符
%s 字符串
%q 雙引號字串
%f 浮點數
%e 科學記號(小寫'e')
%E 科學記號(大寫'E')
%g 更簡潔的 %e 或 %f
%G 更簡潔的 %E 或 %f
%p 指針的十六進制表示
%U Unicode 格式:U+1234
%v 值的默認格式
%b 布林值(true 或 false)
%x 十六進制表示(小寫字母)
%X 十六進制表示(大寫字母)
%d 十進制整數
%o 八進制表示
%s 字符串
%q 雙引號字串
%v 默認格式

使用fmt.Printdf()必須在字串尾部自己加上換行符號 (\n)

以上就是主要著重於指標在Go語言的基本的整理,如果有觀念或文章上的錯誤,歡迎各位提出~~~ > <。


上一篇
[Day02] Go in 30 - 變數與算符 part01 變數
下一篇
[Day04] Go in 30 - 變數與算符 part03 常數、列舉、Scope,套一些流程控制
系列文
Go in 3o30
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言