反射和文件操作
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 main
import (
"fmt"
"reflect"
)
type myInt int
type Person struct {
Name string
Age int
}
// 通过反射获取任意变量的类型
func reflectFn(x interface{}) {
v := reflect.TypeOf(x)
fmt.Println(v)
}
func main() {
reflectFn(10)
var a = 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 = 10
reflectFn(x)
}
2、通过reflect.TypeOf获取返回的类型对象后,该对象里面还有两个方法Name和Kind。Name用来获取类型名称,Kind用来获取类型种类。
package main
import (
"fmt"
"reflect"
)
type myInt int
type Person struct {
Name string
Age int
}
func reflectFn(x interface{}) {
v := reflect.TypeOf(x)
fmt.Printf("类型: %v, 类型名称: %v, 类型种类: %v\n", v, v.Name(), v.Kind())
}
func main() {
var a = 10
reflectFn(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 = 10
reflectFn(x)
}
Go语言的反射中像数组、切片、Map、指针等类型的变量,它们的.Name()都是返回空。种类(Kind)就是指底层的类型。
1.2、reflect.ValueOf()
reflect.ValueOf()返回的是 reflect.Value 类型,其中包含了原始值的值信息。reflect.Value 与原始值之间可以互相转换。
1、例如实现接受任意值的接口,提取出原始值进行运算操作。
package main
import (
"fmt"
"reflect"
)
func reflectFn(x interface{}) {
v := reflect.ValueOf(x)
num := v.Int() + 10
fmt.Println(num)
}
func main() {
reflectFn(10)
}
在上面的代码中,我们先获取reflec.Value对象,然后通过该对象获取原始值进行运算操作。
2、但是上面这种情况是在我们知道类型的情况下,调用对应的获取原始值方法, 如果有多种类型,我们就需要进行判断。我们可以使用reflec.Value对象提供的kind函数获取种类,然后根据种类进行判断再调用对应的获取原始值方法。
package main
import (
"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 main
import (
"fmt"
"reflect"
)
func reflectFn(x interface{}) {
v := reflect.ValueOf(x)
fmt.Println(v, v.Kind(), v.Elem(), v.Elem().Kind())
}
func main() {
var x = 10
reflectFn(&x)
}
由于我们传入的是一个指针,所以获取值对象打印输出就是一个地址,通过kind获取类型是ptr,我们可以通过Elem函数来获取该指针指向的值,而不能通过解引用的方式来直接获取。要修改对应的值就先使用.Elem获取对象,然后再调用SetXXX来修改。
package main
import (
"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 = 10
var 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 main
import (
"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 = name
s.Age = age
s.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 main
import (
"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 main
import (
"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 main
import (
"fmt"
"io"
"os"
)
func main() {
file, err := os.Open("./main.go")
defer file.Close()
if err != nil {
fmt.Println("err:", err)
return
}
var str []byte
tmp := 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 main
import (
"bufio"
"fmt"
"io"
"os"
)
func main() {
file, err := os.Open("./main.go")
defer file.Close()
if err != nil {
fmt.Println(err)
return
}
var fileStr string
reader := bufio.NewReader(file)
for {
str, err := reader.ReadString('\n') // 表示一次读取一行
if err == io.EOF {
fileStr += str
break
}
if err != nil {
fmt.Println(err)
return
}
fileStr += str
}
fmt.Println(fileStr)
}
2.4、方式三:os.ReadFile读取
package main
import (
"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 main
import (
"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 main
import (
"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 main
import (
"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 main
import (
"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 main
import (
"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 main
import (
"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 main
import (
"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 main
import (
"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("删除成功...")
}