0%

一、为什么要用反射

需要反射的 2 个常见场景:

  1. 有时你需要编写一个函数,但是并不知道传给你的参数类型是什么,可能是没约定好;也可能是传入的类型很多,这些类型并不能统一表示。这时反射就会用的上了。
  2. 有时候需要根据某些条件决定调用哪个函数,比如根据用户的输入来决定。这时就需要对函数和函数的参数进行反射,在运行期间动态地执行函数。

但是对于反射,还是有几点不太建议使用反射的理由:

  1. 与反射相关的代码,经常是难以阅读的。在软件工程中,代码可读性也是一个非常重要的指标。
  2. Go 语言作为一门静态语言,编码过程中,编译器能提前发现一些类型错误,但是对于反射代码是无能为力的。所以包含反射相关的代码,很可能会运行很久,才会出错,这时候经常是直接 panic,可能会造成严重的后果。
  3. 反射对性能影响还是比较大的,比正常代码运行速度慢一到两个数量级。所以,对于一个项目中处于运行效率关键位置的代码,尽量避免使用反射特性。

二、相关基础

反射是如何实现的?我们以前学习过 interface,它是 Go 语言实现抽象的一个非常强大的工具。当向接口变量赋予一个实体类型的时候,接口会存储实体的类型信息,反射就是通过接口的类型信息实现的,反射建立在类型的基础上。

Go 语言在 reflect 包里定义了各种类型,实现了反射的各种函数,通过它们可以在运行时检测类型的信息、改变类型的值。在进行更加详细的了解之前,我们需要重新温习一下Go语言相关的一些特性,所谓温故知新,从这些特性中了解其反射机制是如何使用的。

特点 说明
go语言是静态类型语言。 编译时类型已经确定,比如对已基本数据类型的再定义后的类型,反射时候需要确认返回的是何种类型。
空接口interface{} go的反射机制是要通过接口来进行的,而类似于Java的Object的空接口可以和任何类型进行交互,因此对基本数据类型等的反射也直接利用了这一特点

Go语言的类型:

  • 变量包括(type, value)两部分

    理解这一点就知道为什么nil != nil了
    
  • type 包括 static type和concrete type. 简单来说 static type是你在编码是看见的类型(如int、string),concrete type是runtime系统看见的类型

  • 类型断言能否成功,取决于变量的concrete type,而不是static type。因此,一个 reader变量如果它的concrete type也实现了write方法的话,它也可以被类型断言为writer。

Go是静态类型语言。每个变量都拥有一个静态类型,这意味着每个变量的类型在编译时都是确定的:int,float32, *AutoType, []byte, chan []int 诸如此类。

在反射的概念中, 编译时就知道变量类型的是静态类型;运行时才知道一个变量类型的叫做动态类型。

  • 静态类型
    静态类型就是变量声明时的赋予的类型。比如:

    1
    2
    3
    4
    5
    6
    type MyInt int // int 就是静态类型

    type A struct{
    Name string // string就是静态
    }
    var i *int // *int就是静态类型
  • 动态类型
    动态类型:运行时给这个变量赋值时,这个值的类型(如果值为nil的时候没有动态类型)。一个变量的动态类型在运行时可能改变,这主要依赖于它的赋值(前提是这个变量是接口类型)。
1
2
3
4
5
var A interface{} // 静态类型interface{}
A = 10 // 静态类型为interface{} 动态为int
A = "String" // 静态类型为interface{} 动态为string
var M *int
A = M // A的值可以改变

Go语言的反射就是建立在类型之上的,Golang的指定类型的变量的类型是静态的(也就是指定int、string这些的变量,它的type是static type),在创建变量的时候就已经确定,反射主要与Golang的interface类型相关(它的type是concrete type),只有interface类型才有反射一说。

在Golang的实现中,每个interface变量都有一个对应pair,pair中记录了实际变量的值和类型:

1
(value, type)

value是实际变量值,type是实际变量的类型。一个interface{}类型的变量包含了2个指针,一个指针指向值的类型【对应concrete type】,另外一个指针指向实际的值【对应value】。

例如,创建类型为*os.File的变量,然后将其赋给一个接口变量r:

1
2
3
4
tty, err := os.OpenFile("/dev/tty", os.O_RDWR, 0)

var r io.Reader
r = tty

接口变量r的pair中将记录如下信息:(tty, *os.File),这个pair在接口变量的连续赋值过程中是不变的,将接口变量r赋给另一个接口变量w:

1
2
var w io.Writer
w = r.(io.Writer)

接口变量w的pair与r的pair相同,都是:(tty, *os.File),即使w是空接口类型,pair也是不变的。

interface及其pair的存在,是Golang中实现反射的前提,理解了pair,就更容易理解反射。反射就是用来检测存储在接口变量内部(值value;类型concrete type) pair对的一种机制。

所以我们要理解两个基本概念 Type 和 Value,它们也是 Go语言包中 reflect 空间里最重要的两个类型。

三、Type和Value

我们一般用到的包是reflect包。

既然反射就是用来检测存储在接口变量内部(值value;类型concrete type) pair对的一种机制。那么在Golang的reflect反射包中有什么样的方式可以让我们直接获取到变量内部的信息呢? 它提供了两种类型(或者说两个方法)让我们可以很容易的访问接口变量内容,分别是reflect.ValueOf() 和 reflect.TypeOf(),看看官方的解释

1
2
3
4
5
6
7
8
9
10
11
12
// ValueOf returns a new Value initialized to the concrete value
// stored in the interface i. ValueOf(nil) returns the zero
func ValueOf(i interface{}) Value {...}

翻译一下:ValueOf用来获取输入参数接口中的数据的值,如果接口为空则返回0


// TypeOf returns the reflection Type that represents the dynamic type of i.
// If i is a nil interface value, TypeOf returns nil.
func TypeOf(i interface{}) Type {...}

翻译一下:TypeOf用来动态获取输入参数接口中的值的类型,如果接口为空则返回nil

reflect.TypeOf()是获取pair中的type,reflect.ValueOf()获取pair中的value。

