【Go語言從入門到實戰】反射編程、Unsafe篇

反射編程

reflect.TypeOf vs reflect.ValueOf

image-20230517164355957

func TestTypeAndValue(t *testing.T) {var a int64 = 10t.Log(reflect.TypeOf(a), reflect.ValueOf(a))t.Log(reflect.ValueOf(a).Type())
}

image-20230517165343010

判斷類型 - Kind()

當我們需要對反射回來的類型做判斷時,Go 語言內置了一個枚舉,可以通過 Kind() 來返回這個枚舉值:

const (Invalid Kind = iotaBoolIntInt8Int16Int32Int64UintUint8Uint16Uint32Uint64// ...
)
package reflectimport ("fmt""reflect""testing"
)// 檢查反射類型
// 用空接口接收任意類型
func CheckType(v interface{}) {t := reflect.TypeOf(v)switch t.Kind() {case reflect.Int, reflect.Int32, reflect.Int64:fmt.Println("Int")case reflect.Float32, reflect.Float64:fmt.Println("Float")default:fmt.Println("unknown type")}
}func TestBasicType(t *testing.T) {var f float32 = 1.23CheckType(f)
}

image-20230517164832447

利用反射編寫靈活的代碼

image-20230518104627148

reflect.TypeOf()reflect.ValueOf() 都有 FieldByName() 方法。

// s必須是一個 struct 類型// reflect.ValueOf()只會返回一個值
reflect.ValueOf(s).FieldByName("Name")// reflect.TypeOf()可以返回兩個值,第二個值可以用來判斷這個值有沒有;
reflect.TypeOf(s).FieldByName("Name")

FieldByName() 方法返回的是一個 StructField 類型的值。

image-20230518104446918

我們可以通過這個 StructField 來訪問 Struct Tag

type StructField struct {// Name是字段的名字。PkgPath是非導出字段的包路徑,對導出字段該字段為""。// 參見http://golang.org/ref/spec#Uniqueness_of_identifiersName    stringPkgPath stringType      Type      // 字段的類型Tag       StructTag // 字段的標簽Offset    uintptr   // 字段在結構體中的字節偏移量Index     []int     // 用于Type.FieldByIndex時的索引切片Anonymous bool      // 是否匿名字段
}

FieldByName() 方法調用者必須是一個 struct,而不是指針,源碼如下:

image-20230518104417843

// 訪問 MethodByName() 必須是指針類型
reflect.ValueOf(&s).MethodByName("method_name").Call([]reflect.Value{reflect.ValueOf("new_value")})
type Employee struct {EmployeeID string// 注意后面的 struct tag 的寫法,詳情見第5點講解Name string `format:"normal"`Age  int
}// 更新名字,注意這里的 e 是指針類型
func (e *Employee) UpdateName(newVal string) {e.Name = newVal
}// 通過反射調用結構體的方法
func TestInvokeByName(t *testing.T) {e := Employee{"1", "Jane", 18}// reflect.TypeOf()可以返回兩個值,第二個值可以用來判斷這個值有沒有;// 而reflect.ValueOf()只會返回一個值t.Logf("Name: value(%[1]v), Type(%[1]T)", reflect.ValueOf(e).FieldByName("Name"))if nameField, ok := reflect.TypeOf(e).FieldByName("Name"); !ok {t.Error("Failed to get 'Name' field")} else {// 獲取反射取到的字段的 tag 的值t.Log("Tag:Format", nameField.Tag.Get("format"))}// 訪問 MethodByName() 必須是指針類型 reflect.ValueOf(&e).MethodByName("UpdateName").Call([]reflect.Value{reflect.ValueOf("Mike")})t.Log("After update name: ", e)
}

image-20230518104908547

Elem()

因為 FieldByName() 必須要結構體才能調用,如果參數是一個指向結構體的指針,我們需要用到 Elem() 方法,它會幫你獲得指針指向的結構。

  • Elem() 用來獲取指針指向的值
  • 如果參數不是指針,會報 panic 錯誤
  • 如果參數值是 nil,獲取的值為 0
