主頁 > 後端開發 > Go語言基礎之反射

Go語言基礎之反射

2020-09-15 05:52:32 後端開發

本文介紹了Go語言反射的意義和基本使用,

變數的內在機制

Go語言中的變數是分為兩部分的:

  • 型別資訊:預先定義好的元資訊,
  • 值資訊:程式運行程序中可動態變化的,

反射介紹

反射是指在程式運行期對程式本身進行訪問和修改的能力,程式在編譯時,變數被轉換為記憶體地址,變數名不會被編譯器寫入到可執行部分,在運行程式時,程式無法獲取自身的資訊,

支持反射的語言可以在程式編譯期將變數的反射資訊,如欄位名稱、型別資訊、結構體資訊等整合到可執行檔案中,并給程式提供介面訪問反射資訊,這樣就可以在程式運行期獲取型別的反射資訊,并且有能力修改它們,

Go程式在運行期使用reflect包訪問程式的反射資訊,

在上一篇博客中我們介紹了空介面, 空介面可以存盤任意型別的變數,那我們如何知道這個空介面保存的資料是什么呢? 反射就是在運行時動態的獲取一個變數的型別資訊和值資訊,

reflect包

在Go語言的反射機制中,任何介面值都由是一個具體型別具體型別的值兩部分組成的(我們在上一篇介面的博客中有介紹相關概念), 在Go語言中反射的相關功能由內置的reflect包提供,任意介面值在反射中都可以理解為由reflect.Typereflect.Value兩部分組成,并且reflect包提供了reflect.TypeOfreflect.ValueOf兩個函式來獲取任意物件的Value和Type,

TypeOf

在Go語言中,使用reflect.TypeOf()函式可以獲得任意值的型別物件(reflect.Type),程式通過型別物件可以訪問任意值的型別資訊,

package main

import (
	"fmt"
	"reflect"
)

func reflectType(x interface{}) {
	v := reflect.TypeOf(x)
	fmt.Printf("type:%v\n", v)
}
func main() {
	var a float32 = 3.14
	reflectType(a) // type:float32
	var b int64 = 100
	reflectType(b) // type:int64
}

type name和type kind

在反射中關于型別還劃分為兩種:型別(Type)種類(Kind),因為在Go語言中我們可以使用type關鍵字構造很多自定義型別,而種類(Kind)就是指底層的型別,但在反射中,當需要區分指標、結構體等大品種的型別時,就會用到種類(Kind), 舉個例子,我們定義了兩個指標型別和兩個結構體型別,通過反射查看它們的型別和種類,

package main

import (
	"fmt"
	"reflect"
)

type myInt int64

func reflectType(x interface{}) {
	t := reflect.TypeOf(x)
	fmt.Printf("type:%v kind:%v\n", t.Name(), t.Kind())
}

func main() {
	var a *float32 // 指標
	var b myInt    // 自定義型別
	var c rune     // 型別別名
	reflectType(a) // type: kind:ptr
	reflectType(b) // type:myInt kind:int64
	reflectType(c) // type:int32 kind:int32

	type person struct {
		name string
		age  int
	}
	type book struct{ title string }
	var d = person{
		name: "沙河小王子",
		age:  18,
	}
	var e = book{title: "《跟小王子學Go語言》"}
	reflectType(d) // type:person kind:struct
	reflectType(e) // type:book kind:struct
}

Go語言的反射中像陣列、切片、Map、指標等型別的變數,它們的.Name()都是回傳

reflect包中定義的Kind型別如下:

type Kind uint
const (
    Invalid Kind = iota  // 非法型別
    Bool                 // 布爾型
    Int                  // 有符號整型
    Int8                 // 有符號8位整型
    Int16                // 有符號16位整型
    Int32                // 有符號32位整型
    Int64                // 有符號64位整型
    Uint                 // 無符號整型
    Uint8                // 無符號8位整型
    Uint16               // 無符號16位整型
    Uint32               // 無符號32位整型
    Uint64               // 無符號64位整型
    Uintptr              // 指標
    Float32              // 單精度浮點數
    Float64              // 雙精度浮點數
    Complex64            // 64位復數型別
    Complex128           // 128位復數型別
    Array                // 陣列
    Chan                 // 通道
    Func                 // 函式
    Interface            // 介面
    Map                  // 映射
    Ptr                  // 指標
    Slice                // 切片
    String               // 字串
    Struct               // 結構體
    UnsafePointer        // 底層指標
)

ValueOf

reflect.ValueOf()回傳的是reflect.Value型別,其中包含了原始值的值資訊,reflect.Value與原始值之間可以互相轉換,