首先需要把它转化成reflect对象(reflect.Type或者reflect.Value,根据不同的情况调用不同的函数。

1
2
t := reflect.TypeOf(i) //得到类型的元数据,通过t我们能获取类型定义里面的所有元素
v := reflect.ValueOf(i) //得到实际的值,通过v我们获取存储在里面的值,还可以去改变值

示例代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
package main

import (
"fmt"
"reflect"
)

func main() {
//反射操作:通过反射,可以获取一个接口类型变量的 类型和数值
var x float64 =3.4

fmt.Println("type:",reflect.TypeOf(x)) //type: float64
fmt.Println("value:",reflect.ValueOf(x)) //value: 3.4

fmt.Println("-------------------")
//根据反射的值,来获取对应的类型和数值
v := reflect.ValueOf(x)
fmt.Println("kind is float64: ",v.Kind() == reflect.Float64)
fmt.Println("type : ",v.Type())
fmt.Println("value : ",v.Float())
}

运行结果:

1
2
3
4
5
6
type: float64
value: 3.4
-------------------
kind is float64: true
type : float64
value : 3.4

说明

  1. reflect.TypeOf: 直接给到了我们想要的type类型,如float64、int、各种pointer、struct 等等真实的类型
  2. reflect.ValueOf:直接给到了我们想要的具体的值,如1.2345这个具体数值,或者类似&{1 “Allen.Wu” 25} 这样的结构体struct的值
  3. 也就是说明反射可以将“接口类型变量”转换为“反射类型对象”,反射类型指的是reflect.Type和reflect.Value这两种

Type 和 Value 都包含了大量的方法,其中第一个有用的方法应该是 Kind,这个方法返回该类型的具体信息:Uint、Float64 等。Value 类型还包含了一系列类型方法,比如 Int(),用于返回对应的值。以下是Kind的种类:

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

// A Kind represents the specific kind of type that a Type represents.
// The zero Kind is not a valid kind.
type Kind uint

const (
Invalid Kind = iota
Bool
Int
Int8
Int16
Int32
Int64
Uint
Uint8
Uint16
Uint32
Uint64
Uintptr
Float32
Float64
Complex64
Complex128
Array
Chan
Func
Interface
Map
Ptr
Slice
String
Struct
UnsafePointer
)

四、反射的规则

其实反射的操作步骤非常的简单,就是通过实体对象获取反射对象(Value、Type),然后操作相应的方法即可。

下图描述了实例、Value、Type 三者之间的转换关系:

WX20190827-170219/img/WX20190827-170219.png)

反射 API 的分类总结如下:

1) 从实例到 Value

通过实例获取 Value 对象,直接使用 reflect.ValueOf() 函数。例如:

1
func ValueOf(i interface {}) Value

2) 从实例到 Type

通过实例获取反射对象的 Type,直接使用 reflect.TypeOf() 函数。例如:

1
func TypeOf(i interface{}) Type

3) 从 Type 到 Value

Type 里面只有类型信息,所以直接从一个 Type 接口变量里面是无法获得实例的 Value 的,但可以通过该 Type 构建一个新实例的 Value。reflect 包提供了两种方法,示例如下:

1
2
3
4
//New 返回的是一个 Value,该 Value 的 type 为 PtrTo(typ),即 Value 的 Type 是指定 typ 的指针类型
func New(typ Type) Value
//Zero 返回的是一个 typ 类型的零佳,注意返回的 Value 不能寻址,位不可改变
func Zero(typ Type) Value

如果知道一个类型值的底层存放地址,则还有一个函数是可以依据 type 和该地址值恢复出 Value 的。例如:

1
func NewAt(typ Type, p unsafe.Pointer) Value

4) 从 Value 到 Type

从反射对象 Value 到 Type 可以直接调用 Value 的方法,因为 Value 内部存放着到 Type 类型的指针。例如:

1
func (v Value) Type() Type

5) 从 Value 到实例

Value 本身就包含类型和值信息,reflect 提供了丰富的方法来实现从 Value 到实例的转换。例如:

1
2
3
4
5
6
7
8
9
//该方法最通用,用来将 Value 转换为空接口,该空接口内部存放具体类型实例
//可以使用接口类型查询去还原为具体的类型
func (v Value) Interface()i interface{})

//Value 自身也提供丰富的方法,直接将 Value 转换为简单类型实例,如果类型不匹配,则直接引起 panic
func (v Value) Bool () bool
func (v Value) Float() float64
func (v Value) Int() int64
func (v Value) Uint() uint64

6) 从 Value 的指针到值

从一个指针类型的 Value 获得值类型 Value 有两种方法,示例如下。

1
2
3
4
//如果 v 类型是接口,则 Elem() 返回接口绑定的实例的 Value,如采 v 类型是指针,则返回指针值的 Value,否则引起 panic
func (v Value) Elem() Value
//如果 v 是指针,则返回指针值的 Value,否则返回 v 自身,该函数不会引起 panic
func Indirect(v Value) Value

7) Type 指针和值的相互转换

指针类型 Type 到值类型 Type。例如:

1
2
3
//t 必须是 Array、Chan、Map、Ptr、Slice,否则会引起 panic
//Elem 返回的是其内部元素的 Type
t.Elem() Type

值类型 Type 到指针类型 Type。例如:

1
2
//PtrTo 返回的是指向 t 的指针型 Type
func PtrTo(t Type) Type

8) Value 值的可修改性

Value 值的修改涉及如下两个方法:

1
2
3
4
//通过 CanSet 判断是否能修改
func (v Value ) CanSet() bool
//通过 Set 进行修改
func (v Value ) Set(x Value)

Value 值在什么情况下可以修改?我们知道实例对象传递给接口的是一个完全的值拷贝,如果调用反射的方法 reflect.ValueOf() 传进去的是一个值类型变量, 则获得的 Value 实际上是原对象的一个副本,这个 Value 是无论如何也不能被修改的。

根据 Go 官方关于反射的博客,反射有三大定律:

  1. Reflection goes from interface value to reflection object.
  2. Reflection goes from reflection object to interface value.
  3. To modify a reflection object, the value must be settable.

第一条是最基本的:反射可以从接口值得到反射对象。

    反射是一种检测存储在 interface中的类型和值机制。这可以通过 TypeOf函数和 ValueOf函数得到。

第二条实际上和第一条是相反的机制,反射可以从反射对象获得接口值。

    它将 ValueOf的返回值通过 Interface()函数反向转变成 interface变量。

前两条就是说 接口型变量和 反射类型对象可以相互转化,反射类型对象实际上就是指的前面说的 reflect.Type和 reflect.Value。

第三条不太好懂:如果需要操作一个反射变量,则其值必须可以修改。

    反射变量可设置的本质是它存储了原变量本身,这样对反射变量的操作,就会反映到原变量本身;反之,如果反射变量不能代表原变量,那么操作了反射变量,不会对原变量产生任何影响,这会给使用者带来疑惑。所以第二种情况在语言层面是不被允许的。

五、反射的使用

5.1 从relfect.Value中获取接口interface的信息

当执行reflect.ValueOf(interface)之后,就得到了一个类型为”relfect.Value”变量,可以通过它本身的Interface()方法获得接口变量的真实内容,然后可以通过类型判断进行转换,转换为原有真实类型。不过,我们可能是已知原有类型,也有可能是未知原有类型,因此,下面分两种情况进行说明。

已知原有类型

已知类型后转换为其对应的类型的做法如下,直接通过Interface方法然后强制转换,如下:

1
realValue := value.Interface().(已知的类型)

示例代码:

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
package main

import (
"fmt"
"reflect"
)

func main() {
var num float64 = 1.2345
// "接口类型变量"-->"反射类型对象"
value := reflect.ValueOf(num)

// 可以理解为“强制转换”,但是需要注意的时候,转换的时候,如果转换的类型不完全符合,则直接panic
// Golang 对类型要求非常严格,类型一定要完全符合
// 如下两个,一个是*float64,一个是float64,如果弄混,则会panic

// "反射类型对象" --> "接口类型变量"
convertValue := value.Interface().(float64)
fmt.Println(convertValue)

pointer := reflect.ValueOf(&num)
convertPointer := pointer.Interface().(*float64)
fmt.Println(convertPointer)

}

运行结果:

1
2
0xc000098000
1.2345