// reflect.ValueOf(demoPtr)).Elem() 返回的是字段的值
reflect.ValueOf(demoPtr).Elem()// reflect.ValueOf(st)).Elem().Type() 返回的是字段類型
reflect.ValueOf(demoPtr).Elem().Type()// 傳遞指針類型參數調用 FieldByName() 方法
reflect.ValueOf(demoPtr).Elem().FieldByName("Name")// 傳遞指針類型參數調用 FieldByName() 方法
reflect.ValueOf(demoPtr).Elem().Type().FieldByName("Name")

Struct Tag

結構體里面可以對某些字段做特殊的標記,它是一個 `key: “value”` 的格式。

image-20230518103104012

type Demo struct {// 先用這個符號(``)包起來,然后寫上 key: value 的格式Name string `format:"normal"`
}

Go 內置的 Json 解析會用到 tag 來做一些標記。

反射是把雙刃劍

反射是一個強大并富有表現力的工具,能讓我們寫出更靈活的代碼。但是反射不應該被濫用,原因有以下三個:

  1. 基于反射的代碼是極其脆弱的,反射中的類型錯誤會在真正運行的時候才會引發 panic,那很可能是在代碼寫完的很長時間之后。
  2. 大量使用反射的代碼通常難以理解。
  3. 反射的性能低下,基于反射實現的代碼通常比正常代碼運行速度慢一到兩個數量級。

萬能程序

DeepEqual

我們都知道兩個 map 類型之間是不能互相比較的,兩個 slice 類型之間也不能進行比較,但是反射包中的 DeepEqual() 可以幫我們實現這個功能。

用 DeepEqual() 比較 map

// 用 DeepEqual() 比較兩個 map 類型
func TestMapComparing(t *testing.T) {m1 := map[int]string{1: "one", 2: "two", 3: "three"}m2 := map[int]string{1: "one", 2: "two", 3: "three"}if reflect.DeepEqual(m1, m2) {t.Log("yes")} else {t.Log("no")}
}

image-20230519170229458

用 DeepEqual() 比較 slice

// 用 DeepEqual() 比較兩個切片類型
func TestSliceComparing(t *testing.T) {s1 := []int{1, 2, 3, 4}s2 := []int{1, 2, 3, 5}if reflect.DeepEqual(s1, s2) {t.Log("yes")} else {t.Log("no")}
}

image-20230519170305142

用反射實現萬能程序

場景:我們有 Employee Customer 兩個結構體,二者有兩個相同的字段(Name 和 Age),我們希望寫一個通用的程序,可以同時填充這兩個不同的結構體。

type Employee struct {EmployeeId intName       stringAge        int
}type Customer struct {CustomerId intName       stringAge        int
}// 用同一個數據填充不同的結構體
// 思路:既然是不同的結構體,那么要想通用,所以參數必須是一個空接口才行。
// 因為是空接口,所有我們需要對參數類型寫斷言
func fillDifferentStructByData(st interface{}, data map[string]interface{}) error {// 先判斷傳過來的類型是不是指針if reflect.TypeOf(st).Kind() != reflect.Ptr {return errors.New("第一個參數必須傳一個指向結構體的指針")}// 再判斷指針指向的類型是否為結構體// Elem() 用來獲取指針指向的值// 如果參數不是指針,會報 panic 錯誤// 如果參數值是 nil, 獲取的值為 0if reflect.TypeOf(st).Elem().Kind() != reflect.Struct {return errors.New("第一個參數必須是一個結構體類型")}if data == nil {return errors.New("填充用的數據不能為nil")}var (field reflect.StructFieldok    bool)for key, val := range data {// 如果結構體里面沒有 key 這個字段,則跳過// reflect.ValueOf(st)).Elem().Type() 返回的是字段類型// reflect.ValueOf(st)).Elem().Type() 等價于 reflect.TypeOf(st)).Elem()if field, ok = reflect.TypeOf(st).Elem().FieldByName(key); !ok {continue}// 如果字段的類型相同,則用 data 的數據填充這個字段的值if field.Type == reflect.TypeOf(val) {// reflect.ValueOf(st)).Elem() 返回的是字段的值reflect.ValueOf(st).Elem().FieldByName(key).Set(reflect.ValueOf(val))}}return nil
}// 填充姓名和年齡
func TestFillNameAndAge(t *testing.T) {// 聲明一個 map,用來存放數據,這些數據將會填充到 Employee 和 Customer 這兩個結構體中data := map[string]interface{}{"Name": "Jane", "Age": 18}e := Employee{}// 傳給通用的填充方法if err := fillDifferentStructByData(&e, data); err != nil {t.Fatal(err)}c := Customer{}// 傳給通用的填充方法if err := fillDifferentStructByData(&c, data); err != nil {t.Fatal(err)}t.Log(e)t.Log(c)
}