reflect.Value型別提供的獲取原始值的方法如下:

方法說明
Interface() interface {} 將值以 interface{} 型別回傳,可以通過型別斷言轉換為指定型別
Int() int64 將值以 int 型別回傳,所有有符號整型均可以此方式回傳
Uint() uint64 將值以 uint 型別回傳,所有無符號整型均可以此方式回傳
Float() float64 將值以雙精度(float64)型別回傳,所有浮點數(float32、float64)均可以此方式回傳
Bool() bool 將值以 bool 型別回傳
Bytes() []bytes 將值以位元組陣列 []bytes 型別回傳
String() string 將值以字串型別回傳

通過反射獲取值

func reflectValue(x interface{}) {
	v := reflect.ValueOf(x)
	k := v.Kind()
	switch k {
	case reflect.Int64:
		// v.Int()從反射中獲取整型的原始值,然后通過int64()強制型別轉換
		fmt.Printf("type is int64, value is %d\n", int64(v.Int()))
	case reflect.Float32:
		// v.Float()從反射中獲取浮點型的原始值,然后通過float32()強制型別轉換
		fmt.Printf("type is float32, value is %f\n", float32(v.Float()))
	case reflect.Float64:
		// v.Float()從反射中獲取浮點型的原始值,然后通過float64()強制型別轉換
		fmt.Printf("type is float64, value is %f\n", float64(v.Float()))
	}
}
func main() {
	var a float32 = 3.14
	var b int64 = 100
	reflectValue(a) // type is float32, value is 3.140000
	reflectValue(b) // type is int64, value is 100
	// 將int型別的原始值轉換為reflect.Value型別
	c := reflect.ValueOf(10)
	fmt.Printf("type c :%T\n", c) // type c :reflect.Value
}

通過反射設定變數的值

想要在函式中通過反射修改變數的值,需要注意函式引數傳遞的是值拷貝,必須傳遞變數地址才能修改變數值,而反射中使用專有的Elem()方法來獲取指標對應的值,

package main

import (
	"fmt"
	"reflect"
)

func reflectSetValue1(x interface{}) {
	v := reflect.ValueOf(x)
	if v.Kind() == reflect.Int64 {
		v.SetInt(200) //修改的是副本,reflect包會引發panic
	}
}
func reflectSetValue2(x interface{}) {
	v := reflect.ValueOf(x)
	// 反射中使用 Elem()方法獲取指標對應的值
	if v.Elem().Kind() == reflect.Int64 {
		v.Elem().SetInt(200)
	}
}
func main() {
	var a int64 = 100
	// reflectSetValue1(a) //panic: reflect: reflect.Value.SetInt using unaddressable value
	reflectSetValue2(&a)
	fmt.Println(a)
}

isNil()和isValid()

isNil()

func (v Value) IsNil() bool

IsNil()報告v持有的值是否為nil,v持有的值的分類必須是通道、函式、介面、映射、指標、切片之一;否則IsNil函式會導致panic,

isValid()

func (v Value) IsValid() bool

IsValid()回傳v是否持有一個值,如果v是Value零值會回傳假,此時v除了IsValid、String、Kind之外的方法都會導致panic,

舉個例子

IsNil()常被用于判斷指標是否為空;IsValid()常被用于判定回傳值是否有效,

func main() {
	// *int型別空指標
	var a *int
	fmt.Println("var a *int IsNil:", reflect.ValueOf(a).IsNil())
	// nil值
	fmt.Println("nil IsValid:", reflect.ValueOf(nil).IsValid())
	// 實體化一個匿名結構體
	b := struct{}{}
	// 嘗試從結構體中查找"abc"欄位
	fmt.Println("不存在的結構體成員:", reflect.ValueOf(b).FieldByName("abc").IsValid())
	// 嘗試從結構體中查找"abc"方法
	fmt.Println("不存在的結構體方法:", reflect.ValueOf(b).MethodByName("abc").IsValid())
	// map
	c := map[string]int{}
	// 嘗試從map中查找一個不存在的鍵
	fmt.Println("map中不存在的鍵:", reflect.ValueOf(c).MapIndex(reflect.ValueOf("娜扎")).IsValid())
}

結構體反射

與結構體相關的方法

任意值通過reflect.TypeOf()獲得反射物件資訊后,如果它的型別是結構體,可以通過反射值物件(reflect.Type)的NumField()Field()方法獲得結構體成員的詳細資訊,

reflect.Type中與獲取結構體成員相關的的方法如下表所示,

