maksim 发布的文章

0x00 什么是 IoC

IoC (控制反转)并不是一个新鲜的概念,如果之前有过 Java 经验,那么肯定不会对 IoC 陌生,而 PHP 近几年的框架其实也开始出现了大量的容器应用,在这里需要注意 IoC 和 IoC 容器是两个概念,不要混淆

IoC 是编程思想,而 IoC 容器是帮助我们进行 IoC 的工具,我们通过控制反转把对象之间的调用过程交给 IOC 容器来完成,而最终实现业务代码的解耦。

在 Golang 中 IOC 容器的使用时候,我们要根据我们的具体情况还进行分析是否要使用 IOC

  • 如果你的项目对于性能要求比较高,最好就不要使用 IOC 容器了,因为在 Golang 中实现 IoC 要使用到反射,而反射使用的 空间,如果大量使用反射就会对 Golang 的 GC 造成一定的压力。
  • 要是你的项目对于性能要求不高,追求的是业务的封装性和可维护性,那么就可以考虑使用 IoC 容器。

如果你不了解什么是 IoC 也没有关系,下面会带大家简单的了解一下什么是 IOC 容器。

下面来简单解释一下什么是 IoC。

img

假设现在有两个 Service:UserService 和 OrderService。

  • UserService: 用户服务, GetUserInfo 获取用户信息
  • OrderService: 订单服务, GetOrderInfo 获取订单信息。

这两个 Service 中的方法传递的参数也都是用户 id,现在以代码的形式来模拟一下这个场景。

初始目录如下:

.
├── go.mod
├── main.go
└── svc           #service 目录
    ├── OrderService.go
    └── UserService.go

UserService代码如下:

package svc

import "fmt"

type UserService struct {
}

func NewUserService() *UserService {
    return &UserService{}
}

func (this *UserService) GetUserInfo(uid int) {
    fmt.Println("获取用户 id = ", uid, "的详细信息")
}

OrderService

package svc

import "fmt"

type OrderService struct {
}

func NewOrderService() *OrderService {
    return &OrderService{}
}

func (this *OrderService) GetOrderInfo(uid int) {
    fmt.Println("获取用户 id = ", uid, "的订单信息")
}

基础的 Service 已经封装好了,接下来进行调用。

package main

import "study-go/svc"

func main() {
    uid := 123
    
    orderSvc := svc.NewOrderService()
    userSvc := svc.NewUserService()

    //获取用户详细
    userSvc.GetUserInfo(uid)
    orderSvc.GetOrderInfo(uid)
}

如果代码写到这个程度是否就可以结束了,答案是可以的,并不是很 Low,在 Golang 中并不是要写成和 Java 一样,有大量的设计模式和类与类之间的嵌套。

如果这个时候时候你的领导跟你说要将获取订单信息的方法放到 UserService 里面,你应该怎么做?

package svc

import "fmt"

type UserService struct {
}

func NewUserService() *UserService {
    return &UserService{}
}

func (this *UserService) GetUserInfo(uid int) {
    fmt.Println("获取用户 id = ", uid, "的详细信息")
}

func (this *UserService) GetOrderInfo(uid int) {
    NewOrderService().GetOrderInfo(uid)
}

你完全可以这样写,因为获取用户订单信息是和用户相关的,没有必要放在两个 Service 里面,这个时候我们可以视作 UserService 依赖于 OrderService ,而我们处理的方式是主动处理依赖(在方法内初始化了 NewOrderService),也就是将依赖硬编码到我们的代码中,也叫做控制正转

调用的时候就变成了这个样子:

package main

import "study-go/svc"

func main() {
    uid := 123
    userSvc := svc.NewUserService()

    //获取用户详细
    userSvc.GetUserInfo(uid)
    userSvc.GetOrderInfo(uid)
}

而我们要讲的是依赖反转是被动的,也就是说在我们的 Service 代码里面不会有任何主动初始化 OrderService 的代码,我们需要将我们代码更改一下:

package svc

import "fmt"

type UserService struct {
    orderSvc *OrderService
}

func NewUserService(order *OrderService) *UserService {
    return &UserService{orderSvc: order}
}

func (this *UserService) GetUserInfo(uid int) {
    fmt.Println("获取用户 id = ", uid, "的详细信息")
}