说明

  1. 转换的时候,如果转换的类型不完全符合,则直接panic,类型要求非常严格!
  2. 转换的时候,要区分是指针还是指
  3. 也就是说反射可以将“反射类型对象”再重新转换为“接口类型变量”

未知原有类型

很多情况下,我们可能并不知道其具体类型,那么这个时候,该如何做呢?需要我们进行遍历探测其Filed来得知,示例如下:

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
package main

import (
"fmt"
"reflect"
)




type Person struct {
Name string
Age int
Sex string
}

func (p Person)Say(msg string) {
fmt.Println("hello,",msg)
}
func (p Person)PrintInfo() {
fmt.Printf("姓名:%s,年龄:%d,性别:%s\n",p.Name,p.Age,p.Sex)
}



func main() {
p1 := Person{"王二狗",30,"男"}

DoFiledAndMethod(p1)

}

// 通过接口来获取任意参数
func DoFiledAndMethod(input interface{}) {

getType := reflect.TypeOf(input) //先获取input的类型
fmt.Println("get Type is :", getType.Name()) // Person
fmt.Println("get Kind is : ", getType.Kind()) // struct

getValue := reflect.ValueOf(input)
fmt.Println("get all Fields is:", getValue) //{王二狗 30 男}

// 获取方法字段
// 1. 先获取interface的reflect.Type,然后通过NumField进行遍历
// 2. 再通过reflect.Type的Field获取其Field
// 3. 最后通过Field的Interface()得到对应的value
for i := 0; i < getType.NumField(); i++ {
field := getType.Field(i)
value := getValue.Field(i).Interface() //获取第i个值
fmt.Printf("字段名称:%s, 字段类型:%s, 字段数值:%v \n", field.Name, field.Type, value)
}

// 通过反射,操作方法
// 1. 先获取interface的reflect.Type,然后通过.NumMethod进行遍历
// 2. 再公国reflect.Type的Method获取其Method
for i := 0; i < getType.NumMethod(); i++ {
method := getType.Method(i)
fmt.Printf("方法名称:%s, 方法类型:%v \n", method.Name, method.Type)
}
}

运行结果:

1
2
3
4
5
6
7
8
get Type is : Person
get Kind is : struct
get all Fields is: {王二狗 30 男}
字段名称:Name, 字段类型:string, 字段数值:王二狗
字段名称:Age, 字段类型:int, 字段数值:30
字段名称:Sex, 字段类型:string, 字段数值:男
方法名称:PrintInfo, 方法类型:func(main.Person)
方法名称:Say, 方法类型:func(main.Person, string)

说明

通过运行结果可以得知获取未知类型的interface的具体变量及其类型的步骤为:

  1. 先获取interface的reflect.Type,然后通过NumField进行遍历
  2. 再通过reflect.Type的Field获取其Field
  3. 最后通过Field的Interface()得到对应的value

通过运行结果可以得知获取未知类型的interface的所属方法(函数)的步骤为:

  1. 先获取interface的reflect.Type,然后通过NumMethod进行遍历
  2. 再分别通过reflect.Type的Method获取对应的真实的方法(函数)
  3. 最后对结果取其Name和Type得知具体的方法名
  4. 也就是说反射可以将“反射类型对象”再重新转换为“接口类型变量”
  5. struct 或者 struct 的嵌套都是一样的判断处理方式

如果是struct的话,可以使用Elem()

1
2
tag := t.Elem().Field(0).Tag //获取定义在struct里面的Tag属性
name := v.Elem().Field(0).String() //获取存储在第一个字段里面的值

5.2 通过reflect.Value设置实际变量的值

reflect.Value是通过reflect.ValueOf(X)获得的,只有当X是指针的时候,才可以通过reflec.Value修改实际变量X的值,即:要修改反射类型的对象就一定要保证其值是“addressable”的。

这里需要一个方法:

WX20190826-143547/img/WX20190826-143547.png)

解释起来就是:Elem返回接口v包含的值或指针v指向的值。如果v的类型不是interface或ptr,它会恐慌。如果v为零,则返回零值。

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
package main

import (
"fmt"
"reflect"
)

func main() {

var num float64 = 1.2345
fmt.Println("old value of pointer:", num)

// 通过reflect.ValueOf获取num中的reflect.Value,注意,参数必须是指针才能修改其值
pointer := reflect.ValueOf(&num)
newValue := pointer.Elem()

fmt.Println("type of pointer:", newValue.Type())
fmt.Println("settability of pointer:", newValue.CanSet())

// 重新赋值
newValue.SetFloat(77)
fmt.Println("new value of pointer:", num)

////////////////////
// 如果reflect.ValueOf的参数不是指针,会如何?
//pointer = reflect.ValueOf(num)
//newValue = pointer.Elem() // 如果非指针,这里直接panic,“panic: reflect: call of reflect.Value.Elem on float64 Value”
}
// 另外一个demo
package main

import (
"fmt"
"reflect"
)

type Student struct {
Name string
Age int
School string
}
func main() {
s1 := Student{"王二狗",19,"北大"}
//通过反射,更改对象的数值:前提也是数据可以被更改
fmt.Printf("%T\n",s1) //main.Student
p1 := &s1
fmt.Printf("%T\n",p1) //*main.Student
fmt.Println(s1.Name)
fmt.Println((*p1).Name,p1.Name)

//改变数值
value:=reflect.ValueOf(&s1)
if value.Kind() == reflect.Ptr{
newValue:=value.Elem()
fmt.Println(newValue.CanSet()) //true

f1 :=newValue.FieldByName("Name")
f1.SetString("小明")
f3:=newValue.FieldByName("School")
f3.SetString("清华")
fmt.Println(s1)
}
}

运行结果:

1
2
3
4
old value of pointer: 1.2345
type of pointer: float64
settability of pointer: true
new value of pointer: 77

说明

  1. 需要传入的参数是 float64这个指针,然后可以通过pointer.Elem()去获取所指向的Value,*注意一定要是指针
  2. 如果传入的参数不是指针,而是变量,那么
    • 通过Elem获取原始值对应的对象则直接panic
    • 通过CanSet方法查询是否可以设置返回false
  3. newValue.CantSet()表示是否可以重新设置其值,如果输出的是true则可修改,否则不能修改,修改完之后再进行打印发现真的已经修改了。
  4. reflect.Value.Elem() 表示获取原始值对应的反射对象,只有原始对象才能修改,当前反射对象是不能修改的
  5. 也就是说如果要修改反射类型对象,其值必须是“addressable”【对应的要传入的是指针,同时要通过Elem方法获取原始值对应的反射对象】
  6. struct 或者 struct 的嵌套都是一样的判断处理方式

5.3 通过reflect.Value来进行方法的调用

这算是一个高级用法了,前面我们只说到对类型、变量的几种反射的用法,包括如何获取其值、其类型、以及如何重新设置新值。但是在项目应用中,另外一个常用并且属于高级的用法,就是通过reflect来进行方法【函数】的调用。比如我们要做框架工程的时候,需要可以随意扩展方法,或者说用户可以自定义方法,那么我们通过什么手段来扩展让用户能够自定义呢?关键点在于用户的自定义方法是未可知的,因此我们可以通过reflect来搞定。