方法說明
Field(i int) StructField 根據索引,回傳索引對應的結構體欄位的資訊,
NumField() int 回傳結構體成員欄位數量,
FieldByName(name string) (StructField, bool) 根據給定字串回傳字串對應的結構體欄位的資訊,
FieldByIndex(index []int) StructField 多層成員訪問時,根據 []int 提供的每個結構體的欄位索引,回傳欄位的資訊,
FieldByNameFunc(match func(string) bool) (StructField,bool) 根據傳入的匹配函式匹配需要的欄位,
NumMethod() int 回傳該型別的方法集中方法的數目
Method(int) Method 回傳該型別方法集中的第i個方法
MethodByName(string)(Method, bool) 根據方法名回傳該型別方法集中的方法

StructField型別

StructField型別用來描述結構體中的一個欄位的資訊,

StructField的定義如下:

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

結構體反射示例

當我們使用反射得到一個結構體資料之后可以通過索引依次獲取其欄位資訊,也可以通過欄位名去獲取指定的欄位資訊,

type student struct {
	Name  string `json:"name"`
	Score int    `json:"score"`
}

func main() {
	stu1 := student{
		Name:  "小王子",
		Score: 90,
	}

	t := reflect.TypeOf(stu1)
	fmt.Println(t.Name(), t.Kind()) // student struct
	// 通過for回圈遍歷結構體的所有欄位資訊
	for i := 0; i < t.NumField(); i++ {
		field := t.Field(i)
		fmt.Printf("name:%s index:%d type:%v json tag:%v\n", field.Name, field.Index, field.Type, field.Tag.Get("json"))
	}

	// 通過欄位名獲取指定結構體欄位資訊
	if scoreField, ok := t.FieldByName("Score"); ok {
		fmt.Printf("name:%s index:%d type:%v json tag:%v\n", scoreField.Name, scoreField.Index, scoreField.Type, scoreField.Tag.Get("json"))
	}
}

接下來撰寫一個函式printMethod(s interface{})來遍歷列印s包含的方法,

// 給student添加兩個方法 Study和Sleep(注意首字母大寫)
func (s student) Study() string {
	msg := "好好學習,天天向上,"
	fmt.Println(msg)
	return msg
}

func (s student) Sleep() string {
	msg := "好好睡覺,快快長大,"
	fmt.Println(msg)
	return msg
}

func printMethod(x interface{}) {
	t := reflect.TypeOf(x)
	v := reflect.ValueOf(x)

	fmt.Println(t.NumMethod())
	for i := 0; i < v.NumMethod(); i++ {
		methodType := v.Method(i).Type()
		fmt.Printf("method name:%s\n", t.Method(i).Name)
		fmt.Printf("method:%s\n", methodType)
		// 通過反射呼叫方法傳遞的引數必須是 []reflect.Value 型別
		var args = []reflect.Value{}
		v.Method(i).Call(args)
	}
}

反射是把雙刃劍

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

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

練習題

  1. 撰寫代碼利用反射實作一個ini檔案的決議器程式,

轉載請註明出處,本文鏈接:https://www.uj5u.com/houduan/43077.html

標籤:Go

上一篇:Go語言基礎之并發

下一篇:Go語言基礎之介面

標籤雲
其他(157675) Python(38076) JavaScript(25376) Java(17977) C(15215) 區塊鏈(8255) C#(7972) AI(7469) 爪哇(7425) MySQL(7132) html(6777) 基礎類(6313) sql(6102) 熊猫(6058) PHP(5869) 数组(5741) R(5409) Linux(5327) 反应(5209) 腳本語言(PerlPython)(5129) 非技術區(4971) Android(4554) 数据框(4311) css(4259) 节点.js(4032) C語言(3288) json(3245) 列表(3129) 扑(3119) C++語言(3117) 安卓(2998) 打字稿(2995) VBA(2789) Java相關(2746) 疑難問題(2699) 细绳(2522) 單片機工控(2479) iOS(2429) ASP.NET(2402) MongoDB(2323) 麻木的(2285) 正则表达式(2254) 字典(2211) 循环(2198) 迅速(2185) 擅长(2169) 镖(2155) 功能(1967) .NET技术(1958) Web開發(1951) python-3.x(1918) HtmlCss(1915) 弹簧靴(1913) C++(1909) xml(1889) PostgreSQL(1872) .NETCore(1853) 谷歌表格(1846) Unity3D(1843) for循环(1842)