func (this *UserService) GetOrderInfo(uid int) {
    this.orderSvc.GetOrderInfo(uid)
}

观察上方代码的变化:

  1. 我们首先在 UserService 结构体定时以的时候定义了一个新的属性 orderSvc;
  2. 改造了构造方法,在初始化 UserService 的时候,需要给 UserService 传递一个 OrderService 进来;
  3. 在 GetOrderInfo 方法中,因为在构造 UserService 的时候已经传递来了一个 OrderService ,所以可以通过属性的方法进行调用 orderSvc.GetOrderInfo;

那么在调用的时候就变成了这个样子:

package main

import "study-go/svc"

func main() {
    uid := 123
    
    userSvc := svc.NewUserService(svc.NewOrderService())

    //获取用户详细
    userSvc.GetUserInfo(uid)
    userSvc.GetOrderInfo(uid)
}

这个时候,UserService 就被动的 接收了依赖,这就叫做控制反转,也就是 IOC。

这个时候其实是有一些问题的,如果 UserService 有十七八个依赖,我们要怎么样进行处理?这个时候就需要一个工具来帮助我们统一进行管理那就是 IOC 容器,这一部分内容将在下一小节进行讲解。

0x01 利用 Map 存储 Service

在上一小节讲过 IoC 容器是用来帮助我们处理 IoC 的工具,所以我们需要保存两个 Service 之间的依赖关系。

在编写容器代码前,我们现将 OrderService 改造一下,在其中加上 version。

type OrderService struct {
    version string
}

func NewOrderService() *OrderService {
    return &OrderService{version: "1"}
}

我们通过 version 来判断后面我们放到容器中的 OrderService 和取出来的是否一致。

我们先来实现一个基础版本的容器,使用 Map 来存储依赖关系:

package injector

import "reflect"

type BeanMapper map[reflect.Type]reflect.Value

func (this BeanMapper) add(bean interface{}) {
    t := reflect.TypeOf(bean)
    if t.Kind() != reflect.Ptr {
        panic("require ptr object")
    }
    this[t] = reflect.ValueOf(bean)
}

在 Map 中 key 是 reflect.Type, 而 value 是 reflect.Value。

我们来看 add 方法,其实就是将依赖关系存储到 map 中,在使用的时候再从 map 中取出来,在这里为了简化操作,我们目前值允许指针被加入到 map 中。

package injector

var BeanFactory *BeanFactoryImpl

func init() {
    BeanFactory = NewBeanFactory()
}

type BeanFactoryImpl struct {
    BeanMapper BeanMapper
}

func (this *BeanFactoryImpl) Set(vList ...interface{}) {
    if vList == nil || len(vList) == 0 {
        return
    }
    for _, v := range vList {
        this.BeanMapper.add(v)
    }
}

func NewBeanFactory() *BeanFactoryImpl {
    return &BeanFactoryImpl{BeanMapper: make(BeanMapper)}
}

现在创建和添加已经写好了,在这里,我们使用工厂模式来创建容器,init 函数是初始化函数,他在 main 函数执行前执行,用来初始化资源或者提前创建对象。

现在我们再给容器添加获取代码,首先给 mapper 添加 get 方法,很简单,其实就是依靠 map 的获取操作。

func (this BeanMapper) get(bean interface{}) reflect.Value {
    t := reflect.TypeOf(bean)

    if v, ok := this[t]; ok {
        return v
    }
    return reflect.Value{}
}

然后再添加 Factory 的获取方法。

func (this *BeanFactoryImpl) Get(v interface{}) interface{} {
    if v == nil {
        return nil
    }
    bean := this.BeanMapper.get(v)
    if bean.IsValid() {
        return bean.Interface()
    }
    return nil
}

这样一来我们就完成了容器的添加依赖和获取依赖,我们来实践一下。

package main

import (
    "fmt"
    "study-go/injector"
    "study-go/svc"
)

func main() {
    injector.BeanFactory.Set(svc.NewOrderService())
    orderSvc := injector.BeanFactory.Get((*svc.OrderService)(nil))
    fmt.Println(orderSvc)
}

执行一下:

&{2}

输出的时候将 OrderService 中的 version 给打印了出来,这证明我们可以从容器中取到放进去的值。