Call()方法:

WX20190902-144001

通过反射,调用方法。

先获取结构体对象,然后

示例代码:

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
package main

import (
"fmt"
"reflect"
)




type Person struct {
Name string
Age int
Sex string
}

func (p Person)Say(msg string) {
fmt.Println("hello,",msg)
}
func (p Person)PrintInfo() {
fmt.Printf("姓名:%s,年龄:%d,性别:%s\n",p.Name,p.Age,p.Sex)
}

func (p Person) Test(i,j int,s string){
fmt.Println(i,j,s)
}


// 如何通过反射来进行方法的调用?
// 本来可以用结构体对象.方法名称()直接调用的,
// 但是如果要通过反射,
// 那么首先要将方法注册,也就是MethodByName,然后通过反射调动mv.Call

func main() {
p2 := Person{"Ruby",30,"男"}
// 1. 要通过反射来调用起对应的方法,必须要先通过reflect.ValueOf(interface)来获取到reflect.Value,
// 得到“反射类型对象”后才能做下一步处理
getValue := reflect.ValueOf(p2)

// 2.一定要指定参数为正确的方法名
// 先看看没有参数的调用方法

methodValue1 := getValue.MethodByName("PrintInfo")
fmt.Printf("Kind : %s, Type : %s\n",methodValue1.Kind(),methodValue1.Type())
methodValue1.Call(nil) //没有参数,直接写nil

args1 := make([]reflect.Value, 0) //或者创建一个空的切片也可以
methodValue1.Call(args1)

// 有参数的方法调用
methodValue2 := getValue.MethodByName("Say")
fmt.Printf("Kind : %s, Type : %s\n",methodValue2.Kind(),methodValue2.Type())
args2 := []reflect.Value{reflect.ValueOf("反射机制")}
methodValue2.Call(args2)

methodValue3 := getValue.MethodByName("Test")
fmt.Printf("Kind : %s, Type : %s\n",methodValue3.Kind(),methodValue3.Type())
args3 := []reflect.Value{reflect.ValueOf(100), reflect.ValueOf(200),reflect.ValueOf("Hello")}

methodValue3.Call(args3)
}

运行结果:

1
2
3
4
5
6
7
Kind : func, Type : func()
姓名:Ruby,年龄:30,性别:男
姓名:Ruby,年龄:30,性别:男
Kind : func, Type : func(string)
hello, 反射机制
Kind : func, Type : func(int, int, string)
100 200 Hello

通过反射,调用函数。

首先我们要先确认一点,函数像普通的变量一样,之前的章节中我们在讲到函数的本质的时候,是可以把函数作为一种变量类型的,而且是引用类型。如果说Fun()是一个函数,那么f1 := Fun也是可以的,那么f1也是一个函数,如果直接调用f1(),那么运行的就是Fun()函数。

那么我们就先通过ValueOf()来获取函数的反射对象,可以判断它的Kind,是一个func,那么就可以执行Call()进行函数的调用。

示例代码:

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
package main

import (
"fmt"
"reflect"
)

func main() {
//函数的反射
f1 := fun1
value := reflect.ValueOf(f1)
fmt.Printf("Kind : %s , Type : %s\n",value.Kind(),value.Type()) //Kind : func , Type : func()

value2 := reflect.ValueOf(fun2)
fmt.Printf("Kind : %s , Type : %s\n",value2.Kind(),value2.Type()) //Kind : func , Type : func(int, string)


//通过反射调用函数
value.Call(nil)

value2.Call([]reflect.Value{reflect.ValueOf(100),reflect.ValueOf("hello")})

}

func fun1(){
fmt.Println("我是函数fun1(),无参的。。")
}

func fun2(i int, s string){
fmt.Println("我是函数fun2(),有参数。。",i,s)
}

说明

  1. 要通过反射来调用起对应的方法,必须要先通过reflect.ValueOf(interface)来获取到reflect.Value,得到“反射类型对象”后才能做下一步处理
  2. reflect.Value.MethodByName这个MethodByName,需要指定准确真实的方法名字,如果错误将直接panic,MethodByName返回一个函数值对应的reflect.Value方法的名字。
  3. []reflect.Value,这个是最终需要调用的方法的参数,可以没有或者一个或者多个,根据实际参数来定。
  4. reflect.Value的 Call 这个方法,这个方法将最终调用真实的方法,参数务必保持一致,如果reflect.Value.Kind不是一个方法,那么将直接panic。
  5. 本来可以用对象访问方法直接调用的,但是如果要通过反射,那么首先要将方法注册,也就是MethodByName,然后通过反射调用methodValue.Call

本文参照:

http://www.sohu.com/a/313420275_657921

https://studygolang.com/articles/12348?fr=sidebar

http://c.biancheng.net/golang/

RPM软件包

在RPM(红帽软件包管理器)公布之前,要想在Linux系统中安装软件只能采取源码包的方式安装。早期在Linux系统中安装程序是一件非常困难、耗费耐心的事情,而且大多数的服务程序仅仅提供源代码,需要运维人员自行编译代码并解决许多的软件依赖关系,因此要安装好一个服务程序,运维人员需要具备丰富知识、高超的技能,甚至良好的耐心。而且在安装、升级、卸载服务程序时还要考虑到其他程序、库的依赖关系,所以在进行校验、安装、卸载、查询、升级等管理软件操作时难度都非常大。

RPM机制则为解决这些问题而设计的。RPM有点像Windows系统中的控制面板,会建立统一的数据库文件,详细记录软件信息并能够自动分析依赖关系。目前RPM的优势已经被公众所认可,使用范围也已不局限在红帽系统中了。表1-1是一些常用的RPM软件包命令,当前不需要记住它们,大致混个“脸熟”就足够了。

阅读全文 »

为什么需要JWT?

在之前的一些web项目中,我们通常使用的是Cookie-Session模式实现用户认证。相关流程大致如下:

  1. 用户在浏览器端填写用户名和密码,并发送给服务端
  2. 服务端对用户名和密码校验通过后会生成一份保存当前用户相关信息的session数据和一个与之对应的标识(通常称为session_id)
  3. 服务端返回响应时将上一步的session_id写入用户浏览器的Cookie
  4. 后续用户来自该浏览器的每次请求都会自动携带包含session_id的Cookie
  5. 服务端通过请求中的session_id就能找到之前保存的该用户那份session数据,从而获取该用户的相关信息。
阅读全文 »

os.File、bufio、ioutil 比较

效率测试

文件的读取效率是所有开发者都会关心的话题,尤其是当文件特别大的时候。为了尽可能的展示这三者对文件读取的性能,我准备了三个文件,分别为small.txtmidium.txtlarge.txt,分别对应 KB 级别、MB 级别和 GB 级别。

创建文件

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
package main

import (
"encoding/json"
"fmt"
"log"
"math"
"os"
"strings"
)

func main() {
s := map[string]interface{}{
"name": "harry",
"age": 20,
}
sStr, err := json.Marshal(s)
fmt.Println(len(sStr))
if err != nil {
fmt.Println(err)
}
CreateFixedFile(4*1024, string(sStr), "./small.txt")
}

