golang接口

接口也是一种类型,特殊的类型,它不关心对应方法的变量类型(接口只针对有接收者的函数,即方法)是什么,如fmt包的println函数可以传多种数据类型进行打印,接口常用于对不同变量类型定义一个相同方法名的方法(每个方法中的实现内容和结果都可能不同),这样就可以通过一个调用接口里函数的函数的参数的不同类型来调用不同数据类型的方法(可以少记很多方法名,且统一),可以用于连接各种不同数据库,采用同一函数,用传递进去的参数类型不同来区别连接哪种数据库

一个变量如果实现了接口中规定的所有方法(必须是实现了所有方法),那么这个变量就实现了这个接口,也可以称为这个接口类型的变量,那么定义一个接口变量就可以接收该变量的值,最后接口变量的类型会由于接收赋值变成该变量的类型

1
2
3
4
	s1 := stu{name: "学生"}
	var s sayer
	s = s1
	fmt.Printf("%T", s)

接口它自己不会存值,它的类型取决于给了它什么值,给什么值就是什么类型,接口类型定义时分配了一个动态类型和一个动态值,类型和值都为nil,然后将其他类型变量赋值给接口类型就是将值和值的类型赋值给接口,这样子就可以实现接口可以存任意类型的值。

image.png

一个类型的方法通常我们会用指针接收者,因为这样可以对原数据进行修改,也可以对值接收者传递指针,但是这样依然不能更改到原数据

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
//定义一个接口类型,里面只有方法声明
type sayer interface {
	say()
}

//定义一个学生的结构体和方法
type stu struct {
	name string
}
func (s *stu) say() {
	fmt.Println(s.name, "say hi  ttt")
	s.name = "王五"
}

type tea struct {
	name string
}
func (t *tea) say() {
	fmt.Println(t.name, "say hi")
}

func toSay(s sayer) {
	s.say()
}

func main() {
	s1 := stu{name: "学生"}
	toSay(&s1)
	fmt.Println(s1)
}

关于接口需要注意的是:只有当有两个或两个以上的具体类型必须以相同的方式进行处理时才需要定义接口,不要为了写接口而定义接口,那样只会增加不必要的抽象,导致不必要的运行时损耗。

image.png

接口命名习惯以er结尾,只有方法声明,

没有实现也没有数据字段,但是方法是可以添加参数的

image.png

然后在main函数中声明一个接口类型,var i Humaner,然后让 i 等于其他接收者的自定义类型,当 i 等于某个接收者类型它就会调用那个类型的同名方法,但是i等于其他接收者时,i只能调用属于i接口的方法,专属于接收者的方法以及其变量,接口都不能调用

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
//定义接口类型
type Hunmaner interface {
	sayhi()
}
//定义学生结构体类型
type student struct {
	name string
	id   int
}
//定义学生方法
func (student) sayhi() {
	fmt.Println("student say hi")
}
//定义老师结构体类型
type teacher struct {
	name  string
	addre string
}
//定义老师方法
func (teacher) sayhi() {
	fmt.Println("teacher say hi")
}

//实现多态,定义一个接口的方法,方法参数为接口类型,将其他接收者类型的实参传进去就可以实现调用不同方法
func whoSayhi(i Hunmaner){
	i.sayhi()
}

func main() {
	//i等于学生结构体i就可以调用学生的方法
	var i Hunmaner
	i = student{name: "mike"}
	i.sayhi()
	//输出学生方法中的语句

	//通过接口实现不同的方法
	whoSayhi(student{name: "mike"})

	i = teacher{name: "laoshi"}
	i.sayhi()
	//输出老师方法中的语句
}

也可以通过切片同时实现多种接收者的同名的方法

image.png

接口也可以实现继承(使用匿名字段),当定义一个Personer变量时,它可以调用父级的sayhi()方法也可以调用自身的sing()方法

image.png

父级可以等于子级的值,反过来则不可以(由多的向少的转换),如 定义一个父级i,让i等于子级iPro,i=iPro是可以的,注意语法是 父级=子级,这个作用于设置子级的值,然后将子级的值赋给父级使用,父级只能使用属于父级的方法,子级的方法无法使用

空接口

image.png

如何判断一个空接口变量中值的类型(value.(type))

,ok模式常用于测试map的对应key是否有值,有值ok为ture,没有则为false,value用于存放为true时将空接口类型中的值强转成对应类型的值,为false时为对应类型的零值

1
2
	i := map[int]string{1: "a"}
	value, ok := i[1]

还有另外五种用法(https://zhuanlan.zhihu.com/p/129220255)

有一种用法是判断空接口类型变量的type(切片和数组无法使用这个,因为它们两个在定义之初就已经确定了类型),value的返回依然是ok为true时将空接口类型中的值强转成对应类型的值,为false时存对应类型的零值

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
	i := make([]interface{}, 3)
	i[0] = 1
	i[1] = "str"
	i[2] = stu{"mike"}
	for _, deta := range i {
		if value, ok := deta.(int); ok == true {
			fmt.Println("这是一个整型", value)
		} else if value, ok := deta.(string); ok == true {
			fmt.Println("这是一个字符串", value)
		} else if value, ok := deta.(stu); ok == true {
			fmt.Println("这是一个结构体类型", value)
		}
	}

	//也可以使用switch方法,用此方法时括号中为type
	for _, deta := range i {
		switch value := deta.(type) {
		case int:
			fmt.Println("这是一个整型", value)
		case string:
			fmt.Println("这是一个字符串", value)
		case stu:
			fmt.Println("这是一个结构体类型", value)
		}
	}

反射

**反射是指在程序运行期对程序本身进行访问和修改的能力。**程序在编译时,变量被转换为内存地址,变量名不会被编译器写入到可执行部分。在运行程序时,程序无法获取自身的信息。(如函数的参数是一个空接口类型,那么在程序编译时它是不知道这个空接口存入什么类型的,只有在运行到此函数时才能确定)

支持反射的语言可以在程序编译期将变量的反射信息,如字段名称、类型信息、结构体信息等整合到可执行文件中,并给程序提供接口访问反射信息,这样就可以在程序运行期获取类型的反射信息,并且有能力修改它们。

Go程序在运行期使用reflect包访问程序的反射信息。

reflect包

在Go语言的反射机制中,任何接口值都由是 一个具体类型具体类型的值两部分组成的(动态类型和动态值)。 在Go语言中反射的相关功能由内置的reflect包提供,任意接口值在反射中都可以理解为由 reflect.Typereflect.Value两部分组成,并且reflect包提供了 reflect.TypeOfreflect.ValueOf两个函数来获取任意对象的Value和Type。reflect.TypeOf(a)返回值存的是原始值的类型,但此返回值的类型是 *reflect.rtype

在反射中关于类型还划分为两种:类型(Type)种类(Kind)。因为在Go语言中我们可以使用type关键字构造很多自定义类型,而 种类(Kind)就是指底层的类型,但在反射中,当需要区分指针、结构体等大品种的类型时,就会用到 种类(Kind)。通过reflect.TypeOf(a)返回值的name()方法取到的是具体类型,kind()方法取到的是种类(如结构体)

ValueOf

reflect.ValueOf()返回的是 reflect.Value类型,其中包含了原始值的值信息。b与原始值之间可以互相转换。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
func reflectValue(x interface{}) {
	v := reflect.ValueOf(x)
	k := v.Kind()	//这个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
}

ValueOf没有int64、int32之类的,它只有int、float等

参考文档: https://www.liwenzhou.com/posts/Go/13_reflect/

实现对ini配置文件的反射(将ini中的内容写入到结构体中)

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
package main

import (
	"errors"
	"fmt"
	"io/ioutil"
	"reflect"
	"strconv"
	"strings"
)

type MysqlConfig struct {
	Address  string `ini:"address"`
	Port     int    `ini:"port"`
	Username string `ini:"username"`
	Password string `ini:"password"`
}

type RedisConfig struct {
	Host     string `ini:"host"`
	Port     int    `ini:"port"`
	Password string `ini:"password"`
	Database uint8  `ini:"database"`
}

//嵌套结构体,用于关联配置文件和结构体
type Config struct {
	MysqlConfig `ini:"mysql"`
	RedisConfig `ini:"redis"`
}

func loadIni(b []byte, data interface{}) error {
	//1.参数的校验
	t := reflect.TypeOf(data)
	//1.1 传进来的data参数必须是指针类型(因为需要在函数中对其赋值,不使用指针就是修改的副本)
	if t.Kind() != reflect.Ptr {
		err := errors.New("传入的参数不是指针")
		return err
	}
	//1.2 传进来的data参数必须是结构体类型指针(因为配置文件中各种键值对需要赋值给结构体字段)
	if t.Elem().Kind() != reflect.Struct { //Elem()方法可以由对应的指针函数获取到相应的值
		err := errors.New("传入的参数不是结构体指针")
		return err
	}
	var structName string
	//2.一行一行的读数据,先将传进来的字节切片以换行切成字符串切片,windows中换行是\r\n
	lineStr := strings.Split(string(b), "\r\n")
	for index, line := range lineStr {
		//先去掉首尾空格
		line = strings.TrimSpace(line)
		//如果是空行就跳过
		if len(line)==0{
			continue
		}
		// 如果是注释就跳过,首字母是#或者;
		if strings.HasPrefix(line, "#") || strings.HasPrefix(line, ";") {
			continue
		}
		//加判断避免以 ] 开头
		if strings.HasPrefix(line, "]") {
			err := fmt.Errorf("节点定义错误,line:%d", index+1)
			return err
		}
		// 如果是[开头就表示是节(section)
		if strings.HasPrefix(line, "[") {
			if line[len(line)-1] != ']' {
				err := fmt.Errorf("节点定义错误,line:%d", index+1)
				return err
			}
			//先切出不包含[]的内容,然后去空格,最好统计长度为0则表示节点中全是空格或者未写
			sectionName := strings.TrimSpace(line[1 : len(line)-1])
			if len(sectionName) == 0 {
				err := fmt.Errorf("节点定义错误,line:%d", index+1)
				return err
			}
			//根据字符串sectionName去data里面根据反射找到对应的结构体,NumField()返回对应结构体的字段个数
			for i := 0; i < t.Elem().NumField(); i++ {
				filed := t.Elem().Field(i) //找到对应的结构体
				if sectionName ==filed.Tag.Get("ini") {
					structName=filed.Name	//filed.Name指对应结构体名
				}
			}
		} else {
			// 不是节点才是需要的键值对
			//	1.1 以等号分隔这一行,等号左边是key,右边是value
			//如果不包含=,行首或者行位是=就抛出错误
			if strings.Index(line,"=") ==-1 || strings.HasPrefix(line,"=")||strings.HasSuffix(line,"="){
				err:=fmt.Errorf("第%d行语法错误",index+1)
				return err
			}
			idx:=strings.Index(line,"=")
			key:=line[:idx]			//ini中的key
			value:=line[idx+1:]		//ini中的value
			//  1.2 根据structName去data里面把对应的嵌套结构体取出来
			v :=reflect.ValueOf(data)
			sValue:=v.Elem().FieldByName(structName)	//拿到嵌套结构体中的子结构体值信息
			sType:=sValue.Type()						//拿到嵌套结构体中的子结构体累心信息
			if sValue.Kind()!=reflect.Struct{
				err:=fmt.Errorf("%s不是一个结构体",structName)
				return err
			}
			var filedName string
			var filedType reflect.StructField
			// 1.3 遍历嵌套结构体的每一个字段,判断tag是否等于key
			for i:=0;i<sValue.NumField();i++{
				filed:=sType.Field(i)	//找到对应子结构体的成员
				filedType=filed			//tag是存储在类型信息中的
				//将结构体ini的字段和配置文件中的字段进行比较,相等则赋值
				if filed.Tag.Get("ini")==key{
					filedName=filed.Name	//filed.Name返回结构体的成员名
					break
				}
			}
			//for循环过后如果filedName为0则表示在结构体中没找到这个字段,直接跳过
			if len(filedName) ==0 {
				continue
			}
			// 1.4 根据filedName取出这个字段并赋值
			filedObj:=sValue.FieldByName(filedName)
			switch filedType.Type.Kind() {
			case reflect.String:
				filedObj.SetString(value)
			case reflect.Int,reflect.Int8,reflect.Int16,reflect.Int32,reflect.Int64:
				valueInt,err:=strconv.ParseInt(value,10,64)
				if err!=nil{
					err=fmt.Errorf("int转换失败,line:%d",index+1)
					return err
				}
				filedObj.SetInt(valueInt)
			}
		}
	}
	return nil
}

func main() {
	var cfg Config
	//使用ReadFile可以直接将文件内容全部读取出来,然后存进变量。
	//os.open()来一行行的读取会让程序运行时文件一直处于打开状态
	buf, err := ioutil.ReadFile("./config.ini")
	if err != nil {
		fmt.Printf("read file faild,err:%v\n", err)
		return
	}
	err = loadIni(buf, &cfg)
	if err != nil {
		fmt.Printf("loadIni file faild,err:%v\n", err)
		return
	}
	fmt.Println(cfg)
}

ini配置文件:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
; mysql config
[mysql]
address=10.20.30.40
port=3306
username=root
password=123456

# redis config
[redis]
host=127.0.0.1
port=6379
password=root
database=0
Licensed under CC BY-NC-SA 4.0