0x02 处理依赖注入

我们的 UserService 依赖了 OrderService,我们之前使用手动编码来处理依赖关系,现在我们有了容器,现在我们来使用容器来自动注入。

做法其实有很多方案,在Java 可以使用注解, PHP 中使用注释来模拟注解,而在 Golang 中的处理方式稍有不同,因为在 Golang 中并没有对注解的原生支持,所以我们需要使用 Tag 来实现,Tag 是用于标识结构体字段的额外信息,标准库 reflect 包提供了操作 Tag 的方法[1]

package svc

import "fmt"

type UserService struct {
    OrderSvc *OrderService `inject:"-"`
}

func NewUserService(order *OrderService) *UserService {
    return &UserService{OrderSvc: order}
}

func (this *UserService) GetUserInfo(uid int) {
    fmt.Println("获取用户 id = ", uid, "的详细信息")
}

func (this *UserService) GetOrderInfo(uid int) {
    this.OrderSvc.GetOrderInfo(uid)
}

首先我们将 Uservice 中的 orderSvc 改为公开的属性,然后在其后面增加 inject tag,其中 "-" 其实写不写都无所谓,但是因为我们在业务开发的时候,可能还会有 json tag ,所以我们在这里约定 inject:"-" 就等于我要使用依赖注入。

接下来,我们在 BeanFactory 中实现 Apply 方法,用来处理依赖注入。

func (this *BeanFactoryImpl) Apply(bean interface{}) {

    if bean == nil {
        return
    }

    v := reflect.ValueOf(bean)

    if v.Kind() == reflect.Ptr {
        v = v.Elem()
    }

    if v.Kind() != reflect.Struct {
        return
    }

    for i := 0; i < v.NumField(); i++ {
        field := v.Type().Field(i)
        if v.Field(i).CanSet() && field.Tag.Get("inject") != "" {
            if getV := this.Get(field.Type); getV != nil {
                v.Field(i).Set(reflect.ValueOf(getV))
            }
        }
    }
}

首先在代码中判断了是否为结构体,并且从 map 中获取已经实例化好的 Service,在这里我们还需要改造一下 Map 的 get 方法,因为这个时候传递的 field.Type 在 get 方法中会报错,我们需要兼容一下这个类型,在原有代码中增加一下断言。

func (this BeanMapper) get(bean interface{}) reflect.Value {
    var t reflect.Type
    //断言一下 bean
    if bt, ok := bean.(reflect.Type); ok {
        t = bt
    } else {
        t = reflect.TypeOf(bean)
    }

    if v, ok := this[t]; ok {
        return v
    }
    return reflect.Value{}
}

现在让我们来试一下新的成果。

func main() {
    injector.BeanFactory.Set(svc.NewOrderService())

    userService := svc.NewUserService()
    injector.BeanFactory.Apply(userService)
    
    fmt.Println(userService.OrderSvc)
}

现在我们的依赖管理采用 Apply 方法进行管理,Apply 方法自动在容器中寻找其依赖项,然后注入到 UserService 中,运行程序显示结果如下:

&{2}

0x03 动态注入 & Tag 支持表达式

在这个阶段,我们要是实现一个能够基于 Tag 表达式能够动态注入的功能,因为如果项目不复杂,只有几个 Service,那么我们还可以进行手工的将 Service 添加到容器中,那么如果现在随着业务的增加,Service 的数量增加到 20 个,甚至 100 个,我们就需要在 BeanFactory.Set() 方法的参数中写大量的代码来完成容器的初始化。

这么做很显然是不够好的,以为一个完整的容器是需要能够动态注入的,我们只需要基于简单的配置,就可以完成动态注入。

首先我们先创建一个新的文件 ServiceConfig.go 我们将在 ServiceConfig Struct 中来管理我们依赖的创建。

package config

import "study-go/svc"

type ServiceConfig struct {
}

func NewServiceConfig() *ServiceConfig {
    return &ServiceConfig{}
}

func (this *ServiceConfig) OrderService() *svc.OrderService {
    return svc.NewOrderService()
}

以后业务中所有依赖实例化我们都存放在这个文件中,这种做法其实是类似于 Java Spring 的做法。