// 创建指定大小文件
func CreateFixedFile(size float64, source string, fileName string) {
file, err := os.OpenFile(fileName, os.O_CREATE|os.O_WRONLY|os.O_APPEND, os.ModePerm)
if err != nil {
fmt.Println(err)
}
defer file.Close()

count := math.Ceil(float64(size) / float64(len(source)))

fmt.Println(count)
n, err := file.WriteString(strings.Repeat(source+"," + "\r\n", int(count)) )
HandleErr(err)
fmt.Println(n)

}

// 错误处理
func HandleErr(err error) {
if err != nil {
log.Fatal(err)
}
}

开始测试

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
package main

import (
"bufio"
"fmt"
"io"
"io/ioutil"
"log"
"os"
"time"
)

// 使用File自带的Read
func read1(filename string) int {
file, err := os.Open(filename)
if err != nil {
log.Fatal(err)
}
defer file.Close()
buf := make([]byte, 4096)
var countBytes int
for {
n, err := file.Read(buf)

if n == 0 || err ==io.EOF{
fmt.Println("读取文件的末尾了。。。")
break
}
countBytes += n
}
return countBytes
}

// 使用bufio
func read2(filename string) int {
file, err := os.Open(filename)
if err != nil {
log.Fatal(err)
}
defer file.Close()
buf := make([]byte, 4096)
var countBytes int
rd := bufio.NewReader(file)
for {
n, err := rd.Read(buf)
if n== 0 || err == io.EOF{
fmt.Println("读取文件的末尾了。。。")
break
}
countBytes += n

}
return countBytes
}

// 使用ioutil
func read3(filename string) int {
file, err := os.Open(filename)
if err != nil {
log.Fatal(err)
}
defer file.Close()
all, err := ioutil.ReadAll(file)
countBytes := len(all)
return countBytes
}

func testfile(filename string) {
fmt.Printf("============test1 %s ===========\n", filename)
start := time.Now()
size1 := read1(filename)
t1 := time.Now()
fmt.Printf("Read 1 cost: %v, size: %d\n", t1.Sub(start), size1)
size2 := read2(filename)
t2 := time.Now()
fmt.Printf("Read 2 cost: %v, size: %d\n", t2.Sub(t1), size2)
size3 := read3(filename)
t3 := time.Now()
fmt.Printf("Read 3 cost: %v, size: %d\n", t3.Sub(t2), size3)
}

func main() {
testfile("small.txt")
testfile("midium.txt")
testfile("large.txt")
}

  • 当文件较小(KB 级别)时,ioutil > bufio > os。
  • 当文件大小比较常规(MB 级别)时,三者差别不大,但 bufio 又是已经显现出来。
  • 当文件较大(GB 级别)时,bufio > os > ioutil。

原因分析

为什么会出现上面的不同结果?其实ioutil最好理解,当文件较小时,ioutil使用ReadAll函数将文件中所有内容直接读入内存,只进行了一次 io 操作,但是osbufio都是进行了多次读取,才将文件处理完,所以ioutil肯定要快于osbufio的。但是随着文件的增大,达到接近 GB 级别时,ioutil直接读入内存的弊端就显现出来,要将 GB 级别的文件内容全部读入内存,也就意味着要开辟一块 GB 大小的内存用来存放文件数据,这对内存的消耗是非常大的,因此效率就慢了下来。如果文件继续增大,达到 3GB 甚至以上,ioutil这种读取方式就完全无能为力了。(一个单独的进程空间为 4GB,真正存放数据的堆区和栈区更是远远小于 4GB)。而os为什么在面对大文件时,效率会低于bufio?通过查看bufioNewReader源码不难发现,在NewReader里,默认为我们提供了一个大小为 4096 的缓冲区,所以系统调用会每次先读取 4096 字节到缓冲区,然后rd.Read会从缓冲区去读取。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
const (
defaultBufSize = 4096
)

func NewReader(rd io.Reader) *Reader {
return NewReaderSize(rd, defaultBufSize)
}

func NewReaderSize(rd io.Reader, size int) *Reader {
// Is it already a Reader?
b, ok := rd.(*Reader)
if ok && len(b.buf) >= size {
return b
}
if size < minReadBufferSize {
size = minReadBufferSize
}
r := new(Reader)
r.reset(make([]byte, size), rd)
return r
}

os因为少了这一层缓冲区,每次读取,都会执行系统调用,因此内核频繁的在用户态和内核态之间切换,而这种切换,也是需要消耗的,故而会慢于bufio的读取方式。笔者翻阅网上资料,关于缓冲,有内核中的缓冲进程中的缓冲两种,其中,内核中的缓冲是内核提供的,即系统对磁盘提供一个缓冲区,不管有没有提供进程中的缓冲,内核缓冲都是存在的。而进程中的缓冲是对输入输出流做了一定的改进,提供的一种流缓冲,它在读写操作发生时,先将数据存入流缓冲中,只有当流缓冲区满了或者刷新(如调用flush函数)时,才将数据取出,送往内核缓冲区,它起到了一定的保护内核的作用。因此,我们不难发现,os是典型的内核中的缓冲,而bufioioutil都属于进程中的缓冲。

总结

当读取小文件时,使用ioutil效率明显优于os和bufio,但如果是大文件,bufio读取会更快。

读取一行数据

前面简要分析了 go 语言三种不同读取文件方式之间的区别。但实际的开发中,我们对文件的读取往往是以行为单位的,即每次读取一行进行处理。go 语言并没有像 C 语言一样给我们提供好了类似于fgets这样的函数可以正好读取一行内容,因此,需要自己去实现。从前面的对比分析可以知道,无论是处理大文件还是小文件,bufio始终是最为平滑和高效的,因此我们考虑使用bufio库进行处理。翻阅bufio库的源码,发现可以使用如下几种方式进行读取一行文件的处理:

  • ReadBytes
  • ReadString
  • ReadSlice
  • ReadLine
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
package main

import (
"bufio"
"fmt"
"io"
"os"
"time"
)

func readline1(filename string) {
fi, err := os.Open(filename)
if err != nil {
panic(err)
}
defer fi.Close()
rd := bufio.NewReader(fi)
for {
_, err := rd.ReadBytes('\n')
if err != nil || err == io.EOF {
break
}
}
}

func readline2(filename string) {
fi, err := os.Open(filename)
if err != nil {
panic(err)
}
defer fi.Close()
rd := bufio.NewReader(fi)
for {
_, err := rd.ReadString('\n')
if err != nil || err == io.EOF {
break
}
}
}
func readline3(filename string) {
fi, err := os.Open(filename)
if err != nil {
panic(err)
}
defer fi.Close()
rd := bufio.NewReader(fi)
for {
_, err := rd.ReadSlice('\n')
if err != nil || err == io.EOF {
break
}
}
}
func readline4(filename string) {
fi, err := os.Open(filename)
if err != nil {
panic(err)
}
defer fi.Close()
rd := bufio.NewReader(fi)
for {
_, _, err := rd.ReadLine()
if err != nil || err == io.EOF {
break
}
}
}
func testfile2(filename string) {
fmt.Printf("============test %s ===========\n", filename)
start := time.Now()
readline1(filename)
t1 := time.Now()
fmt.Printf("Readline 1 ReadBytes cost: %v\n", t1.Sub(start))
readline2(filename)
t2 := time.Now()
fmt.Printf("Readline 2 ReadString cost: %v\n", t2.Sub(t1))
readline3(filename)
t3 := time.Now()
fmt.Printf("Readline 3 ReadSlice cost: %v\n", t3.Sub(t2))
readline4(filename)
t4 := time.Now()
fmt.Printf("Readline 4 ReadLine cost: %v\n", t4.Sub(t3))
}
func main() {
testfile2("small.txt")
testfile2("midium.txt")
testfile2("large.txt")
}