image-20230523202241804

兩個結構體的 name 和 age 都填充上了,符合預期。

不安全編程-UnSafe

不安全編程指的是 go 語言中有一個 package 叫:unsafe,它的使用場景一般是要和外部 c 程序實現的一些高效的庫來進行交互。

“不安全行為”的危險性

image-20231121161423772

Go 語言中是不支持強制類型轉換的,而我們一旦使用 unsafe.Pointer 拿到指針后,我們可以將它轉換為任意類型的指針,這樣我們是否能利用它來實現強制類型轉換呢?我們可以用代碼來測試一下:

func TestUnsafe(t *testing.T) {i := 10f := *(*float64)(unsafe.Pointer(&i))t.Log(unsafe.Pointer(&i))t.Log(f)
}

image-20231121162713754

可以看到結果根本不是 10,是一串數字字母的組合,所以這是非常危險的。

合理的類型轉換

在 Go 語言中,不同類型的指針是不允許相互賦值的,但是通過合理地使用 unsafe 包,則可以打破這種限制。

例如:int 類型是可以進行轉換賦值的。

func TestConvert1(t *testing.T) {var num int = 10var uintNum uint = *(*uint)(unsafe.Pointer(&num))var int32Num int32 = *(*int32)(unsafe.Pointer(&num))t.Log(num, uintNum, int32Num)t.Log(reflect.TypeOf(num), reflect.TypeOf(uintNum), reflect.TypeOf(int32Num))
}

image-20231121164940949

訪問修改結構體私有成員變量

type User struct {name stringid   int
}func TestOperateStruct(t *testing.T) {user := new(User)user.name = "張三"fmt.Printf("%+v\n", user)// 突破第一個私有變量,因為是結構體的第一個字段,所以不需要額外的指針計算*(*string)(unsafe.Pointer(user)) = "李四"fmt.Printf("%+v\n", user)// 突破第二個私有變量,因為是第二個成員字段,需要偏移一個字符串占用的長度即 16 個字節*(*int)(unsafe.Pointer(uintptr(unsafe.Pointer(user)) + uintptr(16))) = 1fmt.Printf("%+v\n", user)
}

image-20231121170209000

當然我們可以更簡單的獲取到結構體變量的偏移量,這樣就不需要自己計算了:

type Person struct {Name   stringAge    intHeight float64
}func TestUnSafeOffSet(t *testing.T) {nameOffset := unsafe.Offsetof(Person{}.Name)ageOffset := unsafe.Offsetof(Person{}.Age)heightOffset := unsafe.Offsetof(Person{}.Height)t.Log(nameOffset, ageOffset, heightOffset) // 輸出字段的偏移量
}

image-20231121171243427

實現 []byte 和字符串的零拷貝轉換

通過查看源碼,可以發現 slice 切片類型和 string 字符串類型具有類似的結構。

// runtime/slice.go
type slice struct {array unsafe.Pointer	// 底層數組指針,真正存放數據的地方len   int				// 切片長度,通過 len(slice) 返回cap   int				// 切片容量,通過 cap(slice) 返回
}// runtime/string.go
type stringStruct struct {str unsafe.Pointer	// 底層數組指針len int				// 字符串長度,可以通過 len(string) 返回
}

