Go:反射

為什么使用反射

在編程中,有時需編寫函數統一處理多種值類型 ,這些類型可能無法共享同一接口、布局未知,甚至在設計函數時還不存在 。

func Sprint(x interface{}) string {type stringer interface {String() string}switch x := x.(type) {case stringer:return x.String()case string:return xcase int:return strconv.Itoa(x)//...對 int16、uint32 等類型做類似的處理case bool:if x {return "true"}return "false"default:// array、chan、func、map、pointer、slice、structreturn "???"}
}

以實現類似fmt.SprintfSprint函數為例,該函數接收一個參數并返回字符串 。初步實現思路是:

  • 首先判斷參數是否實現了String方法,若實現則直接調用 。
  • 然后通過switch語句判斷參數動態類型是否為基本類型(如stringintbool等 ),針對不同基本類型進行格式化操作 。如string類型直接返回原值;int類型通過strconv.Itoa轉換為字符串;bool類型根據值返回"true""false"
  • 對于默認情況(如arraychanfuncmappointerslicestruct等類型 ),簡單返回"???"

但對于更復雜類型(如[]float64map[string][]string )以及自定義類型(如url.Values ),僅靠上述分支處理會面臨問題 。因為類型數量無限,難以添加所有分支;且即使添加了處理某底層類型的分支,也無法處理具有該底層類型的自定義類型,還可能因引入自定義類型處理分支導致庫的循環引用 。當無法知曉未知類型的布局時,這種基于類型分支的代碼就難以繼續編寫,此時就需要借助反射機制來解決。

reflect.Type 和 reflect.Value

reflect.Type

  • 功能與定義reflect包提供反射功能,reflect.Type表示 Go 語言的一個類型,是有多種方法的接口,可識別類型、透視類型組成部分(如結構體字段、函數參數 ) 。reflect.TypeOf函數接收interface{}參數,返回接口中動態類型的reflect.Type形式 。
// reflect.Type相關示例
t := reflect.TypeOf(3) 
fmt.Println(t.String()) 
fmt.Println(t) var w io.Writer = os.Stdout
fmt.Println(reflect.TypeOf(w)) fmt.Printf("%T\n", 3) 
  • 示例:如reflect.TypeOf(3)返回表示int類型的reflect.Typefmt.Printf("%T\n", 3)內部實現就使用了reflect.TypeOf 。當變量實現接口類型轉換時,reflect.TypeOf返回具體類型而非接口類型,如var w io.Writer = os.Stdoutreflect.TypeOf(w)返回*os.File

reflect.Value

  • 功能與定義reflect.Value可包含任意類型的值 。reflect.ValueOf函數接收interface{}參數,將接口動態值以reflect.Value形式返回 。
// reflect.Value相關示例
v := reflect.ValueOf(3) 
fmt.Println(v) 
fmt.Printf("%v\n", v) 
fmt.Println(v.String()) t := v.Type() 
fmt.Println(t.String()) v := reflect.ValueOf(3) 
x := v.Interface()
i := x.(int)
fmt.Printf("%d\n", i) 
  • 示例reflect.ValueOf(3)返回包含值3reflect.Valuereflect.Value滿足fmt.Stringer ,但非字符串時String方法僅暴露類型 ,常用fmt%v功能處理 。reflect.ValueType方法可返回其類型(reflect.Type形式 ),reflect.Value.Interface方法是reflect.ValueOf的逆操作,返回含相同具體值的interface{}

示例

// 格式化函數示例
package formatimport ("reflect""strconv"
)func Any(value interface{}) string {return formatAtom(reflect.ValueOf(value))
}func formatAtom(v reflect.Value) string {switch v.Kind() {case reflect.Invalid:return "invalid"case reflect.Int, reflect.Int8, reflect.Int16,reflect.Int32, reflect.Int64:return strconv.FormatInt(v.Int(), 10)case reflect.Uint, reflect.Uint8, reflect.Uint16,reflect.Uint32, reflect.Uint64, reflect.Uintptr:return strconv.FormatUint(v.Uint(), 10)//...為簡化起見,省略了浮點數和復數的分支...case reflect.Bool:return strconv.FormatBool(v.Bool())case reflect.String:return strconv.Quote(v.String())case reflect.Chan, reflect.Func, reflect.Ptr, reflect.Slice, reflect.Map:return v.Type().String() + " 0x" +strconv.FormatUint(uint64(v.Pointer()), 16)default: // reflect.Array, reflect.Struct, reflect.Interfacereturn v.Type().String() + " value"}
}

利用reflect.ValueKind方法區分類型,編寫通用格式化函數format.Anyformat.Any調用formatAtomformatAtom通過switch v.Kind()判斷類型并格式化 ,涵蓋基礎類型(BoolString 、各種數字類型 )、聚合類型(ArrayStruct )、引用類型(ChanFuncPtrSliceMap )、接口類型(Interface )及Invalid類型 。當前版本把值當作不可分割物體處理,對聚合類型和接口僅輸出類型,對引用類型輸出類型和引用地址,雖不夠理想但有進步,且對命名類型效果較好 。

Display:一個遞歸的值顯示器

Display是調試工具函數,接收任意復雜值x ,輸出其完整結構及元素路徑 。為避免在包 API 中暴露反射相關內容,定義未導出的display函數做遞歸處理,Display僅為簡單封裝 。Display函數接收interface{}參數,內部調用displaydisplay使用之前定義的formatAtom函數輸出基礎值,并通過reflect.Value的方法遞歸展示復雜類型組成部分 。

處理邏輯

// 主函數,用于封裝和暴露功能
func Display(name string, x interface{}) {fmt.Printf("Display %s (%T):\n", name, x)display(name, reflect.ValueOf(x))
}
// 實際遞歸處理的函數
func display(path string, v reflect.Value) {switch v.Kind() {case reflect.Invalid:fmt.Printf("%s = invalid\n", path)case reflect.Array:for i := 0; i < v.Len(); i++ {display(fmt.Sprintf("%s[%d]", path, i), v.Index(i))}case reflect.Struct:for i := 0; i < v.NumField(); i++ {fieldPath := fmt.Sprintf("%s.%s", path, v.Type().Field(i).Name)display(fieldPath, v.Field(i))}case reflect.Map:for _, key := range v.MapKeys() {display(fmt.Sprintf("%s[%s]", path, formatAtom(key)), v.MapIndex(key))}case reflect.Ptr:if v.IsNil() {fmt.Printf("%s = nil\n", path)} else {display(fmt.Sprintf("(*%s)", path), v.Elem())}case reflect.Interface:if v.IsNil() {fmt.Printf("%s = nil\n", path)} else {fmt.Printf("%s.type = %s\n", path, v.Elem().Type())display(path+".value", v.Elem())}default: // 基本類型、通道、函數fmt.Printf("%s = %s\n", path, formatAtom(v))}
}
  • Invalid類型:若reflect.ValueInvalid類型,輸出%s = invalid
  • 數組與切片Len方法獲取元素個數,通過Index(i)方法按索引遍歷,遞歸調用display ,在路徑后加上[i]
  • 結構體NumField()方法獲取字段數,借助reflect.TypeField(i)獲取字段名,v.Field(i)獲取字段值,遞歸調用display ,路徑加上類似.f的字段選擇標記 。
  • 映射(mapMapKeys方法返回鍵的reflect.Value切片,MapIndex(key)獲取鍵對應的值,遞歸調用display ,路徑追加[key]
  • 指針Elem方法返回指針指向變量,IsNil判斷指針是否為空,為空輸出%s = nil ,非空則遞歸調用display ,路徑加*和圓括號 。
  • 接口IsNil判斷接口是否為空,非空通過v.Elem()獲取動態值,遞歸輸出類型和值 。
  • 其他(基礎類型、通道、函數 ):使用formatAtom格式化輸出 。

使用 reflect.Value 來設置值

Go 語言中,xx.f[i]*p等表達式表示變量,可尋址存儲區域包含值且可更新 ;x+1f(2)等不表示變量 。reflect.Value也有可尋址之分 ,通過示例x := 2等變量聲明,說明reflect.ValueOf返回的一些值不可尋址(如abc ),但可通過指針間接獲取可尋址的reflect.Value(如d := c.Elem() ) 。可使用CanAddr方法詢問reflect.Value是否可尋址 。

獲取可尋址變量

// 通過指針間接獲取可尋址的reflect.Value并更新值
x = 2
d = reflect.ValueOf(&x).Elem()
px := d.Addr().Interface().(*int)
*px = 3
fmt.Println(x) // 3

獲取可尋址的reflect.Value分三步:

  1. 調用Addr()返回含指向變量指針的Value
  2. 在該Value上調用Interface()返回含指針的interface{}值 。
  3. 使用類型斷言將接口內容轉換為普通指針,進而更新變量 。

更新變量的方式

// 直接通過可尋址的reflect.Value更新值
d.Set(reflect.ValueOf(4))
fmt.Println(x) // 4
  • 可直接通過可尋址的reflect.Value調用Set方法更新變量 ,運行時Set方法檢查可賦值性,如變量類型為int ,值類型不匹配會崩潰 。
// 基本類型特化的Set變種使用示例
d = reflect.ValueOf(&x).Elem()
d.SetInt(3)
fmt.Println(x) // 3
  • 有針對基本類型的Set變種方法,如SetIntSetUintSetStringSetFloat等 ,有一定容錯性,但在指向interface{}變量的reflect.Value上調用SetInt會崩潰 。

反射可讀取未導出結構字段值(如os.Filefd字段 ),但不能更新 。可尋址的reflect.Value記錄是否通過遍歷未導出字段獲得,修改其值前用CanAddr檢查不一定準確,需用CanSet方法正確報告reflect.Value是否可尋址且可更改 。

顯示類型的方法

package mainimport ("fmt""reflect""strings""time"
)// Print 輸出值 x 的所有方法
func Print(x interface{}) {v := reflect.ValueOf(x)t := v.Type()fmt.Printf("type %s\n", t)for i := 0; i < v.NumMethod(); i++ {methType := v.Method(i).Type()fmt.Printf("func (%s) %s%s\n", t, t.Method(i).Name,strings.TrimPrefix(methType.String(), "func"))}
}

Print函數接收interface{}參數,通過reflect.ValueOf獲取值的reflect.Value ,再用v.Type()獲取reflect.Type 。先打印值的類型,然后遍歷類型的方法 。通過v.NumMethod()獲取方法數量,v.Method(i).Type()獲取方法類型 。reflect.Typereflect.Value都有Method方法 ,從reflect.Type調用Method返回reflect.Method實例,描述方法名稱和類型;從reflect.Value調用Method返回reflect.Value ,代表綁定接收者的方法 。最后按格式輸出方法簽名 。

注意事項

脆弱性

反射功能強大,但基于反射的代碼很脆弱 。編譯器能在編譯時報告類型錯誤,而反射錯誤在運行時才以崩潰方式呈現,可能在代碼編寫很久后才暴露 。

代碼理解難度

類型本身可作為一種文檔,反射操作無法進行靜態類型檢查 ,大量使用反射的代碼難以理解 。對于接收interface{}reflect.Value的函數,需明確期望的參數類型和限制條件 。

性能問題

基于反射的函數比針對特定類型優化的函數慢一兩個數量級 。在程序中,非關鍵路徑函數為代碼清晰可用反射,測試因使用小數據集也適合反射;但關鍵路徑上的函數應避免使用反射,以保證性能 。

參考資料:《Go程序設計語言》

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

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

相關文章

SS25001-多路復用開關板

1 概述 1.1 簡介 多路復用開關板是使用信號繼電器實現2線制的多路復用開關板卡&#xff1b;多路復用開關是一種可以將一個輸入連接到多個輸出或一個輸出連接到多個輸入的拓撲結構。這種拓撲通常用于掃描&#xff0c;適合將一系列通道自動連接到公共線路的的設備。多路復用開…

vue3 nprogress 使用

nprogress 介紹與作用 1.nprogress 是一個輕量級的進度條組件&#xff0c;主要用于在頁面加載或路由切換時顯示一個進度條&#xff0c;提升用戶體驗。它的原理是通過在頁面頂部創建一個 div&#xff0c;并使用 fixed 定位來實現進度條的效果 2.在 Vite Vue 3 項目中&#xf…

Jsp技術入門指南【六】jsp腳本原理及隱式對象

Jsp技術入門指南【六】jsp腳本原理及隱式對象 前言一、JSP 腳本元素1.1 聲明1.2 表達式1.3 腳本標簽 二、JSP 的隱式對象是什么三、隱式對象詳解outrequestsessionapplicationconfigexception 前言 在之前的博客中&#xff0c;我們已經介紹了JSP的環境搭建、編譯文件查找以及生…

vue3推薦的移動table庫

vxe-table https://gitee.com/js-class/vxe-table#https://gitee.com/link?targethttps%3A%2F%2Fvxetable.cn 文檔api https://vxetable.cn/#/component/table/other/bookkeepingVoucher 引入步驟 安裝 npm install xe-utils vxe-tablenext 在項目main.js引入 import …

HOOPS Exchange 與HOOPS Communicator集成:打造工業3D可視化新標桿!

一、概述 在工業3D開發、BIM建筑、數字孿生和仿真分析等高端應用場景中&#xff0c;數據格式復雜、模型體量龐大、實時交互體驗要求高&#xff0c;一直是困擾開發者的難題。Tech Soft 3D旗下的HOOPS Exchange和HOOPS Communicator&#xff0c;正是解決這類問題的黃金搭檔。二者…

《軟件設計師》復習筆記(14.3)——設計模式

目錄 一、設計模式分類 1. 創建型模式&#xff08;Creational Patterns&#xff09; 2. 結構型模式&#xff08;Structural Patterns&#xff09; 3. 行為型模式&#xff08;Behavioral Patterns&#xff09; 真題示例&#xff1a; 一、設計模式分類 架構模式 高層設計決…

HarmonyOS:使用Refresh組件實現頁面下拉刷新上拉加載更多

一、前言 可以進行頁面下拉操作并顯示刷新動效的容器組件。 說明 該組件從API Version 8開始支持。后續版本如有新增內容&#xff0c;則采用上角標單獨標記該內容的起始版本。該組件從API Version 12開始支持與垂直滾動的Swiper和Web的聯動。當Swiper設置loop屬性為true時&…

55、?屏加載?屏怎么進?優化

答&#xff1a; &#xff08;1&#xff09;使?CDN 減?代碼體積&#xff0c;加快請求速度&#xff1b; (2)SSR通過服務端把所有數據全部渲染完成再返回給客?端&#xff1b; (3) 路由懶加載&#xff0c;當??訪問的時候&#xff0c;再加載相應模塊&#xff1b; (4) 使?外…

什么是Python單例模式

什么是Python單例模式 Python單例模式是一種創建型設計模式,目的是確保一個類僅有一個實例,并提供一個全局訪問點來獲取該實例。以下從作用和示例進行介紹: 作用 控制資源使用:避免對系統資源的重復消耗,像數據庫連接、文件句柄等稀缺資源,只創建一個實例來管理使用,防…

Java 2025:解鎖未來5大技術趨勢,Kotlin融合AI新篇

各位Java開發者們好&#xff01;&#x1f680; 2025年的Java世界正在經歷一場前所未有的技術變革。作為深耕Java領域多年的技術博主&#xff0c;今天我將帶大家深入探索Java生態即將迎來的5大技術趨勢&#xff0c;特別是Kotlin的深度融合和AI技術的新篇章。準備好了嗎&#xff…

計算機視覺cv2入門之車牌號碼識別

前邊我們已經講解了使用cv2進行圖像預處理與邊緣檢測等方面的知識&#xff0c;這里我們以車牌號碼識別這一案例來實操一下。 大致思路 車牌號碼識別的大致流程可以分為這三步&#xff1a;圖像預處理-尋找車牌輪廓-車牌OCR識別 接下來我們按照這三步來進行講解。 圖像預處理 …

CExercise_13_1排序算法_3快速排序算法,包括單向分區以及雙向分區

題目&#xff1a; 請手動實現快速排序算法&#xff0c;包括單向分區以及雙向分區&#xff1a; // 單向分區快速排序算法 void quick_sort_one_way(int arr[], int len); //雙向分區快速排序算法 void quick_sort_two_way(int arr[], int len); 關鍵點 分析&#xff1a; &#x…

FPGA-VGA

目錄 前言 一、VGA是什么&#xff1f; 二、物理接口 三、VGA顯示原理 四、VGA時序標準 五、VGA顯示參數 六、模塊設計 七、波形圖設計 八、彩條波形數據 前言 VGA的FPGA驅動 一、VGA是什么&#xff1f; VGA&#xff08;Video Graphics Array&#xff09;是IBM于1987年推出的…

Linux和Ubuntu的驅動適配情況

舊 一、Linux Yocto3.0 二、Ubuntu 1.驅動 1.rtc正常 2.led正常 3.加密芯片正常 4.硬件看門狗不行&#xff0c;驅動已經適配好&#xff0c;等硬件修復后&#xff0c;直接使用腳本就可以 5.千兆網口可以&#xff0c;兩個百兆網口不行 6.USB上面和下面都可以&#xff08;插u盤…

Python 文本和字節序列(處理文本文件)

本章將討論下述話題&#xff1a; 字符、碼位和字節表述 bytes、bytearray 和 memoryview 等二進制序列的獨特特性 全部 Unicode 和陳舊字符集的編解碼器 避免和處理編碼錯誤 處理文本文件的最佳實踐 默認編碼的陷阱和標準 I/O 的問題 規范化 Unicode 文本&#xff0c;進行安全的…

【Android學習記錄】工具使用

文章目錄 一. 精準找視圖資源ID1. 準備工作2. 使用 uiautomator 工具2.1. 獲取設備的窗口內容2.2. Pull XML 文件2.3. 查看 XML 文件 3. 直接使用 ADB 命令4. 使用 Android Studio 的 Layout Inspector總結 二. adb shell dumpsys activity1. 如何使用 ADB 命令2. 輸出內容解析…

Kafka系列之:計算kafka集群topic占的存儲大小

Kafka系列之:計算kafka集群topic占的存儲大小 topic存儲數據格式統計topic存儲大小定時統計topic存儲大小topic存儲數據格式 單位是字節大小 size_bytes{directory="/data/datum/kafka/optics-all" } 782336計算topic存儲大小腳本邏輯是: 計算指定目錄或文件的大小…

C# 高級編程:Lambda 表達式

在 C# 的高級編程中,Lambda 表達式是一個強大而靈活的工具,廣泛應用于 LINQ 查詢、委托、事件處理以及函數式編程等多個領域。它不僅使代碼更簡潔、表達更直接,而且在某些場景中能極大提高代碼的可讀性與可維護性。本文將從 Lambda 表達式的基本語法入手,深入探討其原理、常…

《軟件設計師》復習筆記(11.5)——測試原則、階段、測試用例設計、調試

目錄 1. 測試基礎概念 2. 測試方法分類 3. 測試階段 真題示例&#xff1a; 題目1 題目2 題目3 4. 測試策略 5. 測試用例設計 真題示例&#xff1a; 6. 調試與度量 真題示例&#xff1a; 1. 測試基礎概念 定義&#xff1a;系統測試是為發現錯誤而執行程序的過程&…

方案解讀:虛擬電廠標桿項目整體建設方案【附全文閱讀】

在電力市場背景下,傳統電力現貨市場存在電能定價不合理、分布式電源并網困難等問題。本虛擬電廠標桿項目旨在研究全時間尺度虛擬電廠智能管控關鍵技術,通過研制虛擬電廠控制器樣機、開發運行管理平臺,實現對分布式能源的合理優化配置。項目內容涵蓋虛擬調控、建設目標、建設…