通过现象,除了small.txt之外,大致可以分为两组:

  • ReadBytes对文件处理效率最差
  • 在处理大文件时,ReadLineReadSlice效率相近,要明显快于ReadStringReadBytes

原因分析

为什么会出现上面的现象,不防从源码层面进行分析。通过阅读源码,我们发现这四个函数之间存在这样一个关系:

  • ReadLine <- (调用) ReadSlice
  • ReadString <- (调用)ReadBytes<-(调用)ReadSlice

既然如此,那为什么在处理大文件时,ReadLine效率要明显高于ReadBytes呢?

首先,我们要知道,ReadSlice是切片式读取,即根据分隔符去进行切片。通过源码发下,ReadLine只是在切片读取的基础上,对换行符\n\r\n做了一些处理:

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
func (b *Reader) ReadLine() (line []byte, isPrefix bool, err error) {
line, err = b.ReadSlice('\n')
if err == ErrBufferFull {
// Handle the case where "\r\n" straddles the buffer.
if len(line) > 0 && line[len(line)-1] == '\r' {
// Put the '\r' back on buf and drop it from line.
// Let the next call to ReadLine check for "\r\n".
if b.r == 0 {
// should be unreachable
panic("bufio: tried to rewind past start of buffer")
}
b.r--
line = line[:len(line)-1]
}
return line, true, nil
}

if len(line) == 0 {
if err != nil {
line = nil
}
return
}
err = nil

if line[len(line)-1] == '\n' {
drop := 1
if len(line) > 1 && line[len(line)-2] == '\r' {
drop = 2
}
line = line[:len(line)-drop]
}
return
}

ReadBytes则是通过append先将读取的内容暂存到full数组中,最后再copy出来,appendcopy都是要消耗内存和 io 的,因此效率自然就慢了。其源码如下所示:

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
func (b *Reader) ReadBytes(delim byte) ([]byte, error) {
// Use ReadSlice to look for array,
// accumulating full buffers.
var frag []byte
var full [][]byte
var err error
n := 0
for {
var e error
frag, e = b.ReadSlice(delim)
if e == nil { // got final fragment
break
}
if e != ErrBufferFull { // unexpected error
err = e
break
}

// Make a copy of the buffer.
buf := make([]byte, len(frag))
copy(buf, frag)
full = append(full, buf)
n += len(buf)
}

n += len(frag)

// Allocate new buffer to hold the full pieces and the fragment.
buf := make([]byte, n)
n = 0
// Copy full pieces and fragment in.
for i := range full {
n += copy(buf[n:], full[i])
}
copy(buf[n:], frag)
return buf, err
}

总结

读取文件中一行内容时,ReadSliceReadLine性能优于ReadBytesReadString,但由于ReadLine对换行的处理更加全面(兼容\n\r\n换行),因此,实际开发过程中,建议使用ReadLine函数。

参考原文链接:https://segmentfault.com/a/1190000023691973

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
package main

import (
"encoding/json"
"fmt"
"log"
"math"
"os"
"strings"
)

func main() {
s := map[string]interface{}{
"name": "harry",
"age": 20,
}
sStr, err := json.Marshal(s)
fmt.Println(len(sStr))
if err != nil {
fmt.Println(err)
}
CreateFixedFile(4*1024, string(sStr), "./small.txt")
}

// 创建指定大小文件
func CreateFixedFile(size float64, source string, fileName string) {
file, err := os.OpenFile(fileName, os.O_CREATE|os.O_WRONLY|os.O_APPEND, os.ModePerm)
if err != nil {
fmt.Println(err)
}
defer file.Close()

count := math.Ceil(float64(size) / float64(len(source)))

fmt.Println(count)
n, err := file.WriteString(strings.Repeat(source+"," + "\r\n", int(count)) )
HandleErr(err)
fmt.Println(n)

}

// 错误处理
func HandleErr(err error) {
if err != nil {
log.Fatal(err)
}
}

简单示例

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
package main

import (
"fmt"
"math/rand"
"net"
"smartphone/db/postg"

"time"
)

func main() {
instr := `insert into aicall.t_rebot_dir (rebot_name,rebot_sign,rebot_state,card_slot,
available_line,normal_line,except_line,device_ip,device_mac) values ($1,$2,$3,$4,$5,$6,$7,$8,$9); `
rand.Seed(time.Now().UnixNano())
for i := 0; i < 100; i++ {
var (
rebot_state int
rebot_name_alias string
)
if i%2 == 0 {
rebot_state = 1
rebot_name_alias = "外呼中心"
} else {
rebot_name_alias = "GOIP"
}

card_slot := rand.Intn(30) + 10
fmt.Println(card_slot)
except_line := rand.Intn(10)
fmt.Println(except_line)
normal_line := card_slot - except_line
fmt.Println(normal_line)
rebot_name := fmt.Sprintf("设备%d(%s)", i, rebot_name_alias)
fmt.Println(rebot_name)
ip := fmt.Sprintf("%d.%d.%d.%d", rand.Intn(255), rand.Intn(255), rand.Intn(255), rand.Intn(255))
fmt.Println(ip)
// 随机mac
mac := GenerateMac().String()
// rebot_sign
rebot_sign := fmt.Sprintf("22948475%d", i)

fmt.Println(mac)
ret, err := postg.DBConnPostGresql().Exec(instr,
rebot_name, rebot_sign, rebot_state, card_slot, card_slot, normal_line, except_line, ip, mac)
if err != nil {
fmt.Printf("inster error:%v\n", err)
return
}
rf, _ := ret.RowsAffected()
fmt.Println(rf)
}

}
func GenerateMac() net.HardwareAddr {
buf := make([]byte, 6)
var mac net.HardwareAddr

_, err := rand.Read(buf)
if err != nil {
}

// Set the local bit
//buf[0] = buf[0] | 2 做按位与运算
buf[0] |= 2

mac = append(mac, buf[0], buf[1], buf[2], buf[3], buf[4], buf[5])

return mac
}
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
package main

import (
"fmt"
"math/rand"
"smartphone/db/postg"
"time"
)