看到這里,你是不是發現很神奇,這兩個數據結構底層實現基本相同,而 slice 只是多了一個cap 字段。可以得出結論:slice 和 string 在內存布局上是對齊的,我們可以直接通過 unsafe 包進行轉換,而不需要申請額外的內存空間。

代碼實現

func StringToBytes(str string) []byte {var b []byte// 切片的底層數組、len字段,指向字符串的底層數組,len字段*(*string)(unsafe.Pointer(&b)) = str// 切片的 cap 字段賦值為 len(str) 的長度,切片的指針、len 字段各占8個字節,直接偏移16個字節*(*int)(unsafe.Pointer(uintptr(unsafe.Pointer(&b)) + 2*uintptr(8))) = len(str)return b
}func BytesToString(data []byte) string {// 直接轉換return *(*string)(unsafe.Pointer(&data))
}func TestStringAndBytesConvert(t *testing.T) {str := "hello"b := StringToBytes(str)t.Log(reflect.TypeOf(b), b)// 此時 b 已經是切片類型,我們再將它轉換為string類型s := BytesToString(b)t.Log(reflect.TypeOf(s), s)
}

image-20231121172404565

符合預期。

原子類型操作

我們會用到 golang 內置 package 中的 atomic 原子操作,它提供了指針的原子操作,通常用在并發讀寫一塊共享緩存時,保證線程安全。

我們在寫數據的時候寫在另外一塊空間,完全寫完之后,我們使用原子操作把讀的指針和寫的指針指向我們新寫入的空間,保證下次再讀的時候就是新寫好的內容了。指針的切換要具有線程安全的特性。

func TestAtomic(t *testing.T) {var shareBufPtr unsafe.Pointer// 寫方法writeDataFn := func() {data := []int{}for i := 0; i < 9; i++ {data = append(data, i)}// 使用原子操作將data的指針指向shareBufPtratomic.StorePointer(&shareBufPtr, unsafe.Pointer(&data))}// 讀方法readDataFn := func() {data := atomic.LoadPointer(&shareBufPtr) // 使用原子操作讀取shareBufPtrfmt.Println(data, *(*[]int)(data))       // 打印shareBufPtr中的數據}var wg sync.WaitGroupwriteDataFn()// 啟動3個讀協程,3個寫協程,每個協程執行3次讀/寫操作for i := 0; i < 3; i++ {wg.Add(1)go func() {for i := 0; i < 3; i++ {writeDataFn()time.Sleep(time.Microsecond * 100)}wg.Done()}()wg.Add(1)go func() {for i := 0; i < 3; i++ {readDataFn()time.Sleep(time.Microsecond * 100)}wg.Done()}()}wg.Wait()
}

image-20231121191610325

使用 atomic + unsafe 來實現共享 buffer 安全的讀寫。

總結

通過 unsafe 包,我們可以繞過 golang 編譯器的檢查,直接操作地址,實現一些高效的操作。但正如 golang 官方給它的命名一樣,它是不安全的,濫用的話可能會導致程序意外的崩潰。關于 unsafe 包,我們應該更關注于它的用法,生產環境不建議使用!!!

  • 筆記整理自極客時間視頻教程:Go語言從入門到實戰
  • UnSafe部分內容參考:go unsafe包使用指南

本文來自互聯網用戶投稿,該文觀點僅代表作者本人,不代表本站立場。本站僅提供信息存儲空間服務,不擁有所有權,不承擔相關法律責任。
如若轉載,請注明出處:http://www.pswp.cn/news/166764.shtml
繁體地址,請注明出處:http://hk.pswp.cn/news/166764.shtml
英文地址,請注明出處:http://en.pswp.cn/news/166764.shtml

如若內容造成侵權/違法違規/事實不符,請聯系多彩編程網進行投訴反饋email:809451989@qq.com,一經查實,立即刪除!

相關文章

【23真題】最簡單的211!均分141分!