熱門瀏覽
  • 【C++】Microsoft C++、C 和匯編程式檔案

    ......

    uj5u.com 2020-09-10 00:57:23 more
  • 例外宣告

    相比于斷言適用于排除邏輯上不可能存在的狀態,例外通常是用于邏輯上可能發生的錯誤。 例外宣告 Item 1:當函式不可能拋出例外或不能接受拋出例外時,使用noexcept 理由 如果不打算拋出例外的話,程式就會認為無法處理這種錯誤,并且應當盡早終止,如此可以有效地阻止例外的傳播與擴散。 示例 //不可 ......

    uj5u.com 2020-09-10 00:57:27 more
  • Codeforces 1400E Clear the Multiset(貪心 + 分治)

    鏈接:https://codeforces.com/problemset/problem/1400/E 來源:Codeforces 思路:給你一個陣列,現在你可以進行兩種操作,操作1:將一段沒有 0 的區間進行減一的操作,操作2:將 i 位置上的元素歸零。最終問:將這個陣列的全部元素歸零后操作的最少 ......

    uj5u.com 2020-09-10 00:57:30 more
  • UVA11610 【Reverse Prime】

    本人看到此題沒有翻譯,就附帶了一個自己的翻譯版本 思考 這一題,它的第一個要求是找出所有 $7$ 位反向質數及其質因數的個數。 我們應該需要質數篩篩選1~$10^{7}$的所有數,這里就不慢慢介紹了。但是,重讀題,我們突然發現反向質數都是 $7$ 位,而將它反過來后的數字卻是 $6$ 位數,這就說明 ......

    uj5u.com 2020-09-10 00:57:36 more
  • 統計區間素數數量

    1 #pragma GCC optimize(2) 2 #include <bits/stdc++.h> 3 using namespace std; 4 bool isprime[1000000010]; 5 vector<int> prime; 6 inline int getlist(int ......

    uj5u.com 2020-09-10 00:57:47 more
  • C/C++編程筆記:C++中的 const 變數詳解,教你正確認識const用法

    1、C中的const 1、區域const變數存放在堆疊區中,會分配記憶體(也就是說可以通過地址間接修改變數的值)。測驗代碼如下: 運行結果: 2、全域const變數存放在只讀資料段(不能通過地址修改,會發生寫入錯誤), 默認為外部聯編,可以給其他源檔案使用(需要用extern關鍵字修飾) 運行結果: ......

    uj5u.com 2020-09-10 00:58:04 more
  • 【C++犯錯記錄】VS2019 MFC添加資源不懂如何修改資源宏ID

    1. 首先在資源視圖中,添加資源 2. 點擊新添加的資源,復制自動生成的ID 3. 在解決方案資源管理器中找到Resource.h檔案,編輯,使用整個專案搜索和替換的方式快速替換 宏宣告 4. Ctrl+Shift+F 全域搜索,點擊查找全部,然后逐個替換 5. 為什么使用搜索替換而不使用屬性視窗直 ......

    uj5u.com 2020-09-10 00:59:11 more
  • 【C++犯錯記錄】VS2019 MFC不懂的批量添加資源

    1. 打開資源頭檔案Resource.h,在其中預先定義好宏 ID(不清楚其實ID值應該設定多少,可以先新建一個相同的資源項,再在這個資源的ID值的基礎上遞增即可) 2. 在資源視圖中選中專案資源,按F7編輯資源檔案,按 ID 型別 相對路徑的形式添加 資源。(別忘了先把檔案拷貝到專案中的res檔案 ......

    uj5u.com 2020-09-10 01:00:19 more
  • C/C++編程筆記:關于C++的參考型別,專供新手入門使用

    今天要講的是C++中我最喜歡的一個用法——參考,也叫別名。 參考就是給一個變數名取一個變數名,方便我們間接地使用這個變數。我們可以給一個變數創建N個參考,這N + 1個變數共享了同一塊記憶體區域。(參考型別的變數會占用記憶體空間,占用的記憶體空間的大小和指標型別的大小是相同的。雖然參考是一個物件的別名,但 ......

    uj5u.com 2020-09-10 01:00:22 more
  • 【C/C++編程筆記】從頭開始學習C ++:初學者完整指南

    眾所周知,C ++的學習曲線陡峭,但是花時間學習這種語言將為您的職業帶來奇跡,并使您與其他開發人員區分開。您會更輕松地學習新語言,形成真正的解決問題的技能,并在編程的基礎上打下堅實的基礎。 C ++將幫助您養成良好的編程習慣(即清晰一致的編碼風格,在撰寫代碼時注釋代碼,并限制類內部的可見性),并且由 ......

    uj5u.com 2020-09-10 01:00:41 more
最新发布
  • Rust中的智能指標:Box<T> Rc<T> Arc<T> Cell<T> RefCell<T> Weak

    Rust中的智能指標是什么 智能指標(smart pointers)是一類資料結構,是擁有資料所有權和額外功能的指標。是指標的進一步發展 指標(pointer)是一個包含記憶體地址的變數的通用概念。這個地址參考,或 ” 指向”(points at)一些其 他資料 。參考以 & 符號為標志并借用了他們所 ......

    uj5u.com 2023-04-20 07:24:10 more
  • Java的值傳遞和參考傳遞

    值傳遞不會改變本身,參考傳遞(如果傳遞的值需要實體化到堆里)如果發生修改了會改變本身。 1.基本資料型別都是值傳遞 package com.example.basic; public class Test { public static void main(String[] args) { int ......

    uj5u.com 2023-04-20 07:24:04 more
  • [2]SpinalHDL教程——Scala簡單入門

    第一個 Scala 程式 shell里面輸入 $ scala scala> 1 + 1 res0: Int = 2 scala> println("Hello World!") Hello World! 檔案形式 object HelloWorld { /* 這是我的第一個 Scala 程式 * 以 ......

    uj5u.com 2023-04-20 07:23:58 more
  • 理解函式指標和回呼函式

    理解 函式指標 指向函式的指標。比如: 理解函式指標的偽代碼 void (*p)(int type, char *data); // 定義一個函式指標p void func(int type, char *data); // 宣告一個函式func p = func; // 將指標p指向函式func ......

    uj5u.com 2023-04-20 07:23:52 more
  • Django筆記二十五之資料庫函式之日期函式

    本文首發于公眾號:Hunter后端 原文鏈接:Django筆記二十五之資料庫函式之日期函式 日期函式主要介紹兩個大類,Extract() 和 Trunc() Extract() 函式作用是提取日期,比如我們可以提取一個日期欄位的年份,月份,日等資料 Trunc() 的作用則是截取,比如 2022-0 ......

    uj5u.com 2023-04-20 07:23:45 more
  • 一天吃透JVM面試八股文

    什么是JVM? JVM,全稱Java Virtual Machine(Java虛擬機),是通過在實際的計算機上仿真模擬各種計算機功能來實作的。由一套位元組碼指令集、一組暫存器、一個堆疊、一個垃圾回收堆和一個存盤方法域等組成。JVM屏蔽了與作業系統平臺相關的資訊,使得Java程式只需要生成在Java虛擬機 ......

    uj5u.com 2023-04-20 07:23:31 more
  • 使用Java接入小程式訂閱訊息!

    更新完微信服務號的模板訊息之后,我又趕緊把微信小程式的訂閱訊息給實作了!之前我一直以為微信小程式也是要企業才能申請,沒想到小程式個人就能申請。 訊息推送平臺🔥推送下發【郵件】【短信】【微信服務號】【微信小程式】【企業微信】【釘釘】等訊息型別。 https://gitee.com/zhongfuch ......

    uj5u.com 2023-04-20 07:22:59 more
  • java -- 緩沖流、轉換流、序列化流

    緩沖流 緩沖流, 也叫高效流, 按照資料型別分類: 位元組緩沖流:BufferedInputStream,BufferedOutputStream 字符緩沖流:BufferedReader,BufferedWriter 緩沖流的基本原理,是在創建流物件時,會創建一個內置的默認大小的緩沖區陣列,通過緩沖 ......

    uj5u.com 2023-04-20 07:22:49 more
  • Java-SpringBoot-Range請求頭設定實作視頻分段傳輸

    老實說,人太懶了,現在基本都不喜歡寫筆記了,但是網上有關Range請求頭的文章都太水了 下面是抄的一段StackOverflow的代碼...自己大修改過的,寫的注釋挺全的,應該直接看得懂,就不解釋了 寫的不好...只是希望能給視頻網站開發的新手一點點幫助吧. 業務場景:視頻分段傳輸、視頻多段傳輸(理 ......

    uj5u.com 2023-04-20 07:22:42 more
  • Windows 10開發教程_編程入門自學教程_菜鳥教程-免費教程分享

    教程簡介 Windows 10開發入門教程 - 從簡單的步驟了解Windows 10開發,從基本到高級概念,包括簡介,UWP,第一個應用程式,商店,XAML控制元件,資料系結,XAML性能,自適應設計,自適應UI,自適應代碼,檔案管理,SQLite資料庫,應用程式到應用程式通信,應用程式本地化,應用程式 ......

    uj5u.com 2023-04-20 07:22:35 more