func main() {
instr := `insert into aicall.t_rebot_dir_detail (rebot_id,line_name,line_state,except_cause) values ($1,$2,$3,$4); `
query_sql := `
select id from aicall.t_rebot_dir
`
rand.Seed(time.Now().UnixNano())
rows, err := postg.DBConnPostGresql().Query(query_sql)
if err != nil {
fmt.Println(err)
}
//var allid []int
for rows.Next() {
var id int
rows.Scan(&id)
//allid = append(allid, id)
scope := rand.Intn(11) + 2
for i := 0; i < scope; i++ {
line_name := fmt.Sprintf("线路%d", i)
var line_state int
var except_cause string
if i%2 == 0 {
line_state = 1
} else if i == 7 {
except_cause = "其他故障"
} else {
except_cause = "欠费"
}
ret, err := postg.DBConnPostGresql().Exec(instr, id, line_name, line_state, except_cause)
if err != nil {
fmt.Println(err)
}
rf, _ := ret.RowsAffected()
fmt.Println(rf)

}
}
//fmt.Println(allid)

}

360EntSecGroup-Skylar/excelize

安装

1
go get github.com/360EntSecGroup-Skylar/excelize

Gin 中导出 Excel

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
type ExportField struct {
phone_num sql.NullInt64 `json:"phone_num"`
intent_level sql.NullInt64
call_result sql.NullInt64
call_start_time sql.NullString
call_end_time sql.NullString
talk_duration sql.NullInt64
r_time sql.NullString
calling_number sql.NullString
hang_up_note sql.NullString
}
func main() {
engine := gin.Default()
engine.GET("/export", func(c *gin.Context) {
head := []string{"序号","预警意向", "号码", "完成状态", "开始时间", "结束时间", "通话时长", "通话状态", "主叫号码", "上传时间", "挂机短信"}
query_sql := `
SELECT
phone_num,
intent_level,
call_result,
to_char(
call_start_time,
'YYYY-MM-DD hh24:mi:ss'
) AS call_start_time,
to_char(
call_end_time,
'YYYY-MM-DD hh24:mi:ss'
) AS call_end_time,
talk_duration,
to_char(
r_time,
'YYYY-MM-DD hh24:mi:ss'
) AS r_time,
calling_number, hang_up_note
FROM
aicall.sbc_task_list`
rows, err := postgresql.DBConnPostGresql().Query(query_sql)
if err != nil {
fmt.Println(err)

}
var alldata [][]interface{}
var num int
for rows.Next(){
var export ExportField
err := rows.Scan(&export.phone_num, &export.intent_level, &export.call_result, &export.call_start_time,
&export.call_end_time, &export.talk_duration, &export.r_time, &export.calling_number, &export.hang_up_note)
if err != nil {
fmt.Println(err)
}
data := []interface{}{
num,
export.intent_level.Int64,
export.phone_num.Int64,
export.call_result.Int64,
export.call_start_time.String,
export.call_end_time.String,
export.talk_duration.Int64,
export.call_result.Int64,
export.calling_number.String,
export.r_time.String,
export.hang_up_note.String,
}
alldata = append(alldata, data)
}
filename := "task" + "_" + time.Now().Format("20060102150405") + ".xlsx"
Export(c, head, alldata, filename)

})
engine.Run(":9900")
}

func Export(c *gin.Context, head []string, body [][]interface{}, filename string) {
xlsx := excelize.NewFile()
xlsx.SetSheetRow("Sheet1", "A1", &head)
for index, rowData := range body{
xlsx.SetSheetRow("Sheet1", "A" + strconv.Itoa(index + 2), &rowData) // SetSheetRow:设置一行数据 SetCellValue:设置一个数据
}
c.Header("Content-Type", "application/octet-stream")
c.Header("Content-Disposition", "attachment; filename=" + filename)
c.Header("Content-Transfer-Encoding", "binary")
_ = xlsx.Write(c.Writer)

}

HBase伪分布式环境部署

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
hadoop@hadoop001:~ $ cd ~/softeware
hadoop@hadoop001:~/software $ tar -zxvf hbase-1.2.4-bin.tar.gz –C ../app
hadoop@hadoop001:~/software $ cd ../app
hadoop@hadoop001:~/app $ cd hbase-1.2.4/conf
hadoop@hadoop001:~/app/hbase-1.2.4/conf $ cp ~/app/hadoop-2.7.3/etc/hadoop
/hdfs-site.xml .
hadoop@hadoop001:~/app/hbase-1.2.4/conf $ cp ~/app/hadoop-2.7.3/etc/hadoop
/core-site.xml .
hadoop@hadoop001:~/app/hbase-1.2.4/conf $ vi hbase-env.sh
export JAVA_HOME=~/app/jdk1.8.0_161
# export HBASE_MASTER_OPTS=”$HBASE_MASTER_OPTS –XX:PermSize=128m –XX:MaxPermSize
=128m”
# export HBASE_REGIONSERVER_OPTS=”$HBASE_REGIONSERVER_OPTS –XX:PermSize=128m –XX:
MaxPermSize=128m”
hadoop@hadoop001:~/app/hbase-1.2.4/conf $ vi hbase-site.xml
<configuration>
<property>
<name>hbase.root.dir </name>
<value>hdfs://localhost:9000/hbase</value>
</property>
<property>
<name>hbase.zookeeper.property.dataDir</name>
<value>/home/hadoop/hadoop_data/zookeeper </value>
</property>
<property>
<name>hbase.cluster.distributed</name>
<value>true</value>
</property>
</configuration>
hadoop@hadoop001:~/app/hbase-1.2.4/conf $ cd ../bin
hadoop@hadoop001:~/app/hbase-1.2.4/bin $ ./start-hbase.sh
hadoop@hadoop001:~/app/hbase-1.2.4/bin $ jps
13792 HQuorumPeer
13968 HRegionServer
13864 HMaster
12156 NameNode
12556 SecondaryNameNode
14317 Jps
12335 DataNode
hadoop@hadoop001:~/app/hbase-1.2.4/bin $ ./hbase shell
hbase(main):001:0> status
1 active master, 0 backup master, 1 servers, 0 dead, 2.0000 average load
hadoop@hadoop001:~/app/hbase-1.2.4/bin $ cd ~/app/hadoop-2.7.3/bin
hadoop@hadoop001: ~/app/hadoop-2.7.3/bin $ ./hdfs dfs –ls /
Found 2 items
drwxr-xr-x - hadoop supergroup 0 2018-07-15 17:23 /hbase
drwxr-xr-x - hadoop supergroup 0 2018-07-15 17:08 /test

HBase简介

  • HBase是一个分布式的、面向列的开源Nosql数据库

  • 来源于googlBigtable

    HBase在Hadoop之上提供了类似于Bigtable的能力(是基于Hadoop的HDFS进行存储)

  • HBase不同于一般的关系数据库,它适合非结构化数据存储

  • Bigtable是什么

    Bigtable是压缩的、高性能的、高可扩展性的、基于Google GFS文件系统的数据库

    用于存储大规模的结构化数据

    在扩展性和性能方面有很大的优势

  • 什么是面向列的数据库

    即列式数据库,就是把每一列中的数据值放在一起进行存储

    对应的就是行式数据库(常见的有关系型数据库):把每一行的数据放在一起存储,存储完了之后就存储下一行的数据

为什么HBase适合非结构化数据存储

1
2
3
4
5
6
7
8
为什么HBase适合非结构化数据存储
结构化数据与非结构化数据的概念

结构化数据:可以用二维表格形式存储的数据

非结构化数据:图片、文档这些可以认为为非结构化数据