今天分享的是23年河海大學863的信號與系統試題及解析。 我猜測是由于23年太簡單&#xff0c;均分都141分&#xff0c;導致24考研臨時新增一門數字信號處理&#xff01;今年考研的同學趕不上這么簡單的專業課啦&#xff01; 本套試卷難度分析&#xff1a;平均分為102和141分&a…

ECharts與DataV:數據可視化的得力助手

文章目錄 引言一、ECharts簡介優勢&#xff1a;劣勢&#xff1a; 二、DataV簡介優勢&#xff1a;劣勢&#xff1a; 三、ECharts與DataV的聯系四、區別與選擇五、如何選擇根據需求選擇技術棧考慮預算和商業考慮 結論我是將軍&#xff0c;我一直都在&#xff0c;。&#xff01; 引…

LeetCode題解:13. 羅馬數字轉整數,哈希表,JavaScript,詳細注釋

原題鏈接&#xff1a;13. 羅馬數字轉整數 解題思路&#xff1a; 本題涉及到的羅馬數字都是唯一的&#xff0c;因此可以創建一個哈希表&#xff0c;存儲羅馬數字和整數的對應關系。遍歷s&#xff0c;分別截取從i開始的2位和1位字符串&#xff0c;查看其在哈希表中的羅馬數字對…

pytest調用其他測試用例方法

pytest調用其他測試用例方法 一. 第一種方法&#xff0c;測試用例前置pytest.fixture() def test1():print("我是用例一") pytest.fixture(test1) def test2():print("我是用例二")二.第二種方法,如果不是同一文件中測試用例調用或者同一py文件中 def t…

3.10-容器的操作

這一節講解一下對于container我們可以進行哪些操作&#xff1f; 可以使用以下命令來停止正在運行的Docker容器&#xff1a; docker container stop <CONTAINER ID> 關于運行中的容器&#xff0c;我們可以進行的操作&#xff1a; 第一個是docker exec命令&#xff0c;這個…

NLP實踐——LLM生成過程中防止重復循環

NLP實踐——LLM生成過程中防止重復 1. 準備工作2. 問題分析3. 創建processor3.1 防止重復生成的processor3.2 防止數字無規則循環的processor 4. 使用 本文介紹如何使用LogitsProcessor避免大模型在生成過程中出現重復的問題。 1. 準備工作 首先實例化一個大模型&#xff0c;…

實時語音克隆:5 秒內生成任意文本的語音 | 開源日報 No.84

CorentinJ/Real-Time-Voice-Cloning Stars: 43.3k License: NOASSERTION 這個開源項目是一個實時語音克隆工具&#xff0c;可以在5秒內復制一種聲音&#xff0c;并生成任意文本的語音。 該項目的主要功能包括&#xff1a; 從幾秒鐘的錄音中創建聲紋模型根據給定文本使用參考…

數字化轉型沒錢?沒人?沒IT?低代碼平臺輕松幫你搞定

隨著數字技術的不斷滲透&#xff0c;數字化已經不僅僅是一個趨勢&#xff0c;而是深入人心的日常生活部分。在這樣的時代背景下&#xff0c;企業面臨的挑戰也愈發嚴峻&#xff1a;如何不斷創新&#xff0c;滿足用戶日益增長的業務需求&#xff1f; 傳統的開發方式&#xff0c;隨…

基于單片機設計的大氣氣壓檢測裝置(STC89C52+BMP180實現)

一、前言 本項目設計一個大氣氣壓檢測裝置&#xff0c;該裝置以單片機為基礎&#xff0c;采用STC89C52作為核心控制芯片&#xff0c;結合BMP180模塊作為氣壓傳感器。大氣氣壓&#xff0c;也就是由氣體重力在大氣層中產生的壓力&#xff0c;其變化與天氣預報、氣象觀測以及高度…

江蘇某市人民醫院實現IT基礎資源統一監控

一、背景介紹 江蘇某市人民醫院是一家擁有豐富醫療資源和龐大患者群體的醫療機構。隨著醫療業務的不斷發展&#xff0c;其IT系統的規模和復雜性也不斷增加&#xff0c;涉及各類IT資源&#xff0c;包括服務器、網絡設備、數據庫、應用軟件等。為了提高IT系統的可靠性和穩定性&am…

