后端

Elasticsearch批量insert和批量upsert

月盾

golang版本的elasticsearch批量插入和批量更新方法如下:

package main

import (
  "github.com/elastic/go-elasticsearch/v8"
  "github.com/elastic/go-elasticsearch/v8/esutil"
)
func main()  {
  list:=make(User, 0)
  bulkES(list)
}

func bulkES(list []User) error {
	indexer, err := esutil.NewBulkIndexer(esutil.BulkIndexerConfig{
		Index:  "search-user",
		Client: ES,
	})
	if err != nil {
		return err
	}
	for _, v := range list {
		data, err := json.Marshal(v)
		if err != nil {
			return err
		}
		err = indexer.Add(
			context.Background(),
			esutil.BulkIndexerItem{
				Action: "index",
				Body:   bytes.NewReader(data),
			},
		)
		if err != nil {
			return err
		}
	}
	indexer.Close(context.Background())
	return nil
}

最简单版的代码整体上就是如此,但需要注意,该代码不适用于生产环境,只能作为测试。主要原因是indexer实例不能多次实例化,所以上面所示代码是一个函数,这个函数是不能多次调用,因为多次进行indexer实例化了,很快就会内存溢出崩溃掉。

正确的使用方式是在init函数或者连接elasticsearch后进行实例化一次,作为全局变量在后续使用,大概是这样:

package main

import (
  "github.com/elastic/go-elasticsearch/v8"
  "github.com/elastic/go-elasticsearch/v8/esutil"
)
var userindexer esutil.BulkIndexer

func init() {
  indexer, err := esutil.NewBulkIndexer(esutil.BulkIndexerConfig{
		Index:  "search-user",
		Client: ES,
	})
  if err != nil {
		return err
	}
  userindexer = indexer
}
func bulkES(list []User) error {
	for _, v := range list {
		data, err := json.Marshal(v)
		if err != nil {
			return err
		}
		err = userindexer.Add(
			context.Background(),
			esutil.BulkIndexerItem{
				Action: "index",
				Body:   bytes.NewReader(data),
			},
		)
		if err != nil {
			return err
		}
	}
	// userindexer.Close(context.Background())
	return nil
}

注意: 代码只做演示使用,不保证能正确运行。

go http响应乱码

月盾

golang请求接口返回的数据乱码,原因之一是请求头设置了"Accept-Encoding": "gzip, deflate, br",那么如果服务器支持的话响应的数据就会经过gzipdeflatebr等方式的压缩,解决方式是对数据解压,或者可以不设置接收方式,即"Accept-Encoding": ""

import "github.com/andybalholm/brotli"
import "compress/flate"
import "compress/gzip"
// 检测返回的body是否经过压缩,并返回解压的内容
func contentDecoding(res *http.Response) (bodyReader io.Reader, err error) {
	switch res.Header.Get("Content-Encoding") {
	case "gzip":
		bodyReader, err = gzip.NewReader(res.Body)
	case "deflate":
		bodyReader = flate.NewReader(res.Body)
	case "br":
		bodyReader = brotli.NewReader(res.Body)
	default:
		bodyReader = res.Body
	}
	return
}

Gorm Model Find First Where等查询函数的区别

月盾

gorm是一款优秀的国产golang orm关系型数据库框架,在国内外使用比较广泛。它的链式调用还算是一种符合人类思维的风格。

不过在使用过程中也遇到一些困扰,比如:Model, Find, First, Where这些函数该什么时候使用,有时候会有边界不清楚,使用混乱的情况。

以下代码示例使用v2版本,v1和v2大体上相同,有些细微的不同

Where和Find

search := User{UserName:"月盾"}
db.Find(&user, search)
// SELECT * FROM `user` WHERE `user`.`user_name` = '月盾'

db.Where(search).Find(&user)
// SELECT * FROM `user` WHERE `user`.`user_name` = '月盾'

以上两种查询方式结果一样。

Find(dest interface{}, conds ...interface{})Find函数有两个参数,dest是数据接收者,conds是查询条件。所以Find也是可以代替Where来传入条件的。

Where的参数主要分为两类:String,Struct&Map。还有其他不常用类型。

String参数

当使用string参数时,使用方式类似于fmt.Printf,第一个参数为字符串格式,使用?作为占位符,后面的参数作为值。

Struct&Map参数

使用结构体和映射作为参数时,则推荐一个参数即可,struct和map本身就是键值对格式。否则容易引起混淆。比如这样的:

db.Where(&User{Name: "jinzhu"}, "name", "Age").Find(&users)
// SELECT * FROM users WHERE name = "jinzhu" AND age = 0;

db.Where(&User{Name: "jinzhu"}, "Age").Find(&users)
// SELECT * FROM users WHERE age = 0;