我们可以将这些非结构化数据以二进制的方式存到HBase里面,这样无论是存储还是查询都是比较方便快捷的,而且很容易进行扩展

HBase与HDFS

  • HBase建立在Hadoop文件系统之上,利用了Hadoop的文件系统的容错能力
  • HBase提供了对数据的随机实时读/写访问功能
  • HBase内部使用哈希表,并存储索引,可将在HDFS文件中的数据进行快速查找

HBase使用场景

  • 瞬间写入量很大,常用数据库不好支撑或需要很高成本支撑的场景
  • 数据需要长久保存,且量会持久增长到比较大的场景
  • HBase不适用于有join,多级索引,表关系复杂的数据模型

CAP定理

CAP定理就是对于一个分布式计算系统不可能同时满足以下三点:

  • 一致性(所有节点在同一时间具有相同的数据)
  • 可用性(保证每个请求不管成功或者失败都有响应,但不保证获取的数据为正确的数据)
  • 分区容错性(系统中任意信息的丢失或失败不会影响系统的继续运作,系统如果不能在某一个时限内达成数据一致性,就必须在上面两个操作之间做出选择)

那么对于分布式数据系统,分区容错性是最基本的要求,否则就失去了存在的意义,因此需要在一致性和可用性上做出取舍:在很多情况下会牺牲一致性从而来换取可用性,比如:Cassandra就是AP类型的,当然,牺牲一致性,只是要求不像关系型数据库一样,要求强一致性,而是要求系统能达到最终一致性。而HBase属于CP类型的,是强一致性的,它的每一行有regionserver、rowkey、版本标签等来组合,从而保证行的一致性。

ACID定义

数据库事务正确执行的4个基本要素

  • 原子性(一个事务要么全部执行,要么全部不执行。如果执行过程中发生了错误,系统会回滚到最初的状态)
  • 一致性(事务的运行,不会改变数据库中数据的一致性)
  • 隔离性(2个以上的事务在执行的过程中,不会出现交错执行的状态(因为这样的话可能会导致数据的不一致))
  • 持久性(一个事务执行成功之后,该事务对数据库的更改,要持久性的保存在数据库当中)

一个支持事务的数据库系统中必须得有这4个特性,否则在事务的过程当中就无法保证事务的正确性

HBase作为一个NoSQL数据库,为了性能不支持严格的ACID,只支持到单个的行

HBase概念

  • NameSpace:可以把NameSpace理解为RDBMS的“数据库”,1个NameSpace包含一组表
  • Table:表名必须是能用在文件路径里的合法名字。这样做是因为HBase的表是映射成HDFS上相应的文件的,因此表名必须是合法的路径
  • Row:在表里面,每一行代表着一个数据对象,每一行都是以一个Row Key来进行唯一标识的,Row Key并没有什么特定的数据类型,以二进制的字节来存储
  • Column:HBase的列由Column family和Column qualifier组成,由冒号(:)进行间隔;比如 family:qualifier
  • RowKey:可以唯一标识一行记录,不可被改变;改变的唯一方式是删除这个RowKey,再重新插入
  • Column Family:是一些Column的集合,1个Column Family所包含的所有的Column成员是有着相同的前缀;在物理上1个Column Family所有的成员是存储在一起的,存储的优化都是针对Column Family级别的;这就意味着1个- Column Family的成员都是用相同的方式进行访问的;在定义HBase表的时候需要提前设置好列族,表中所有的列都需要组织在列族里面;列族一旦定义好之后,就不能轻易的更改了,因为它会影响到HBase真实的物理存储结构
  • Column Qualifier:列族中的数据通过列标识(Column Qualifier)来进行映射,可以理解为一个键值对,Column Qualifier就是key
  • Cell:每一个RowKey、Column Family、Column Qualifier共同组成的一个单元;存储在Cell里面就是我们想要保存的数据;Cell存储的数据没有特定的数据类型,以二进制字节来进行存储
  • Timestamp:每个值都会有一个timestamp,作为该值特定版本的标识符;默认HBase中每次插入数据的时候,都会用timestamp来进行版本标识;读取数据时,如果这个时间戳没有被指定,就默认返回最新的数据;写入数据时,如果没有设置时间戳,默认使用当前的时间戳;每一个列族的数据的版本都由HBase单独维护;默认情况下,HBase会保留3个版本的数据

HBase与传统关系型数据库的区别

查询的时候只能通过API去查询,不支持SQL,所以也默认支持通过RowKey去进行查询
两者的数据排布方式有很大的区别:

  • 传统的数据库就是行列的组织
  • 对于HBase这种NoSQL数据库我们可以理解为稀疏的多维的map

每一行都是一个文件,每一列都是相关的属性

关系型数据库:

HBase数据模型

HBase基础架构

HBase依托于HDFS之上;整体上又划分为HMaster和RegionServer,在上层通过Java API提供查询的功能;通过Zookeeper进行管理

通过上图我们可以发现,HBase工作的三大模块:

  1. HMaster

    • HMaster是HBase主/从集群架构中的中央节点
    • HMaster用于协调多个RegionServer、检测各个RegionServer的状态、并且平衡各个RegionServer之间的负载、同时还负责分配region到RegionServer
    • region:region是HBase中存储的最小的单元、是HBase表格的基本单位
    • HMaster维护表和Region的元数据,不参与数据的输入/输出过程
    • HBase本身是支持HA的,也就是说同时可以有多个HMaster进行运行,但是只有1个处于active状态;如果处于active的节点失效了,挂掉了,其它的HMaster节点就会选举出一个active节点来接管整个HBase集群
    • HMaster只负责各种协调工作( 其实就是打杂) , 比如建表、 删表、移动Region、 合并等操作
  2. RegionServer

    • 维护HMaster分配给他的region,处理堆这些region的io请求。当用户需要读取数据的时候会连接到对应的RegionServer,从相关的region中去获取数据
    • 负责切分正在运行过程中变的过大的region,从而保证查询的效率
  3. Zookeeer

    • Zookeeper是HBase HA的解决方案,是整个集群的协调器

      通过Zookeeper保证了至少有一个HMaster处于active状态

      HMaster并不直接参与数据的读写操作,当我们使用HBase的API的时候,当我们想用HBase的API去读取数据的时候,我们并不需要知道HMaster的地址、也不需要知道RegionServer的地址,我们只需要知Zookeeper集群的地址就可以了

    • HMaster启动将系统加载到Zookeeper

      Zookeeper保存了HBase集群region的信息、meta的信息等等

    • 维护着RegionServer的状态信息,知道哪些数据需要从哪些RegionServer去读取

结构化数据:可以用二维表格形式存储的数据

非结构化数据:图片、文档这些可以认为为非结构化数据

我们可以将这些非结构化数据以二进制的方式存到HBase里面,这样无论是存储还是查询都是比较方便快捷的,而且很容易进行扩展

LEO(log end offset) 和HW(Hight watemark)概念 简单解释

1
一个分区有3个副本,一个leader,2个follower。producer向leader写了10条消息,follower1从leader处拷贝了5条消息,follower2从leader处拷贝了3条消息,那么leader副本的LEO就是10,HW=3;follower1副本的LEO是5