11.7統一功能處理

一.登錄攔截器 1.實現一個普通的類,實現HeadlerInterceptor接口,重寫preHeadler方法. 2.將攔截器添加到配置中,并設定攔截規則. 二.訪問前綴添加 方法1: 方法2:properties 三.統一異常處理 以上返回的是空指針異常,如果是別的異常就不會識別,建議加上最終異常 . 四.統一數據格…

英語學習軟件 Eudic歐路詞典 mac中文版介紹說明

歐路詞典 mac (Eudic) 是一個功能強大的英語學習工具&#xff0c;它包含了豐富的英語詞匯、短語和例句&#xff0c;并提供了發音、例句朗讀、單詞筆記等功能。 Eudic歐路詞典 mac 軟件介紹 多語種支持&#xff1a;歐路詞典支持多種語言&#xff0c;包括英語、中文、日語、法語…

uni微信小程序 map 添加padding

問題背景&#xff1a; 規劃駕車線路的時候&#xff0c;使用uni的include-points指定可視范圍的時候&#xff0c;會很極限。導致marker不能完全顯示。 解決方法 給地圖顯示范圍添加padding (推薦) <mapid"myMap":markers"markers":polyline"pol…

視頻服務網關的三大部署(二)

視頻網關是軟硬一體的一款產品&#xff0c;可提供多協議&#xff08;RTSP/ONVIF/GB28181/海康ISUP/EHOME/大華、海康SDK等&#xff09;的設備視頻接入、采集、處理、存儲和分發等服務&#xff0c; 配合視頻網關云管理平臺&#xff0c;可廣泛應用于安防監控、智能檢測、智慧園區…

spark寫入關系型數據庫的duplicateIncs參數使用

在看一段spark寫數據到關系型數據庫代碼時&#xff0c;發現一個參數沒有見過&#xff1a; df.write.format("org.apache.spark.sql.execution.datasources.jdbc2").options(Map("savemode" -> JDBCSaveMode.Update.toString,"driver" -> …

Android13 launcher循環切頁

launcher 常規切頁&#xff1a;https://blog.csdn.net/a396604593/article/details/125305234 循環切頁 我們知道&#xff0c;launcher切頁是在packages\apps\Launcher3\src\com\android\launcher3\PagedView.java的onTouchEvent中實現的。 1、滑動限制 public boolean onT…

Python與設計模式--門面模式

8-Python與設計模式–門面模式 一、火警報警器&#xff08;1&#xff09; 假設有一組火警報警系統&#xff0c;由三個子元件構成&#xff1a;一個警報器&#xff0c;一個噴水器&#xff0c; 一個自動撥打電話的裝置。其抽象如下&#xff1a; class AlarmSensor:def run(self):…

c語言習題1124

分別定義函數求圓的面積和周長。 寫一個函數&#xff0c;分別求三個數當中的最大數。 寫一個函數&#xff0c;計算輸入n個數的乘積 一個判斷素數的函數&#xff0c;在主函數輸入一個整數&#xff0c;輸出是否為素數的信息 寫一個函數求n! ,利用該函數求1&#xff01;2&…

功率半導體器件CV測試系統

概述 電容-電壓(C-V)測量廣泛用于測量半導體參數&#xff0c;尤其是MOS CAP和MOSFET結構。MOS(金屬-氧化物-半導體)結構的電容是外加電壓的函數&#xff0c;MOS電容隨外加電壓變化的曲線稱之為C-V曲線&#xff08;簡稱C-V特性&#xff09;&#xff0c;C-V 曲線測試可以方便的確…

opencv-使用 Haar 分類器進行面部檢測

Haar 分類器是一種用于對象檢測的方法&#xff0c;最常見的應用之一是面部檢測。Haar 分類器基于Haar-like 特征&#xff0c;這些特征可以通過計算圖像中的積分圖來高效地計算。 在OpenCV中&#xff0c;Haar 分類器被廣泛用于面部檢測。以下是一個簡單的使用OpenCV進行面部檢測…