Go 語言指針
我們都知道,變量是一種使用方便的占位符,用于引用計算機內存地址。
Golang 支持指針類型 *T,指針的指針 **T,以及包含包名前綴的 *<package>.T。
~~~
? 默認值 nil,沒有 NULL 常量。
? 操作符 "&" (取地址符) 取變量地址,"*" (取值符)透過指針訪問目標對象。
? 不支持指針運算,不支持 "->" 運算符,直接用 "." 訪問目標成員。
~~~
以下實例演示了變量在內存中地址:
~~~
package main
import "fmt"
func main() {
var a int = 10
fmt.Printf("變量的地址: %x\n", &a)
}
~~~
執行以上代碼輸出結果為:
~~~
變量的地址: c420012058
~~~
現在我們已經了解了什么是內存地址和如何去反問它。接下來我們將具體介紹指針。
什么是指針
一個指針變量可以指向任何一個值的內存地址,它指向那個值的內存地址。
類似于變量和常量,在使用指針前你需要聲明指針。
指針聲明格式如下:
var name *類型
指針聲明:
~~~
package main
var ip *int /* 指向整型*/ //聲明一個int值得指針變量
var fp *float32 /* 指向浮點型 */
var sp *string /* 指向字符串類型 */
func main() {
}
~~~
如何使用指針
指針使用流程:
定義指針變量。
為指針變量賦值。
訪問指針變量中指向地址的值。
在指針類型前面加上 * 號(前綴)來獲取指針所指向的內容。
~~~
package main
import "fmt"
func main() {
var a int = 20 /* 聲明實際變量 */
var ip *int /* 聲明指針變量 */
ip = &a /* 指針變量的存儲地址 */
fmt.Printf("a 變量的地址是: %x\n", &a)
/* 指針變量的存儲地址 */
fmt.Printf("ip 變量的存儲地址: %x\n", ip)
/* 使用指針訪問值 */
fmt.Printf("*ip 變量的值: %d\n", *ip)
}
~~~
輸出結果:
~~~
a 變量的地址是: c420012058
ip 變量的存儲地址: c420012058
*ip 變量的值: 20
~~~
直接用指針訪問目標對象成員:
~~~
package main
import (
"fmt"
)
func main() {
type data struct{ a int }
var d = data{1234}
var p *data
p = &d
fmt.Printf("%p, %v\n", p, p.a) // 直接用指針訪問目標對象成員,無須轉換。
}
~~~
輸出結果:
~~~
0xc420012058, 1234
~~~
不能對指針做加減法等運算。
~~~
package main
func main() {
x := 1234
p := &x
p++
}
~~~
輸出結果:
~~~
./main.go:6:3: invalid operation: p++ (non-numeric type *int)
~~~
可以在 unsafe.Pointer 和任意類型指針間進 轉換。
~~~
package main
import (
"fmt"
"unsafe"
)
func main() {
x := 0x12345678
p := unsafe.Pointer(&x) // *int -> Pointer
n := (*[4]byte)(p) // Pointer -> *[4]byte
for i := 0; i < len(n); i++ {
fmt.Printf("%X ", n[i])
}
fmt.Println()
}
~~~
輸出結果:
~~~
78 56 34 12
~~~
返回局部變量指針是安全的,編譯器會根據需要將其分配在 GC Heap 上。
~~~
package main
func main() {}
func test() *int {
x := 100
return &x // 在堆上分配 x 內存。但在內聯時,也可能直接分配在目標棧。
}
~~~
將 Pointer 轉換成 uintptr,可變相實現指針運算。
~~~
package main
import (
"fmt"
"unsafe"
)
func main() {
d := struct {
s string
x int
}{"abc", 100}
p := uintptr(unsafe.Pointer(&d)) // *struct -> Pointer -> uintptr
p += unsafe.Offsetof(d.x) // uintptr + offset
p2 := unsafe.Pointer(p) // uintptr -> Pointer
px := (*int)(p2) // Pointer -> *int
*px = 200 // d.x = 200
fmt.Printf("%#v\n", d)
}
~~~
輸出結果:
~~~
struct { s string; x int }{s:"abc", x:200}
~~~
注意:GC 把 uintptr 當成普通整數對象,它無法阻止 "關聯" 對象被回收。
Go 空指針 (nil)
當一個指針被定義后沒有分配到任何變量時,它的值為 nil。
nil 指針也稱為空指針。
nil在概念上和其它語言的null、None、nil、NULL一樣,都指代零值或空值。
一個指針變量通常縮寫為 ptr。
實例:
~~~
package main
import "fmt"
func main() {
var ptr *int
fmt.Printf("ptr 的值為 : %x\n", ptr)
}
~~~
輸出結果:
~~~
ptr 的值為 : 0
~~~
空指針判斷:
~~~
package main
import (
"fmt"
)
func main() {
var ptr1 *int
var i int = 1
ptr2 := &i
if ptr1 == nil {
fmt.Println("prt1 是空指針")
}
if ptr2 != nil {
fmt.Println("prt2 不是空指針")
}
}
~~~
輸出結果:
~~~
prt1 是空指針
prt2 不是空指針
~~~
Go 指針數組
在我們了解指針數組前,先看個實例,定義了長度為 3 的整型數組:
~~~
package main
import "fmt"
const MAX int = 3
func main() {
a := [MAX]int{10, 100, 200}
var i int
for i = 0; i < MAX; i++ {
fmt.Printf("a[%d] = %d\n", i, a[i])
}
}
~~~
輸出結果:
~~~
a[0] = 10
a[1] = 100
a[2] = 200
~~~
有一種情況,我們可能需要保存數組,這樣我們就需要使用到指針。
以下聲明了整型指針數組:
~~~
var ptr [MAX]*int;
~~~
ptr 為整型指針數組。因此每個元素都指向了一個值。以下實例的三個整數將存儲在指針數組中:
~~~
package main
import "fmt"
const MAX int = 3
func main() {
a := []int{9, 99, 999}
var i int
var ptr [MAX]*int
for i = 0; i < MAX; i++ {
ptr[i] = &a[i] /* 整數地址賦值給指針數組 */
}
for i = 0; i < MAX; i++ {
fmt.Printf("a[%d] = %d\n", i, *ptr[i])
}
}
~~~
輸出結果:
~~~
a[0] = 9
a[1] = 99
a[2] = 999
~~~
如果一個指針變量存放的又是另一個指針變量的地址,則稱這個指針變量為指向指針的指針變量。
當定義一個指向指針的指針變量時,第一個指針存放第二個指針的地址,第二個指針存放變量的地址:
指向指針的指針變量聲明格式如下:
~~~
var ptr **int
~~~
以上指向指針的指針變量為整型。
訪問指向指針的指針變量值需要使用兩個 * 號,如下所示:
~~~
package main
import "fmt"
func main() {
var a int
var ptr *int
var pptr **int
a = 3000
/* 指針 ptr 地址 */
ptr = &a
/* 指向指針 ptr 地址 */
pptr = &ptr
/* 獲取 pptr 的值 */
fmt.Printf("變量 a = %d\n", a)
fmt.Printf("指針變量 *ptr = %d\n", *ptr)
fmt.Printf("指向指針的指針變量 **pptr = %d\n", **pptr)
}
~~~
輸出結果:
~~~
變量 a = 3000
指針變量 *ptr = 3000
指向指針的指針變量 **pptr = 3000
~~~
Go 像函數傳遞指針參數
Go 語言允許向函數傳遞指針,志需要在函數定義的參數上設置為指針類型即可。
以下實例演示了如何向函數傳遞指針,并在函數調用后修改函數內的值,:
~~~
package main
import "fmt"
func main() {
/* 定義局部變量 */
var a int = 100
var b int = 200
fmt.Printf("交換前 a 的值 : %d\n", a)
fmt.Printf("交換前 b 的值 : %d\n", b)
/* 調用函數用于交換值
* &a 指向 a 變量的地址
* &b 指向 b 變量的地址
*/
swap(&a, &b)
fmt.Printf("交換后 a 的值 : %d\n", a)
fmt.Printf("交換后 b 的值 : %d\n", b)
}
func swap(x *int, y *int) {
var temp int
temp = *x /* 保存 x 地址的值 */
*x = *y /* 將 y 賦值給 x */
*y = temp /* 將 temp 賦值給 y */
}
~~~
輸出結果:
~~~
交換前 a 的值 : 100
交換前 b 的值 : 200
交換后 a 的值 : 200
交換后 b 的值 : 100
~~~
指針類型轉換
Go語言是不允許兩個指針類型進行轉換的。
我們一般使用*T作為一個指針類型,表示一個指向類型T變量的指針。為了安全的考慮,兩個不同的指針類型不能相互轉換,比如*int不能轉為*float64。
~~~
package main
import "fmt"
func main() {
i := 10
ip := &i
var fp *float64 = (*float64)(ip)
fmt.Println(fp)
}
~~~
輸出結果:
~~~
./main.go:9:30: cannot convert ip (type *int) to type *float64
~~~
以上代碼我們在編譯的時候,會提示cannot convert ip (type *int) to type *float64,也就是不能進行強制轉型。那如果我們還是需要進行轉換怎么做呢?這就需要我們使用unsafe包里的Pointer了,下面我們先看看unsafe.Pointer是什么,然后再介紹如何轉換。
Pointer
unsafe.Pointer是一種特殊意義的指針,它可以包含任意類型的地址,有點類似于C語言里的void*指針,全能型的。
~~~
package main
import (
"fmt"
"unsafe"
)
func main() {
i := 10
ip := &i
var fp *float64 = (*float64)(unsafe.Pointer(ip))
*fp = *fp * 3
fmt.Println(i)
}
~~~
輸出結果:
~~~
30
~~~
以上示例,我們可以把*int轉為*float64,并且我們嘗試了對新的*float64進行操作,打印輸出i,就會發現i的址同樣被改變。
以上這個例子沒有任何實際的意義,但是我們說明了,通過unsafe.Pointer這個萬能的指針,我們可以在*T之間做任何轉換。
~~~
type ArbitraryType int
type Pointer *ArbitraryType
~~~
可以看到unsafe.Pointer其實就是一個*int,一個通用型的指針。
我們看下關于unsafe.Pointer的4個規則。
任何指針都可以轉換為unsafe.Pointer
unsafe.Pointer可以轉換為任何指針
uintptr可以轉換為unsafe.Pointer
unsafe.Pointer可以轉換為uintptr
前面兩個規則我們剛剛已經演示了,主要用于*T1和*T2之間的轉換,那么最后兩個規則是做什么的呢?我們都知道*T是不能計算偏移量的,也不能進行計算,但是uintptr可以,所以我們可以把指針轉為uintptr再進行便宜計算,這樣我們就可以訪問特定的內存了,達到對不同的內存讀寫的目的。
下面我們以通過指針偏移修改Struct結構體內的字段為例,來演示uintptr的用法。
~~~
package main
import (
"fmt"
"unsafe"
)
func main() {
u := new(user)
fmt.Println(*u)
pName := (*string)(unsafe.Pointer(u))
*pName = "張三"
pAge := (*int)(unsafe.Pointer(uintptr(unsafe.Pointer(u)) + unsafe.Offsetof(u.age)))
*pAge = 20
fmt.Println(*u)
}
type user struct {
name string
age int
}
~~~
輸出結果:
~~~
{ 0}
{張三 20}
~~~
以上我們通過內存偏移的方式,定位到我們需要操作的字段,然后改變他們的值。
第一個修改user的name值的時候,因為name是第一個字段,所以不用偏移,我們獲取user的指針,然后通過unsafe.Pointer轉為*string進行賦值操作即可。
第二個修改user的age值的時候,因為age不是第一個字段,所以我們需要內存偏移,內存偏移牽涉到的計算只能通過uintptr,所我們要先把user的指針地址轉為uintptr,然后我們再通過unsafe.Offsetof(u.age)獲取需要偏移的值,進行地址運算(+)偏移即可。
現在偏移后,地址已經是user的age字段了,如果要給它賦值,我們需要把uintptr轉為*int才可以。所以我們通過把uintptr轉為unsafe.Pointer,再轉為*int就可以操作了。
這里我們可以看到,我們第二個偏移的表達式非常長,但是也千萬不要把他們分段,不能像下面這樣。
~~~
temp:=uintptr(unsafe.Pointer(u))+unsafe.Offsetof(u.age)
pAge:=(*int)(unsafe.Pointer(temp))
*pAge = 20
~~~
邏輯上看,以上代碼不會有什么問題,但是這里會牽涉到GC,如果我們的這些臨時變量被GC,那么導致的內存操作就錯了,我們最終操作的,就不知道是哪塊內存了,會引起莫名其妙的問題。
小結
unsafe是不安全的,所以我們應該盡可能少的使用它,比如內存的操縱,這是繞過Go本身設計的安全機制的,不當的操作,可能會破壞一塊內存,而且這種問題非常不好定位。
當然必須的時候我們可以使用它,比如底層類型相同的數組之間的轉換;比如使用sync/atomic包中的一些函數時;還有訪問Struct的私有字段時;該用還是要用,不過一定要慎之又慎。
還有,整個unsafe包都是用于Go編譯器的,不用運行時,在我們編譯的時候,Go編譯器已經把他們都處理了。
- 序言
- 目錄
- 環境搭建
- Linux搭建golang環境
- Windows搭建golang環境
- Mac搭建golang環境
- 介紹
- 1.Go語言的主要特征
- 2.golang內置類型和函數
- 3.init函數和main函數
- 4.包
- 1.工作空間
- 2.源文件
- 3.包結構
- 4.文檔
- 5.編寫 Hello World
- 6.Go語言 “ _ ”(下劃線)
- 7.運算符
- 8.命令
- 類型
- 1.變量
- 2.常量
- 3.基本類型
- 1.基本類型介紹
- 2.字符串String
- 3.數組Array
- 4.類型轉換
- 4.引用類型
- 1.引用類型介紹
- 2.切片Slice
- 3.容器Map
- 4.管道Channel
- 5.指針
- 6.自定義類型Struct
- 編碼格式轉換
- 流程控制
- 1.條件語句(if)
- 2.條件語句 (switch)
- 3.條件語句 (select)
- 4.循環語句 (for)
- 5.循環語句 (range)
- 6.循環控制Goto、Break、Continue
- 函數
- 1.函數定義
- 2.參數
- 3.返回值
- 4.匿名函數
- 5.閉包、遞歸
- 6.延遲調用 (defer)
- 7.異常處理
- 8.單元測試
- 壓力測試
- 方法
- 1.方法定義
- 2.匿名字段
- 3.方法集
- 4.表達式
- 5.自定義error
- 接口
- 1.接口定義
- 2.執行機制
- 3.接口轉換
- 4.接口技巧
- 面向對象特性
- 并發
- 1.并發介紹
- 2.Goroutine
- 3.Chan
- 4.WaitGroup
- 5.Context
- 應用
- 反射reflection
- 1.獲取基本類型
- 2.獲取結構體
- 3.Elem反射操作基本類型
- 4.反射調用結構體方法
- 5.Elem反射操作結構體
- 6.Elem反射獲取tag
- 7.應用
- json協議
- 1.結構體轉json
- 2.map轉json
- 3.int轉json
- 4.slice轉json
- 5.json反序列化為結構體
- 6.json反序列化為map
- 終端讀取
- 1.鍵盤(控制臺)輸入fmt
- 2.命令行參數os.Args
- 3.命令行參數flag
- 文件操作
- 1.文件創建
- 2.文件寫入
- 3.文件讀取
- 4.文件刪除
- 5.壓縮文件讀寫
- 6.判斷文件或文件夾是否存在
- 7.從一個文件拷貝到另一個文件
- 8.寫入內容到Excel
- 9.日志(log)文件
- server服務
- 1.服務端
- 2.客戶端
- 3.tcp獲取網頁數據
- 4.http初識-瀏覽器訪問服務器
- 5.客戶端訪問服務器
- 6.訪問延遲處理
- 7.form表單提交
- web模板
- 1.渲染終端
- 2.渲染瀏覽器
- 3.渲染存儲文件
- 4.自定義io.Writer渲染
- 5.模板語法
- 時間處理
- 1.格式化
- 2.運行時間
- 3.定時器
- 鎖機制
- 互斥鎖
- 讀寫鎖
- 性能比較
- sync.Map
- 原子操作
- 1.原子增(減)值
- 2.比較并交換
- 3.導入、導出、交換
- 加密解密
- 1.md5
- 2.base64
- 3.sha
- 4.hmac
- 常用算法
- 1.冒泡排序
- 2.選擇排序
- 3.快速排序
- 4.插入排序
- 5.睡眠排序
- 設計模式
- 創建型模式
- 單例模式
- 抽象工廠模式
- 工廠方法模式
- 原型模式
- 結構型模式
- 適配器模式
- 橋接模式
- 合成/組合模式
- 裝飾模式
- 外觀模式
- 享元模式
- 代理模式
- 行為性模式
- 職責鏈模式
- 命令模式
- 解釋器模式
- 迭代器模式
- 中介者模式
- 備忘錄模式
- 觀察者模式
- 狀態模式
- 策略模式
- 模板模式
- 訪問者模式
- 數據庫操作
- golang操作MySQL
- 1.mysql使用
- 2.insert操作
- 3.select 操作
- 4.update 操作
- 5.delete 操作
- 6.MySQL事務
- golang操作Redis
- 1.redis介紹
- 2.golang鏈接redis
- 3.String類型 Set、Get操作
- 4.String 批量操作
- 5.設置過期時間
- 6.list隊列操作
- 7.Hash表
- 8.Redis連接池
- golang操作ETCD
- 1.etcd介紹
- 2.鏈接etcd
- 3.etcd存取
- 4.etcd監聽Watch
- golang操作kafka
- 1.kafka介紹
- 2.寫入kafka
- 3.kafka消費
- golang操作ElasticSearch
- 1.ElasticSearch介紹
- 2.kibana介紹
- 3.寫入ElasticSearch
- NSQ
- 安裝
- 生產者
- 消費者
- beego框架
- 1.beego框架環境搭建
- 2.參數配置
- 1.默認參數
- 2.自定義配置
- 3.config包使用
- 3.路由設置
- 1.自動匹配
- 2.固定路由
- 3.正則路由
- 4.注解路由
- 5.namespace
- 4.多種數據格式輸出
- 1.直接輸出字符串
- 2.模板數據輸出
- 3.json格式數據輸出
- 4.xml格式數據輸出
- 5.jsonp調用
- 5.模板處理
- 1.模板語法
- 2.基本函數
- 3.模板函數
- 6.請求處理
- 1.GET請求
- 2.POST請求
- 3.文件上傳
- 7.表單驗證
- 1.表單驗證
- 2.定制錯誤信息
- 3.struct tag 驗證
- 4.XSRF過濾
- 8.靜態文件處理
- 1.layout設計
- 9.日志處理
- 1.日志處理
- 2.logs 模塊
- 10.會話控制
- 1.會話控制
- 2.session 包使用
- 11.ORM 使用
- 1.鏈接數據庫
- 2. CRUD 操作
- 3.原生 SQL 操作
- 4.構造查詢
- 5.事務處理
- 6.自動建表
- 12.beego 驗證碼
- 1.驗證碼插件
- 2.驗證碼使用
- beego admin
- 1.admin安裝
- 2.admin開發
- beego 熱升級
- gin框架
- 安裝使用
- 項目
- 秒殺項目
- 日志收集
- 面試題
- 面試題一
- 面試題二
- 錯題集
- Go語言陷阱和常見錯誤
- 常見語法錯誤
- 初級
- 中級
- 高級
- Go高級應用
- goim
- goim 啟動流程
- goim 工作流程
- goim 結構體
- gopush
- gopush工作流程
- gopush啟動流程
- gopush業務流程
- gopush應用
- gopush新添功能
- rpc
- HTTP RPC
- TCP RPC
- JSON RPC
- 常見RPC開源框架
- pprof
- pprof介紹
- pprof應用
- 封裝 websocket
- zookeeper
- 基本操作測試
- 簡單的分布式server
- Zookeeper命令行使用
- cgo
- Go語言 demo
- 用Go語言計算一個人的年齡,生肖,星座
- 超簡易Go語言實現的留言板代碼
- 信號處理模塊,可用于在線加載配置,配置動態加載的信號為SIGHUP
- 陽歷和陰歷相互轉化的工具類 golang版本
- 錯誤總結