注意 当使用结构作为条件查询时,GORM 只会查询非零值字段。这意味着如果您的字段值为 0、’’、false 或其他 零值,该字段不会被用于构建查询条件,例如:

nestjs中使用携程Apollo配置中心

月盾

nest框架官方文档中使用的是本地文件配置,也就是@nestjs/config包。本地配置文件的好处是使用简单,但是对于一些更新较快的项目,难免会增加配置数据,曾经吃过不少配置文件的亏,在发布的时候很容易因为缺少配置文件直接把服务发挂了,或者需要在服务器上修改配置很容易修改错误导致服务发布失败。

集中的配置中心可以解决上面问题,本文以apollo配置中心为例来说明。 在使用的过程中需要注意以下问题:从配置中心获取数据库连接信息,再去连接会连接失败,因为在连接的时候还没有获取到配置信息。 先看代码再解释。

// main.ts
import { NestFactory } from '@nestjs/core';
import { ExpressAdapter, NestExpressApplication } from '@nestjs/platform-express';
import { AppModule } from './app.module';
import { MyLogger } from './libs/mylog.service';
import { join } from 'path';
const Apollo = require('node-apollo');
const dotenv = require('dotenv');

async function bootstrap() {
    try {
        const root = join(__dirname, '../');
        let envFile = join(root, '.env')
        dotenv.config({ "path": envFile })
        const {
            APOLLO_APPID,
            APOLLO_ENV,
            APOLLO_HOST,
            APOLLO_NAMESPACE,
            APOLLO_PORT,
            APOLLO_TOKEN,
            APOLLO_ClUSTER
        } = process.env;
        let apolloEnv = {
            configServerUrl: `http://${APOLLO_HOST}:${APOLLO_PORT}`,
            appId: `${APOLLO_APPID}`,
            clusterName: `${APOLLO_ClUSTER}`,
            apolloEnv: `${APOLLO_ENV}`,
            token: `${APOLLO_TOKEN}`,
            namespaceName: [`${APOLLO_NAMESPACE}`]
        };
        // 获取到的配置信息
        let zmConf = await Apollo.remoteConfigService(apolloEnv);
        console.log(">>>>>>>main.config", zmConf);
        process.env = Object.assign(process.env, zmConf);
    } catch (err) {
        console.log(`获取环境变量异常:${err}`)
    }

    const app = await NestFactory.create<NestExpressApplication>(AppModule, new ExpressAdapter());
    app.useLogger(app.get(MyLogger));

    await app.listen(3434, () => {
        const logger = new MyLogger('main.ts');
        logger.debug(process.env.NODE_ENV, 'main.ts');
        logger.log('server start on http://localhost:3434');
    });
}
bootstrap();
// app.module.ts
@Module({
    imports: [
        // MongooseModule.forRoot(`mongodb://${process.env.DATABASE_USER}:${process.env.DATABASE_PASSWORD}@${process.env.DATABASE_HOST}:${process.env.DATABASE_PORT}/${process.env.DATABASE_DATABASE}`),
        MongooseModule.forRootAsync({
            useFactory: () => ({
                uri: `mongodb://${process.env.DATABASE_USER}:${process.env.DATABASE_PASSWORD}@${process.env.DATABASE_HOST}:${process.env.DATABASE_PORT}/${process.env.DATABASE_DATABASE}`
            }),
        }),
        //  Nest can't resolve dependencies of the AppService (?). Please make sure that the argument ArticleService at index [0] is available in the AppModule context.
        // 在article.service中exports:[ArticleService]
        ArticleModule,
        LoggerModule,
        // HttpModule,
    ], // 导入模块所需的导入模块列表
    controllers: [AppController], // 必须创建的一组控制器
    providers: [AppService, MyLogger], // 由 Nest 注入器实例化的提供者,并且可以在整个模块中共享
})
export class AppModule implements NestModule {
    // 中间件模块在此处添加,可以给某一部分增加中间件,如果要全局增加则在main.ts中使用app.use添加
    configure(consumer: MiddlewareConsumer) {
        consumer
            .apply()
            // .with('中间件参数')
            .forRoutes('/*');
    }
}

使用MongooseModule.forRoot连接数据库肯定是不行的,需要改成异步的:

go单元测试初始化

月盾

go单元测试会遇到这样的场景: 写好了service层函数getUser()。然后测试测试getUser函数。有个问题是,函数中使用了数据库连接,如果直接测试的话会报错误,比如空指针错误。

panic: runtime error: invalid memory address or nil pointer dereference [recovered] panic: runtime error: invalid memory address or nil pointer dereference [signal 0xc0000005 code=0x0 addr=0xb0 pc=0x167680d]