上面刚才说了,我们要实现一个能够基于 Tag 动态注入的功能,目前字段 Tag 中给的约定是 "-" ,用来直接去到容器中被实例化好的 Service,我们可以更近一步,将约定在增加一份,也就是所谓的表达式,例如 inject:"ServiceConfig.OrderService()

根据这个表达式可以清晰的看出,该字段依赖于 ServiceConfig.OrderService,那么只需要 BeanFactory 代替我们去把这个值拿到就可以了。

但是目前 BeanFactory 目前只有一个 BeanMap 来存储被实例化好的 struct ,并不支持表达式,所以我们需要先增加一个字段。

type BeanFactoryImpl struct {
    BeanMapper BeanMapper             // 存储依赖
    ExprMap    map[string]interface{} // 新增的字段,利用表达式的方式存储依赖 
}

新增的 ExprMap 中 key 存储的就是我们需要支持的表达式,而他的 value 其实就是我们的实例化好的 struct。

接下来,我们先引入一个新的库 github.com/shenyisyn/goft-expr ,这个库的作用是使用表达式来执行函数或者 struct,我们来修改 BeanFactory 中 的 Apply 方法,以支持新的表达式。

func (this *BeanFactoryImpl) Apply(bean interface{}) {
    if bean == nil {
        return
    }

    v := reflect.ValueOf(bean)

    if v.Kind() == reflect.Ptr {
        v = v.Elem()
    }

    if v.Kind() != reflect.Struct {
        return
    }

    for i := 0; i < v.NumField(); i++ {
        field := v.Type().Field(i)
        if v.Field(i).CanSet() && field.Tag.Get("inject") != "" {

            // 兼容老的方式 注入
            if field.Tag.Get("inject") == "-" {
                if getV := this.Get(field.Type); getV != nil {
                    v.Field(i).Set(reflect.ValueOf(getV))
                }
            } else {
                fmt.Println("使用了表达式")
                ret := expr.BeanExpr(field.Tag.Get("inject"), this.ExprMap)
                if ret != nil && !ret.IsEmpty() {
                    v.Field(i).Set(reflect.ValueOf(ret[0]))
                }
            }

        }
    }
}

我们来看这一段代码:

// 兼容老的方式 注入
if field.Tag.Get("inject") == "-" {
    ...
} else {
        ret := expr.BeanExpr(field.Tag.Get("inject"), this.ExprMap)
        if ret != nil && !ret.IsEmpty() {
        v.Field(i).Set(reflect.ValueOf(ret[0]))
}

首先是判断了 inject 是否等于 - 符号,这样是为了兼容老的表达式,也就是默认去读取依赖的 Service,然后在下方,我们去根据表达式取出了已经在容器中准备好的 Service。

这一切都已经准备好了,我们来先试验一下根据表达式来进行注入,先修改 UserService:

type UserService struct {
    OrderSvc *OrderService `inject:"ServiceConfig.OrderService()"`
}

然后是 main.go

func main() {
  srvConfig := config.NewServiceConfig()
  
    injector.BeanFactory.ExprMap = map[string]interface{}{
        "ServiceConfig": srvConfig,
    }
  
    injector.BeanFactory.Set(srvConfig)

    userService := svc.NewUserService()
    injector.BeanFactory.Apply(userService)

    fmt.Println(userService.OrderSvc)
}

接下来执行结果如下:

➜  study-go go run main.go 
&{2}

接下来,我们继续优化我们的代码,能够让代码真正的动态注入,在上面,我们需要手动配置 ExprMap ,但是其实我们同样可以使用反射,来自动完成这一配置,这一次我们先来看 main.go

func main() {
  srvConfig := config.NewServiceConfig()
  
    injector.BeanFactory.Set(srvConfig)
  
    userService := svc.NewUserService()
    injector.BeanFactory.Apply(userService)

    fmt.Println(userService.OrderSvc)
}

BeanFactory 新增 Config 方法:

func (this *BeanFactoryImpl) Config(cfgs ...interface{}) {
    for _, cfg := range cfgs {
        t := reflect.TypeOf(cfg)
        if t.Kind() != reflect.Pointer {
            panic("required ptr object!")
        }
        this.Set(cfg) //把 cfg 本身也加入到 bean 中
        t = t.Elem()
        this.ExprMap[t.Name()] = cfg  // 自动设置 ExprMap 
        v := reflect.ValueOf(cfg).Elem()
        for i := 0; i < t.NumMethod(); i++ {
            method := v.Method(i)
            callRet := method.Call(nil)
            if callRet != nil && len(callRet) == 1 {
                this.Set(callRet[0].Interface())
            }
        }
    }
}

这段代码其实很好理解:

  1. 判断是否为指针,因为这里我们需要的是被实例化好的 Strcut
  2. 然后将 ServiceConfig 也放入到 BeanMap 当中
  3. 然后设置 ExprMap
  4. 最后序列化 ServiceConfig 中的所有 NewXXX 函数

这样一来动态注入就已经完成了

0x04 支持接口注入

如果要让我们的容器支持接口,其实也非常简单,只需要修改 BeanMap 中的 Get 方法即可:

func (this BeanMapper) get(bean interface{}) reflect.Value {
  ...
    if v, ok := this[t]; ok {
        return v
    }
    //如果没有找到那就去看看接口
    for k, v := range this {
        if k.Implements(t) {
            return v
        }
    }
    return reflect.Value{}
}

0x05 处理循环依赖

当我们的 Service 存在多级嵌套的时候,按照现在的代码逻辑是无法去自动完成下一级的自动注入的,我们仅需要在apply 完成的时候,在对其依赖进行递归调用就可以

// 兼容老的方式 注入
if field.Tag.Get("inject") == "-" {
    if getV := this.Get(field.Type); getV != nil {
          v.Field(i).Set(reflect.ValueOf(getV))
          this.Apply(getV)
    }
} else {
        ret := expr.BeanExpr(field.Tag.Get("inject"), this.ExprMap)
        if ret != nil && !ret.IsEmpty() {
             retV := ret[0]
             v.Field(i).Set(reflect.ValueOf(retV))
             this.Apply(retV)
        }
}

0x06 IOC 容器还需要做的事情

到目前为止,我们的 基础版的 IOC 就已经完成了,但是目前还有两个问题没有解决:

  1. 单例和多例,目前这个 IOC 容器,每一次在执行的时候其实都是会调用 NewXXX 实例化出来一个依赖,所以需要能够支持单例或者多例
  2. ServiceConfig 目前还不支持参数实例化

后面如果有时间的话,我会将这块补上。

0x07 结尾

在这里首先要感谢程序员在囧途的沈逸的课程,这篇文章其实就是以 Golang 手撸 IOC 容器为基础编写的,大部分都是他讲课的内容,我在其基础上进行了一些编辑,并且引用了其他的一些文章内容来充实整篇文章,目前与课程不同的是没有去实现单例和多例。

Ref

在导入数据之前,我这里准备了一个book 表,在表中存储了一万条数据。

select * from books limit 1 \G;
*************************** 1. row ***************************
book_id: 220
book_name:  Java项目实战教程+Java程序设计与项目实训教程
book_intr:
book_price1: 72.00
book_price2: 72.50
book_author: 姜华刘闯
book_press: 清华大学出版社
book_date:  2012-09-01
book_kind_str: PHP
book_kind: 2
dd_id: 1328395676
1 row in set (0.001 sec)

我们现在想办法,将这些数据导入到 es 中,在这里我们是用 grom 来完成数据的读取,我们先导入 50 条数据。导入数据 之前我们应该先创建 mapping。

Untitled

先简单说一下 key 和 keyword 的区别,如果需要全文检索那么就用 text ,如果是要精准匹配那么就使用 keyword。

在上一节中我们说过id 这个属性,一般来说我们可以灌入自己的id,不适用 es 自动生成,要不然取数据的时候会比较麻烦。

插入数据的步骤

利用 grom 获取数据

定义db 工具类

package db

import (
    "github.com/jinzhu/gorm"
    _ "github.com/jinzhu/gorm/dialects/mysql"
    "log"
)

var db *gorm.DB

func init() {
    var err error
    db, err = gorm.Open("mysql",
        "root:123123@tcp(localhost:3306)/books?charset=utf8mb4&parseTime=True&loc=Local")
    if err != nil {
        log.Fatal(err)
    }
    db.SingularTable(true)
    db.DB().SetMaxIdleConns(5)
    db.DB().SetMaxOpenConns(10)

}
func  GetDB() *gorm.DB {
    return db
}

定义 model

package model

type Books struct {
    BookID     int     `gorm:"column:book_id;AUTO_INCREMENT;PRIMARY_KEY"`
    BookName   string  `gorm:"column:book_name;type:varchar(50)"`
    BookIntr   string  `gorm:"column:book_intr;type:text"`
    BookPrice1 float64 `gorm:"column:book_price1;type:decimal"`
    BookPrice2 float64 `gorm:"column:book_price2;type:decimal"`
    BookAuthor string  `gorm:"column:book_author;type:varchar(50)"`
    BookPress  string  `gorm:"column:book_press;type:varchar(50)"`
    BookDate   string  `gorm:"column:book_date;type:varchar(50)"`
    BookKind   int     `gorm:"column:book_kind;type:int"`
}
type BookList []*Books

获取数据

package main

import (
    "es/db"
    "es/model"
    "fmt"
)

func main() {
    page := 1
    pageSize := 50
    for {
        bookList := model.BookList{}
        dbConn := db.GetDB().Order("book_id desc").Limit(pageSize).Offset((page - 1) * page).Find(&bookList)
        if dbConn.Error != nil || len(bookList) == 0 {
            break
        }
        fmt.Println(bookList[1])
        break
    }
}

# 
# &{19551  C语言程序设计教程(第2版) 导语_点评_推荐词 27.2 32  湘潭大学出版社  8}

在 main 函数中我们用分页的方式获取数据,每一次获取 50 条。

es 插入数据

我们先将 esclient 抽出一个单独的包。

package es

import (
    "github.com/olivere/elastic/v7"
)

func GetEsClient() (*elastic.Client, error) {
    client, err := elastic.NewClient(
        elastic.SetURL("http://192.168.124.16:9200/"),
        elastic.SetSniff(false),
    )
    if err != nil {
        return nil, err
    }
    return client, nil
}

在之前插入我们使用的是写死的 json 字符串,在这里我们使用模型。

package main

import (
    "context"
    "es/db"
    "es/es"
    "es/model"
    "fmt"
    "log"
    "strconv"
)

func main() {
    page := 1
    pageSize := 50
    client, err := es.GetEsClient()
    if err != nil {
        log.Fatal(err)
    }
    for {
        bookList := model.BookList{}
        dbConn := db.GetDB().Order("book_id desc").Limit(pageSize).Offset((page - 1) * page).Find(&bookList)
        if dbConn.Error != nil || len(bookList) == 0 {
            break
        }
        fmt.Println(bookList[1])

        for _, book := range bookList {
            resp, err := client.Index().Index("books").Id(strconv.Itoa(book.BookID)).BodyJson(book).Do(context.Background())
            if err != nil {
                fmt.Println(err)
            } else {
                fmt.Println(resp)
            }
        }

        break
    }
}

接下来我们执行代码,后查看统计:

curl http://192.168.124.16:9200/books/_count
{"count":50,"_shards":{"total":1,"successful":1,"skipped":0,"failed":0}}

我们可以看到在 es 中已经有 50 条数据了。

$ curl http://192.168.124.16:9200/books/_search | python -m json.tool
{
    "_shards": {
        "failed": 0,
        "skipped": 0,
        "successful": 1,
        "total": 1
    },
    "hits": {
        "hits": [
            {
                "_id": "19552",
                "_index": "books",
                "_score": 1.0,
                "_source": {
                    "BookAuthor": "\u848b\u6e05\u660e",
                    "BookDate": " 2008-10-01",
                    "BookID": 19552,
                    "BookIntr": "\u5bfc\u8bed_\u70b9\u8bc4_\u63a8\u8350\u8bcd",
                    "BookKind": 8,
                    "BookName": " C\u8bed\u8a00\u7a0b\u5e8f\u8bbe\u8ba1\u5b9e\u9a8c\u6307\u5bfc\u4e0e\u4e60\u9898\u89e3\u7b54",
                    "BookPress": "\u4eba\u6c11\u90ae\u7535\u51fa\u7248\u793e",
                    "BookPrice1": 19,
                    "BookPrice2": 19
                },
                "_type": "_doc"
            },
....
}

/books/_search 如果没有接参数的话会默认显示 10 条数据。

在上一篇博文中,我们通过 curl 请求项 es 中添加了一条索引,提交数据的本质也是发送 http 请求。不过我们使用 curl 发送请求的时候没有提示,接下来我们使用 kibana 来提交数据。

目前大多数开发者应该都听说过 elk,e 就是es,其中的 k,就是 kibana ,一款为 es 设计的可视化工具,我们可以使用 kibana 来操作 es。

安装 kibana

docker pull blacktop/kibana:7.4

docker tag blacktop/kibnana:7.4 kb:7.4

版本必须和 es 保持一致。

启动 kibana

docker run --init -d --name kb -e elasticsearch.hosts="http://192.168.124.16:9200" \
 -p 5601:5601 kb:74

当启动成功后,我们访问 http://192.168.124.16:5601,点击 consolse。

console

在 kibana 中输入请求是有提示的:

代码提示

我们点击 play 按钮就会发送请求到 es 中。

play_button

这样我们就获得了在上节中设置的mapping。

用 GoSDK 向 ES 中插入第一条数据

接下来我们新增一个数据。

package main

import (
    "context"
    "fmt"
    "github.com/olivere/elastic/v7"
    "log"
)

func main() {
    client, err := elastic.NewClient(
        elastic.SetURL("http://192.168.124.16:9200/"),
        elastic.SetSniff(false),
    )
    if err != nil {
        log.Fatal(err)
    }
    ctx := context.Background()
    json := `{"news_title": "test1", "news_type":"php", "news_status":1}`
    data, err := client.Index().Index("news").Id("101").BodyString(json).Do(ctx)
    if err != nil {
        log.Fatal(data)
    }

    fmt.Println(data)
}
  • 在 es 中有 id 的概念,我们可以指定也可以自动生成,在这里我们指定了 id 为 101。
  • 我们用 BodyString 传入一个写死的 json 字符串,在真是项目中肯定是不会这么写!

接下来我们回到 kibana 中查看数据。

get

在 kibana 中可以查到我们的数据,证明写入成功了,其中 _source 存储的就是具体的值。

如果我们想要删除,只需要将 get 改为 DELETE 就可以了。

delete

successful:1 表示删除成功了,再去查询会发现 found: false,表示没有查询到数据。

get2

快速部署

这里为了方便直接使用 Docker,官方镜像是 CentOS 镜像,不够美丽,因此我们使用了第三方的镜像,体积比较小,适合我们在学习过程中快速部署。

docker pull blacktop/elasticsearch:7.4

下载好后修改 tag,不然太长了

docker tag blacktop/elasticsearch:7.4 es:74

运行 ES。

docker run  -d --name es -p 9200:9200 es:74

如果我们是在虚拟机中使用 docker 那么内存尽量要大了一点,至少

有 4G 的内存。

执行 curl 看一下容器是否启动成功:

$ curl [http://192.168.124.16:9200/](http://192.168.124.16:9200/)
{
"name" : "832e35c81ff4",
"cluster_name" : "elasticsearch",
"cluster_uuid" : "tfwkn9ZKQNO0PD0iNfqdwg",
"version" : {
"number" : "7.4.2",
"build_flavor" : "oss",
"build_type" : "tar",
"build_hash" : "2f90bbf7b93631e52bafb59b3b049cb44ec25e96",
"build_date" : "2019-10-28T20:40:44.881551Z",
"build_snapshot" : false,
"lucene_version" : "8.2.0",
"minimum_wire_compatibility_version" : "6.8.0",
"minimum_index_compatibility_version" : "6.0.0-beta1"
},
"tagline" : "You Know, for Search"
}

GolangSDK

Go 的 SDK我们也选择第三方的库,因为目前官方的库还没有第三方实用。

go get github.com/olivere/elastic/v7

mapping

mapping 映射类似于在数据库中定义表结构,表里里有哪些字段、字段是什么类型。没有mapping 也能够创建索引,但是正规的方式是自己来创建 mapping,而不是动态映射。

例如现在一个 MySQL 中有一个news 表,其中存储的是新闻,表结构如下:

字段 name备注
news_id新闻 id
news_title新闻标题
news_type新闻类型(目前为中文)
news_status0 代表下架 1 正常 2 不可修改

在向 es 中导入的时候,我们可以不创建这个表结构,直接向 es中写入数据,es 可以进行类型推断,但是有的时候会推测错误,一般来说我们得先创建表结构。

同时向 es 写入数据有两种模式:

  1. 直接调用 es 的 api
  2. 通过 logstat 导入数据

一般我们不会选择第一种,而是直接选择使用 logstat 导入。

在 es 中字段一旦增加就不能删除了,只能新增,所以在最开始我们尽量就设计好,要不更改的时候会比较麻烦。

我们现在来创建 mapping,es 为我们提供了接口,我们使用 curl 发送请求创建接口

$ curl --location --request PUT 'http://192.168.124.16:9200/news' \
> --header 'Content-Type: application/json' \
> --data-raw '{
>     "mappings" : {
>         "properties" : {
>             "news_title": {
>                 "type": "text"
>             },
>             "news_type": {
>                 "type" : "keyword"
>             },
>             "news_status": {
>                 "type": "byte"
>             }
>         }
>     }
> }'
{"acknowledged":true,"shards_acknowledged":true,"index":"news"}

/news 是我们的索引名称,—data-raw 中就是我们要创建的数据。

创建完成后我们通过 get 方式可以查看 mapping。

curl --location --request GET 'http://192.168.124.16:9200/news/_mapping'
{"news":{"mappings":{"properties":{"news_status":{"type":"byte"},"news_title":{"type":"text"},"news_type":{"type":"keyword"}}}}}%

Go 查看 Mapping

接下来,我们用 golang 实现获取 mapping,代码其实非常简单:

package main

import (
    "context"
    "fmt"
    "github.com/olivere/elastic/v7"
    "log"
)

func main() {
    client, err := elastic.NewClient(
        elastic.SetURL("http://192.168.124.16:9200/"),
        elastic.SetSniff(false),
    )
    if err != nil {
        log.Fatal(err)
    }
    ctx := context.Background()
    mapping, err := client.GetMapping().Index("news").Do(ctx)
    if err != nil {
        log.Fatal(err)
    }
    fmt.Println(mapping)
}

# 代码运行结果
# map[news:map[mappings:map[properties:map[news_status:map[type:byte] news_title:map[type:text] news_type:map[type:keyword]]]]]

这里需要注意的是 elastic.SetSniff(false) ,当我们使用 elastic 的时候会自动转换地址,不过获取的地址由于我们使用的是 docker,这就导致获取IP 地址的时候其实得到的是容器的内容地址,我们是否无法访问的。

package main

import "fmt"

func main() {
    var f func()

    var a *struct{}

    list := []interface{}{f, a}

    for _, item := range list {
        if item == nil {
            fmt.Println("nil")
        }
    }
}

我们来看上面的代码,我们的第一反应应该是输出两个 nil,但事实上并非如此。我们来在 for 循环中输出 item。

    ...
    for _, item := range list {
    fmt.Println(item)
        if item == nil {
            fmt.Println("nil")
        }
    }
    ...

 title=

item 的两个输出的确是 nil ,因为我们只是定义了两个变量却没有对变量进行复制,所以他的值肯定就是 nil,但是为什么在判断的时候确认为他们不是 nil 呢?

首先,我们要知道 fmt.Println 打印数据的时候是打印出变量的值,也就是相当于 fmt.Printlf("%v\n" ,item) ,但是 item 其实是有类型的,我们先修改一下代码。

package main

import "fmt"

func main() {
    //var f func()

    var a *struct{}

    list := []*struct{}{a}

    for _, item := range list {
        if item == nil {
            fmt.Println("nil")
        }
    }
}

//output: nil

当我们将 list 的类型修改为 []*struct{} 的时候就可以正确判断了,那么就说明是由于 interface 导致的判断出错。

我们在对 interface 进行 != nil 判断的时候,必须值和类型都为 nil 的时候才可以,我们可以通过 reflect 反射包来判断 一个 interface 的值是否为 nil.

package main

import (
    "fmt"
    "reflect"
)

func main() {
    //var f func()

    var a *struct{}

    list := []*struct{}{a}

    for _, item := range list {
        if  reflect.ValueOf(item).IsNil() {
            fmt.Println("nil")
        }
    }
}