反射和文件操作
- 1、反射
- 1.1、reflect.TypeOf()獲取任意值的類型對象
- 1.2、reflect.ValueOf()
- 1.3、結構體反射
- 2、文件操作
- 2.1、os.Open()打開文件
- 2.2、方式一:使用Read()讀取文件
- 2.3、方式二:bufio讀取文件
- 2.4、方式三:os.ReadFile讀取
- 2.5、寫入文件
- 2.6、復制文件
- 2.7、文件重命名
- 2.8、創建目錄
- 2.9、刪除目錄和文件
1、反射
有時我們需要寫一個函數,這個函數有能力統一處理各種值類型,而這些類型可能無法共享同一個接口, 也可能布局未知,也有可能這個類型在我們設計函數時還不存在,這個時候我們就可以用到反射。
空接口可以存儲任意類型的變量,那我們如何知道這個空接口保存數據的類型是什么?值是什么呢?
1、可以使用類型斷言
2、可以使用反射實現, 也就是在程序運行時動態的獲取一個變量的類型信息和值信息。
Golang 中反射可以實現以下功能:
1、反射可以在程序運行期間動態的獲取變量的各種信息,比如變量的類型、類別
2、如果是結構體,通過反射還可以獲取結構體本身的信息,比如結構體的字段、結構體的方法、結構體的tag。
3、通過反射,可以修改變量的值,可以調用關聯的方法。
Go 語言中的變量是分為兩部分的:
? 類型信息:預先定義好的元信息。
? 值信息:程序運行過程中可動態變化的。
在GoLang的反射機制中,任何接口值都由是一個具體類型和具體類型的值兩部分組成的。在GoLang中,反射的相關功能由內置的reflect包提供,任意接口值在反射中都可以理解為由reflect.Type和reflect.Value兩部分組成,并且reflect包提供了reflect.TypeOf和reflect.ValueOf兩個重要函數來獲取任意對象的Value和Type。
1.1、reflect.TypeOf()獲取任意值的類型對象
1、使用reflect.TypeOf可以獲取函數類型對象。
package mainimport ("fmt""reflect"
)type myInt inttype Person struct {Name stringAge int
}// 通過反射獲取任意變量的類型
func reflectFn(x interface{}) {v := reflect.TypeOf(x)fmt.Println(v)
}func main() {reflectFn(10)var a = 10reflectFn(3.1415)reflectFn(true)reflectFn("你好golang")reflectFn([]int{1, 2, 3, 4, 5})reflectFn(&a)p := Person{Name: "張三",Age: 18,}reflectFn(p)var x myInt = 10reflectFn(x)
}
2、通過reflect.TypeOf獲取返回的類型對象后,該對象里面還有兩個方法Name和Kind。Name用來獲取類型名稱,Kind用來獲取類型種類。
package mainimport ("fmt""reflect"
)type myInt inttype Person struct {Name stringAge int
}func reflectFn(x interface{}) {v := reflect.TypeOf(x)fmt.Printf("類型: %v, 類型名稱: %v, 類型種類: %v\n", v, v.Name(), v.Kind())
}func main() {var a = 10reflectFn(10)reflectFn(3.1415)reflectFn(true)reflectFn("你好golang")reflectFn([]int{1, 2, 3, 4, 5})reflectFn(&a)p := Person{Name: "張三",Age: 18,}reflectFn(p)var x myInt = 10reflectFn(x)
}
Go語言的反射中像數組、切片、Map、指針等類型的變量,它們的.Name()都是返回空。種類(Kind)就是指底層的類型。
1.2、reflect.ValueOf()
reflect.ValueOf()返回的是 reflect.Value 類型,其中包含了原始值的值信息。reflect.Value 與原始值之間可以互相轉換。
1、例如實現接受任意值的接口,提取出原始值進行運算操作。
package mainimport ("fmt""reflect"
)func reflectFn(x interface{}) {v := reflect.ValueOf(x)num := v.Int() + 10fmt.Println(num)
}func main() {reflectFn(10)
}
在上面的代碼中,我們先獲取reflec.Value對象,然后通過該對象獲取原始值進行運算操作。
2、但是上面這種情況是在我們知道類型的情況下,調用對應的獲取原始值方法, 如果有多種類型,我們就需要進行判斷。我們可以使用reflec.Value對象提供的kind函數獲取種類,然后根據種類進行判斷再調用對應的獲取原始值方法。
package mainimport ("fmt""reflect"
)func reflectFn(x interface{}) {v := reflect.ValueOf(x)kind := v.Kind()switch kind {case reflect.Int:fmt.Printf("int類型的原始值: %v\n", v.Int())case reflect.Bool:fmt.Printf("bool類型的原始值: %v\n", v.Bool())case reflect.Float64:fmt.Printf("float64類型的原始值: %v\n", v.Float())case reflect.String:fmt.Printf("string類型的原始值: %v\n", v.String())default:fmt.Println("還沒有判斷這個類型...")}
}func main() {reflectFn(10)reflectFn(true)reflectFn(3.1415)reflectFn("你好golang")
}
3、在反射中修改變量的值。
想要在函數中通過反射修改變量的值, 需要注意函數參數傳遞的是值拷貝, 必須傳遞變量地址才能修改變量值。而反射中使用專有的 Elem()方法來獲取指針對應的值。
package mainimport ("fmt""reflect"
)func reflectFn(x interface{}) {v := reflect.ValueOf(x)fmt.Println(v, v.Kind(), v.Elem(), v.Elem().Kind())
}func main() {var x = 10reflectFn(&x)
}
由于我們傳入的是一個指針,所以獲取值對象打印輸出就是一個地址,通過kind獲取類型是ptr,我們可以通過Elem函數來獲取該指針指向的值,而不能通過解引用的方式來直接獲取。要修改對應的值就先使用.Elem獲取對象,然后再調用SetXXX來修改。
package mainimport ("fmt""reflect"
)func reflectFn(x interface{}) {v := reflect.ValueOf(x)if v.Elem().Kind() == reflect.Int {v.Elem().SetInt(20)} else if v.Elem().Kind() == reflect.String {v.Elem().SetString("hello c++")}
}func main() {var x = 10var str = "hello golang"fmt.Println(x, str)reflectFn(&x)reflectFn(&str)fmt.Println(x, str)
}
1.3、結構體反射
任意值通過reflect.TypeOf()獲得反射對象信息后,如果它的類型是結構體,可以通過反射值對象(reflect.Type)的NumField()和Field()方法獲得結構體成員的詳細信息。
1、通過類型變量的Field方法獲取結構體字段。
package mainimport ("fmt""reflect"
)type Student struct {Name string `json:"name1" form:"uername"`Age int `json:"age"`Score int `json:"score"`
}func (s Student) GetInfo() {fmt.Printf("姓名: %v, 年齡: %v, 成績: %v\n", s.Name, s.Age, s.Score)
}func (s *Student) SetInfo(name string, age int, score int) {s.Name = names.Age = ages.Score = score
}func PrintStructField(s Student) {t := reflect.TypeOf(s)// v := reflect.ValueOf(s)if t.Kind() != reflect.Struct && t.Elem().Kind() != reflect.Struct {fmt.Println("傳入的不是一個結構體類型...")return}// 1.通過類型變量的Field方法獲取結構體字段field0 := t.Field(0)fmt.Printf("%#v\n", field0)
}func main() {stu := Student{Name: "張三",Age: 18,Score: 99,}PrintStructField(stu)
}
返回的是一個reflect.StructField對象,我們打印出看看里面有什么內容。
我們可以通過該結構體對象的Name、Type、Tag方法來獲取對應的信息:
field0 := t.Field(0)
fmt.Printf("%#v\n", field0)
fmt.Println("字段名稱:", field0.Name)
fmt.Println("字段類型:", field0.Type)
fmt.Println("字段Tag:", field0.Tag.Get("json"))
fmt.Println("字段Tag:", field0.Tag.Get("form"))
2、通過類型變量的FieldByName方法可以獲取結構體字段。
field1, _ := t.FieldByName("Age")
fmt.Println("字段名稱:", field1.Name)
fmt.Println("字段類型:", field1.Type)
fmt.Println("字段Tag:", field1.Tag.Get("json"))
3、通過類型變量的NumField方法可以獲取到該結構體里有幾個字段。
var fieldCount = t.NumField()
fmt.Println("該結構體字段數量:", fieldCount)
4、通過值變量獲取結構體里面對應的值。
v := reflect.ValueOf(s)
fmt.Printf("Name的值: %v, Age的值: %v\n", v.Field(0), v.FieldByName("Age"))
5、通過類型變量里面的Method方法獲取結構體中的方法。
func PrintStructFn(x interface{}) {t := reflect.TypeOf(x)if t.Kind() != reflect.Struct && t.Elem().Kind() != reflect.Struct {fmt.Println("傳入的參數不是一個結構體...")return}method0 := t.Method(0)fmt.Println("方法名稱:", method0.Name)fmt.Println("方法類型:", method0.Type)
}
這里傳入0獲取方法并不是按照方法的定義順序來獲取的,而是通過字典序來獲取的。
6、通過類型變量里面的MethodByName獲取方法。
method1, _ := t.MethodByName("GetInfo")
fmt.Println("方法名稱:", method1.Name)
fmt.Println("方法類型:", method1.Type)
也可以通過NumMethod獲取該結構體中有多少個方法。
7、通過值變量來執行方法。
v := reflect.ValueOf(x)
v.MethodByName("GetInfo").Call(nil)
上面調用GetInfo方法不需要傳參,Call里面填寫nil即可,如果需要傳參需要定義一個reflect.Value切片傳入,如下:
fmt.Println(x)
var params []reflect.Value
params = append(params, reflect.ValueOf("李四"))
params = append(params, reflect.ValueOf(22))
params = append(params, reflect.ValueOf(100))
v.MethodByName("SetInfo").Call(params)
fmt.Println(x)
8、反射修改結構體屬性。
package mainimport ("fmt""reflect"
)type Student struct {Name string `json:"name"`Age int `json:"age"`Score int `json:"score"`
}func reflectChange(x interface{}) {t := reflect.TypeOf(x)v := reflect.ValueOf(x)if t.Kind() != reflect.Ptr {fmt.Println("傳入的不是指針類型...")}if t.Elem().Kind() != reflect.Struct {fmt.Println("傳入的不是結構體指針類型...")}name := v.Elem().FieldByName("Name")name.SetString("李四")age := v.Elem().FieldByName("Age")age.SetInt(22)
}func main() {stu := Student{Name: "張三",Age: 18,Score: 100,}fmt.Println(stu)reflectChange(&stu)fmt.Println(stu)
}
2、文件操作
2.1、os.Open()打開文件
打開文件使用os.Open函數,傳入文件的地址,可以采用絕對地址也可以采用相對地址。記得調用Close函數關閉文件。
package mainimport ("fmt""os"
)func main() {file, err := os.Open("./main.go")defer file.Close()if err != nil {fmt.Println("err:", err)return}
}
這種打開方式默認是只讀的。
2.2、方式一:使用Read()讀取文件
調用Read函數需要傳入一個byte切片類型用來存儲數據。該函數有兩個返回值,第一個返回值表示讀取的字節數。
package mainimport ("fmt""io""os"
)func main() {file, err := os.Open("./main.go")defer file.Close()if err != nil {fmt.Println("err:", err)return}var str []bytetmp := make([]byte, 128)for {n, err := file.Read(tmp)if err == io.EOF {fmt.Println("讀取完畢...")break}if err != nil {fmt.Println("err:", err)return}str = append(str, tmp[:n]...)}fmt.Println(string(str))
}
注意:
1、每次只能讀取128個字節,所以我們不知道什么時候讀取完畢,通過對err == io.EOF來判斷是否讀取完畢。
2、切片是飲用類型,我們在拼接兩個切片的時候要指明tmp的區間,否則可能會把之前殘留的數據也拼接到str中。
2.3、方式二:bufio讀取文件
package mainimport ("bufio""fmt""io""os"
)func main() {file, err := os.Open("./main.go")defer file.Close()if err != nil {fmt.Println(err)return}var fileStr stringreader := bufio.NewReader(file)for {str, err := reader.ReadString('\n') // 表示一次讀取一行if err == io.EOF {fileStr += strbreak}if err != nil {fmt.Println(err)return}fileStr += str}fmt.Println(fileStr)
}
2.4、方式三:os.ReadFile讀取
package mainimport ("fmt""os"
)func main() {byteStr, err := os.ReadFile("./main.go")if err != nil {fmt.Println(err)return}fmt.Println(string(byteStr))
}
2.5、寫入文件
寫入文件就不能使用os.Open()打開了,這樣是只讀的,需要使用os.OpenFile()函數。
func OpenFile(name string, flag int, perm FileMode) (*File, error) {
...
}
name:要打開的文件名。flag:打開文件的模式。模式有以下幾種:
perm表示文件的權限,如果文件不存在我們創建文件的權限,傳入0666即可。
下面演示三種寫入文件的方式:
1、Write和WriteString寫入。
package mainimport ("fmt""os"
)func main() {file, err := os.OpenFile("./file.txt", os.O_CREATE|os.O_APPEND|os.O_RDWR, 0666)defer file.Close()if err != nil {fmt.Println(err)return}for i := 0; i < 10; i++ {file.WriteString(fmt.Sprintf("我是一行字符串-%d\n", i))}var str = "哈哈哈哈哈"file.Write([]byte(str))
}
2、bufio.NewWriter配合Flush寫入。
package mainimport ("bufio""fmt""os"
)func main() {file, err := os.OpenFile("./file.txt", os.O_CREATE|os.O_APPEND|os.O_RDWR, 0666)defer file.Close()if err != nil {fmt.Println(err)}writer := bufio.NewWriter(file)for i := 1; i <= 10; i++ {writer.WriteString(fmt.Sprintf("我是一行字符串-%d\n", i))}writer.Flush()
}
這里寫入的是緩存中,我們還需要調用Flush刷新才行。
3、os.WriteFile寫入。
package mainimport ("fmt""os"
)func main() {str := "hello world"err := os.WriteFile("./file.txt", []byte(str), 0666)if err != nil {fmt.Println(err)}
}
注意,這種寫入的方式每次都會清空文件內容,所以如果要追加寫入還是得采用前兩種方式。
2.6、復制文件
實現方式一,通過os.ReadFile從文件中讀取所有內容,再通過os.WriteFile寫入到另一個文件中。
package mainimport ("fmt""os"
)func copy(srcFileName string, dstFileName string) error {byteStr, err := os.ReadFile(srcFileName)if err != nil {return err}err = os.WriteFile(dstFileName, byteStr, 0666)if err != nil {return err}return nil
}func main() {src := "./file1.txt"dst := "./file2.txt"err := copy(src, dst)if err != nil {fmt.Println(err)return}fmt.Println("復制文件成功")
}
方式二:方法流的方式復制。
package mainimport ("fmt""io""os"
)func copy(srcFileName string, dstFileName string) error {src, err := os.Open(srcFileName)defer src.Close()if err != nil {return err}dst, err := os.OpenFile(dstFileName, os.O_CREATE|os.O_TRUNC|os.O_RDWR, 0666)defer dst.Close()if err != nil {return err}var byteStr = make([]byte, 128)for {n, err := src.Read(byteStr)if err == io.EOF {break}if err != nil {return err}if _, err := dst.Write(byteStr[:n]); err != nil {return err}}return nil
}func main() {src := "./file1.txt"dst := "./file2.txt"err := copy(src, dst)if err != nil {fmt.Println(err)}fmt.Println("復制文件完成...")
}
2.7、文件重命名
使用os.Rename函數對文件進行重命名。
package mainimport ("fmt""os"
)func main() {err := os.Rename("./file1.txt", "./file2.txt")if err != nil {fmt.Println(err)return}fmt.Println("重命名成功...")
}
2.8、創建目錄
使用os.Mkdir創建目錄,使用os.MkdirAll創建多級目錄。
package mainimport ("fmt""os"
)func main() {err := os.Mkdir("./abc", 0666)if err != nil {fmt.Println(err)return}fmt.Println("創建目錄成功...")err = os.MkdirAll("./d1/d2/d3", 0666)if err != nil {fmt.Println(err)return}fmt.Println("創建多級目錄成功...")
}
2.9、刪除目錄和文件
使用os.Remove刪除目錄或文件,使用os.RemoveAll刪除多個文件或目錄。
package mainimport ("fmt""os"
)func main() {err := os.Remove("./file1.txt")if err != nil {fmt.Println(err)return}err = os.Remove("./d1")if err != nil {fmt.Println(err)return}err = os.RemoveAll("./d2")if err != nil {fmt.Println(err)return}fmt.Println("刪除成功...")
}