如果遇到这种情况很有可能就是数据库连接未初始化。但是单元测试并不会主动去初始化数据库连接。不用担心,有办法。 go test提供了用于初始化的方法:TestMain函数。只需要在这个函数中进行数据库初始化,后面需要用的的数据库连接可直接使用,不需要重复初始化。

func TestMain(m *testing.M) {
	fmt.Println("begin")
	dba, err := gorm.Open("sqlite3", "../../website.db")
	db.SQLLite = dba
	if err != nil {
		panic(err)
	}
	m.Run()
	fmt.Println("end")
}

func TestProjectUsers(t *testing.T) {
	userService := user.NewService(db.SQLLite)
	users, err := userService.GetProjectUsers(25)
	if err != nil {
		t.Error(err)
	}
	t.Log("返回结果:", users)
}

goquery 中文乱码

月盾

乱码的情况目前有两种可能:

  • 常规乱码,网页非utf-8。
  • 非常规乱码,代码导致的乱码。

goquery中文乱码

关于常规乱码可参考issue获取中文网页有乱码的问题 #185 非常规乱码就像我遇到的一样,最开始以为是网页问题,使用了github.com/djimenez/iconv-go转换还是乱码,使用了golang.org/x/text/encoding/simplifiedchinese还是乱码。 试试英文网页,还是乱码。最终一点点调试发现是由header引起的。 req.Header.Add("Accept-Encoding", "gzip, deflate") 这一行的作用是告诉服务器浏览器要接收的数据编码是gzip,dflate,到达浏览器后会自动解码。但是我们的代码并非浏览器,不会自动解码,所以接收到的就是非常规的压缩数据。

golang操作mongodb

月盾

在之前mgo是一个使用广泛的mongodb驱动器,不过从2018年开始已不再维护,虽然觉得怪可惜的,但也不推荐使用了,毕竟mongodb本身一直在迭代,如果驱动器不更新后续也没法使用。 详细说明见仓库:https://github.com/go-mgo/mgo

而mongodb提供了官方驱动,目前能找到的中文文档大多比较旧了,推荐直接看官方文档,有完整的操作手册:https://www.mongodb.com/blog/search/golang 本文也不想做一次搬运工,毕竟也不能随时保持更新,还是直接看官方文档比较好。下面列出一些主要的文章链接:

Stack Overflow Research of 100,000 Developers Finds MongoDB is the Most Wanted Database (2019-2-2)

Official MongoDB Go Driver Now Available for Beta Testing (2019-2-2) mongodb将为go提供官方驱动支持

MongoDB Go Driver Tutorial (2019-5-30) MongoDB Go驱动程序教程

Go Migration Guide (2019-2-2) 从社区驱动(mgo)迁移到官方驱动

MongoDB Stitch Functions – The AWS re:Invent Stitch Rover Demo(2019-10-15)

Calling the MongoDB Atlas API - How to do it from Go(2019-3-18)

MongoDB Go Driver Tutorial Part 1: Connecting, Using BSON, and CRUD Operations(2019-4-23)

使用pm2一键部署多个服务

月盾
  1. pm2支持远程部署服务,创建文件ecosystem.json,内容形式如:
{
  // Applications part
  "apps" : [{
    "name"      : "API",
    "script"    : "app.js",
    "env": {
      "COMMON_VARIABLE": "true"
    },
    // Environment variables injected when starting with --env production
    // http://pm2.keymetrics.io/docs/usage/application-declaration/#switching-to-different-environments
    "env_production" : {
      "NODE_ENV": "production"
    }
  },{
    "name"      : "WEB",
    "script"    : "web.js"
  }],
  // 部署部分
  // Here you describe each environment
  "deploy" : {
    "production" : {
      "user" : "node",
      // 多主机配置
      "host" : ["212.83.163.1", "212.83.163.2", "212.83.163.3"],
      // 服务使用的分支
      "ref"  : "origin/master",
      // Git 仓库地址
      "repo" : "git@github.com:repo.git",
      // 项目目录
      "path" : "/var/www/production",
      // Can be used to give options in the format used in the configura-
      // tion file.  This is useful for specifying options for which there
      // is no separate command-line flag, see 'man ssh'
      // can be either a single string or an array of strings
      "ssh_options": "StrictHostKeyChecking=no",
      // To prepare the host by installing required software (eg: git)
      // even before the setup process starts
      // 可以使用";"分割多个命令
      // or path to a script on your local machine
      "pre-setup" : "npm install",
      // Commands / path to a script on the host machine
      // This will be executed on the host after cloning the repository
      // eg: placing configurations in the shared dir etc
      "post-setup": "ls -la",
      // Commands to execute locally (on the same machine you deploy things)
      // Can be multiple commands separated by the character ";"
      "pre-deploy-local" : "echo 'This is a local executed command'"
      // Commands to be executed on the server after the repo has been cloned
      "post-deploy" : "npm install && pm2 startOrRestart ecosystem.json --env production"
      // Environment variables that must be injected in all applications on this env
      "env"  : {
        "NODE_ENV": "production"
      }
    },
    "staging" : {
      "user" : "node",
      "host" : "212.83.163.1",
      "ref"  : "origin/master",
      "repo" : "git@github.com:repo.git",
      "path" : "/var/www/development",
      "ssh_options": ["StrictHostKeyChecking=no", "PasswordAuthentication=no"],
      "post-deploy" : "pm2 startOrRestart ecosystem.json --env dev",
      "env"  : {
        "NODE_ENV": "staging"
      }
    }
  }
}

Edit the file according to your needs.

gorm模糊查询和分页查询同时查总条数

月盾

gorm概述

  • 全功能ORM(几乎)
  • 关联(包含一个,包含多个,属于,多对多,多种包含)
  • Callbacks(创建/保存/更新/删除/查找之前/之后)
  • 预加载(急加载)
  • 事务
  • 复合主键
  • SQL Builder
  • 自动迁移
  • 日志
  • 可扩展,编写基于GORM回调的插件
  • 每个功能都有测试
  • 开发人员友好

like查询

gorm提供了丰富的查询功能,在开发中我们经常需要组合查询,比如列表查询,列表查询一般需要支持条件查询,模糊查询,分页查询,数据条数查询。 已上支持基本满足了日常开发需要,一些基本的查询需求可以查看文档得到解决,不过文档并没有覆盖所有日常开发案例,尤其是一些组合需求,本文挑了一段常见的场景。

func (u *userService) GetuserList(offset, limit int, search User) (users []User, count int, err error) {
	query := u.mysql.Model(&User{})
	if search.Name != "" {
		query.Where("name LIKE ?", search.Name+"%")
	}
	if search.Age != "" {
		query.Where("age = ?", search.Age)
	}

	err = query.Offset(offset).Limit(limit).Find(&users).Offset(-1).Limit(-1).Count(&count).Error
	return users, count, err
}

这简单的一小段已经包含了gorm的模糊查询动态条件分页查询数据条数。 这就是一个最常见的列表查询,列表需要支持条件查询,模糊查询,分页,从代码可以直接看到。

  1. if代码是动态组装条件。

  2. err = query.Offset(offset).Limit(limit).Find(&users).Offset(-1).Limit(-1).Count(&count).Error 这行代码包含了数据列表查询和数据条数。

  3. 有些需要注意的地方是query.Offset(offset).Limit(limit).Find(&users) 用于查询数据列表,

  4. .Offset(-1).Limit(-1).Count(&count)用户查询条数,**Offset(-1)和Limit(-1)**很重要,也是一个小技巧,不加的话会在统计条数后也加上offset和limit,导致查不到条数。 查询结果:

SELECT * FROM `user` LIMIT 10 OFFSET 0;
SELECT count(*) FROM `user`;

go-micro线上部署,注册服务到etcd

月盾

线上部署

在线上部署就不能使用go run main.go命令了,需要打包编译成可执行文件。 linux系统需要这样编译:GOOS=linux go build -o service main.go,就是在windows系统上进行交叉编译,可根据自己服务器情况修改参数。

go build -o service main.go
go build -o api api/api.go

线上的restful api也不能使用micro api了。需要选择适合自己的web服务框架,在web服务中调用api服务。

etcd启动

线上etcd和本地启动有区别,如果etcd是单独的服务器,那么在不加任何参数的情况下直接启动,那基本是调不通的。

$ ./service --registry=etcd --registry_address=xx.xx.xx.xx:2379
2020-03-17 17:04:42 Starting [service] go.micro.srv.user
2020-03-17 17:04:42 Server [grpc] Listening on [::]:48493
2020-03-17 17:04:42 Registry [etcd] Registering node: go.micro.srv.user-f32a2950-8e59-44d4-ac86-f4e1ec103395
{"level":"warn","ts":"2020-03-17T17:04:47.849+0800","caller":"clientv3/retry_interceptor.go:61","msg":"retrying of unary invoker failed","target":"endpoint://client-e45decee-12bf-4a9b-a7ab-f92eece39420/xx.xx.xx.xx:2379","attempt":0,"error":"rpc error: code = DeadlineExceeded desc = latest connection error: connection error: desc = \"transport: Error while dialing dial tcp xx.xx.xx.xx:2379: connect: connection refused\""}
2020-03-17 17:04:47 Server register error: %!(EXTRA context.deadlineExceededError=context deadline exceeded)

这就是错误示例。 为了能顺利看到胜利的结果,需要这样启动etcd: