简介

ECharts是一款基于JavaScript的数据可视化图表库,提供直观,生动,可交互,可个性化定制的数据可视化图表。ECharts最初由百度团队开源,并于2018年初捐赠给Apache基金会,成为ASF孵化级项目。

在 Golang 这门语言中,目前数据可视化的第三方库还是特别少,go-echarts 的开发就是为了填补这部分的空隙。Apache ECharts 是非常优秀的可视化图表库,凭借着良好的交互性,精巧的图表设计,得到了众多开发者的认可。也有其他语言为其实现了相应语言版本的接口,如 Python 的 pyecharts,go-echarts 也是借鉴了 pyecharts 的一些设计思想。

特性

  • 简洁的 API 设计,使用如丝滑般流畅
  • 囊括了 25+ 种常见图表,应有尽有
  • 高度灵活的配置项,可轻松搭配出精美的图表
  • 详细的文档和示例,帮助开发者更快的上手项目
  • 多达 400+ 地图,为地理数据可视化提供强有力的支持

如何使用

1.安装go-echarts库

go get -u github.com/go-echarts/go-echarts/v2

2.接着,我们来创建一个图表实例,绘制一个条形图,来演示如何使用。

package main

import (
	"math/rand"
	"os"

	"github.com/go-echarts/go-echarts/v2/charts"
	"github.com/go-echarts/go-echarts/v2/opts"
)

// generate random data for bar chartg
func generateBarItems() []opts.BarData {
	items := make([]opts.BarData, 0)
	for i := 0; i < 7; i++ {
		items = append(items, opts.BarData{Value: rand.Intn(300)})
	}
	return items
}

func main() {
	//创建一个新的bar实例
	bar := charts.NewBar()
	//设置一些全局选项,如标题/图例/工具提示或其他任何东西
	//example: 标题/子标题
	bar.SetGlobalOptions(charts.WithTitleOpts(opts.Title{
		Title:    "My first bar chart generated by go-echarts",
		Subtitle: "It's extremely easy to use, right?",
	}))

	//将数据放入实例
	bar.SetXAxis([]string{"Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun"}).
		AddSeries("Category A", generateBarItems()).
		AddSeries("Category B", generateBarItems())
	//奇迹发生的地方-绘图生成html
	f, _ := os.Create("bar.html")
	bar.Render(f)
}

运行上例,会生成bar.html,详细:

  1. 如果想把上例改成折线图呢
package main

import (
	"github.com/go-echarts/go-echarts/v2/charts"
	"github.com/go-echarts/go-echarts/v2/opts"
	"math/rand"
	"os"
)

// generate random data for line chart
func generateLineItems() []opts.LineData {
	items := make([]opts.LineData, 0)
	for i := 0; i < 7; i++ {
		items = append(items, opts.LineData{Value: rand.Intn(300)})
	}
	return items
}

func main() {
	//创建一个折线图实例
	line := charts.NewLine()
	//设置一些全局选项,如标题/图例/工具提示或其他任何东西
	//example: 标题/子标题
	line.SetGlobalOptions(charts.WithTitleOpts(opts.Title{
		Title:    "My first lines chart generated by go-echarts",
		Subtitle: "It's extremely easy to use, right?",
	}))

	//将数据放入实例
	line.SetXAxis([]string{"Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun"}).
		AddSeries("Category A", generateLineItems()).
		AddSeries("Category B", generateLineItems()).
		SetSeriesOptions(charts.WithLineChartOpts(opts.LineChart{Smooth: true}))

	//奇迹发生的地方-绘图生成html
	f, _ := os.Create("lines.html")
	line.Render(f)
}

运行上例,会生成lines.html,详细:

  1. 如果想直接生成图表的 http serve呢:
package main

import (
	"github.com/go-echarts/go-echarts/v2/charts"
	"github.com/go-echarts/go-echarts/v2/opts"
	"math/rand"
	"net/http"
)

// generate random data for line chart
func generateLineItems() []opts.LineData {
	items := make([]opts.LineData, 0)
	for i := 0; i < 7; i++ {
		items = append(items, opts.LineData{Value: rand.Intn(300)})
	}
	return items
}

func httpserver(w http.ResponseWriter, _ *http.Request) {
	//创建一个折线图实例
	line := charts.NewLine()
	//设置一些全局选项,如标题/图例/工具提示或其他任何东西
	//example: 标题/子标题
	line.SetGlobalOptions(charts.WithTitleOpts(opts.Title{
		Title:    "My first lines chart generated by go-echarts",
		Subtitle: "It's extremely easy to use, right?",
	}))

	//将数据放入实例
	line.SetXAxis([]string{"Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun"}).
		AddSeries("Category A", generateLineItems()).
		AddSeries("Category B", generateLineItems()).
		SetSeriesOptions(charts.WithLineChartOpts(opts.LineChart{Smooth: true}))

	//奇迹发生的地方-绘图生成html
	line.Render(w)
}

func main() {
	http.HandleFunc("/", httpserver)
	http.ListenAndServe(":8080", nil)
}

访问,http://localhost:8080/ ,详细如下

三、总结

本文简要介绍go-echarts和使用方法,并通过绘制条形图、折线图和图表的http server 的例子演示了使用方法。更多功能待续。

参考

在数据的传输过程,为了安全,我们需要对其进行加密。加密算法分为双向加密和单向加密。单向加密包括MD5、SHA等摘要算法,它们是不可逆的。双向加密包括对称加密和非对称加密,对称加密包括AES加密、DES加密等。双向加密是可逆的,它使用相同的密钥进行加密和解密。本文主要介绍AES算法以及如何使用golang实现AES加解密。

AES简介

AES是高级加密标准,在密码学中又称Rijndael加密法,是美国联邦政府采用的一种区块加密标准。这个标准用来替代原先的DES,目前已经被全世界广泛使用,同时AES已经成为对称密钥加密中最流行的算法之一。AES支持三种长度的密钥:128位,192位,256位。

加解密过程为:字节代替、行移位、列混淆、轮密钥加。

相关概念:

  1. 密钥:AES128,AES192,AES256,实际上就是指的AES算法对不同长度密钥的使用

  2. 分组: 把明文拆分成一个个独立的明文块,每一个明文块长度128bit。加密生成一个个独立的密文块,这些密文块拼接在一起。填充模式有:

    1. NoPadding :不做任何填充,但是要求明文必须是16字节的整数倍。
    2. PKCS5Padding: 如果明文块少于16个字节(128bit),在明文块末尾补足相应数量的字符,且每个字节的值等于缺少的字符数。
    3. ISO10126Padding: 如果明文块少于16个字节(128bit),在明文块末尾补足相应数量的字节,最后一个字符值等于缺少的字符数,其他字符填充随机数
  3. 加密模式:

    1. 电码本模式(Electronic Codebook Book (ECB)) 模式是将整个明文分成若干段相同的小段,然后对每一小段进行加密。
    2. 密码分组链接模式(Cipher Block Chaining (CBC)) 模式是先将明文切分成若干小段,然后每一小段与初始块或者上一段的密文段进行异或运算,再与密钥进行加密
    3. 计算器模式(Counter (CTR))不常用,在 CTR 模式中, 有一个自增的算子,这个算子用密钥加密之后输出和明文异或的结果得到密文,相当于一次一密
    4. 密码反馈模式(Cipher FeedBack (CFB))不常用
    5. 输出反馈模式(Output FeedBack (OFB))

golang实现AES加解密

golang使用“crypto/aes”进行AES加解密 。接着我们通过一个使用CBC模式对原文进行加解密的例子来演示如何实现。

package main

import (
    "crypto/aes"
    "crypto/cipher"
    "encoding/base64"
    "fmt"
)

func main() {
    // 原始数据
    plaintext := []byte("Hello, world!")

    // 密钥,长度必须为 16、24 或 32 字节(128位,192位,256位)
    key := []byte("0123456789abcdef")

    // 使用 AES 创建加密算法对象
    block, err := aes.NewCipher(key)
    if err != nil {
        panic(err)
    }

    // 对数据进行填充,使其长度为块大小的整数倍
    plaintext = PKCS5Padding(plaintext, block.BlockSize())

    // 创建 CBC 模式的加密算法对象
    iv := []byte("0123456789abcdef") // 初始化向量,长度必须为块大小
    mode := cipher.NewCBCEncrypter(block, iv)

    // 加密数据
    ciphertext := make([]byte, len(plaintext))
    mode.CryptBlocks(ciphertext, plaintext)

    // 将密文进行 base64 编码
    encoded := base64.StdEncoding.EncodeToString(ciphertext)
    fmt.Println(encoded)

    // 解码 base64 编码的密文
    decoded, _ := base64.StdEncoding.DecodeString(encoded)

    // 创建 CBC 模式的解密算法对象
    mode = cipher.NewCBCDecrypter(block, iv)

    // 解密数据
    decrypted := make([]byte, len(decoded))
    mode.CryptBlocks(decrypted, decoded)

    // 去除填充数据
    decrypted = PKCS5UnPadding(decrypted)
    fmt.Println(string(decrypted))
}

// 对数据进行填充,使其长度为 blockSize 的整数倍
func PKCS5Padding(data []byte, blockSize int) []byte {
    padding := blockSize - len(data)%blockSize
    padtext := bytes.Repeat([]byte{byte(padding)}, padding)
    return append(data, padtext...)
}

// 去除填充数据
func PKCS5UnPadding(data []byte) []byte {
    length := len(data)
    unpadding := int(data[length-1])
    return data[:(length - unpadding)]
}

输出:

+HzZQh0Do42Kg1PQsdhdcw==
Hello, world!

上例中,分别演示了

  1. 对加密过程:创建加密对象、原文填充、创建CBC模式,加密。
  2. 解密过程: 创建CBC解密模式,解密,去除填充。

为了方便使用,我们对上例的加解密进行封装,详细如下:

package main

import (
	"bytes"
	"crypto/aes"
	"crypto/cipher"
	"encoding/base64"
	"errors"
	"fmt"
)

func main() {
	plaintext := []byte("Hello, world!")
	key := []byte("0123456789abcdef")
	iv := []byte("0123456789abcdef")

	// 加密数据
	encoded, err := AesCBCEncrypt(plaintext, key, iv)
	if err != nil {
		panic(err)
	}
	fmt.Println(encoded)

	// 解密数据
	decoded, err := AesCBCDecrypt(encoded, key, iv)
	if err != nil {
		panic(err)
	}
	fmt.Println(string(decoded))
}

// AesCBCEncrypt 使用 AES CBC 模式加密数据
func AesCBCEncrypt(plaintext, key, iv []byte) (string, error) {
	block, err := aes.NewCipher(key)
	if err != nil {
		return "", err
	}

	plaintext = PKCS5Padding(plaintext, block.BlockSize())

	mode := cipher.NewCBCEncrypter(block, iv)

	ciphertext := make([]byte, len(plaintext))
	mode.CryptBlocks(ciphertext, plaintext)

	return base64.StdEncoding.EncodeToString(ciphertext), nil
}

// AesCBCDecrypt 使用 AES CBC 模式解密数据
func AesCBCDecrypt(ciphertext string, key, iv []byte) ([]byte, error) {
	block, err := aes.NewCipher(key)
	if err != nil {
		return nil, err
	}

	decoded, err := base64.StdEncoding.DecodeString(ciphertext)
	if err != nil {
		return nil, err
	}

	if len(decoded)%block.BlockSize() != 0 {
		return nil, errors.New("ciphertext is not a multiple of the block size")
	}

	mode := cipher.NewCBCDecrypter(block, iv)

	decrypted := make([]byte, len(decoded))
	mode.CryptBlocks(decrypted, decoded)

	decrypted = PKCS5UnPadding(decrypted)

	return decrypted, nil
}

// PKCS5Padding 对数据进行填充,使其长度为 blockSize 的整数倍
func PKCS5Padding(data []byte, blockSize int) []byte {
	padding := blockSize - len(data)%blockSize
	padtext := bytes.Repeat([]byte{byte(padding)}, padding)
	return append(data, padtext...)
}

// PKCS5UnPadding 去除填充数据
func PKCS5UnPadding(data []byte) []byte {
	length := len(data)
	unpadding := int(data[length-1])
	return data[:(length - unpadding)]
}

总结

本文主要介绍对称加密算法AES,AES使用不同指定长度的密钥,对原文进行分组,可选用更安全的加密模式CBC。在 golang中,可以使用“crypto/aes”快速实现原文填充、加密模式、加密、解密、去除填充等AES加解密过程。

参考

上篇我们介绍了RedisStream实现消息队列和发布订阅模式 ,今天我们将介绍Go语言的实现方法。

消息队列

接上篇的例子,我们通过go-redis库的接口XAdd、XReadGroup、XGroupCreateMkStream来实现,详细如下:

package main

import (
	"context"
	"fmt"
	"github.com/go-redis/redis/v8"
)
func main() {
	var (
		ctx      = context.Background()
		consumer = "consumer1"
		stream   = "mystream"
		group    = "group1"
		start    = ">"
		count    = 10
	)

	// 创建Redis客户端
	client := redis.NewClient(&redis.Options{
		Addr:     "localhost:6379",
		Password: "",
		DB:       0,
	})
	// 创建消费者组
	err := client.XGroupCreateMkStream(ctx, stream, group, "0-0").Err()
	if err != nil {
		panic(err)
	}
	//添加消息
	_, err = client.XAdd(ctx, &redis.XAddArgs{
		Stream: stream,
		ID:     "*",
		Values: map[string]interface{}{
			"name": "foo",
		},
	}).Result()
	if err != nil {
		panic(err)
	}
	//读取消息
	messages, err := client.XReadGroup(ctx, &redis.XReadGroupArgs{
		Group:    group,
		Consumer: consumer,
		Streams:  []string{stream, start},
		Count:    int64(count),
		Block:    0,
		NoAck:    false,
	}).Result()
	if err != nil {
		panic(err)
	} 
	for _, message := range messages[0].Messages {
		fmt.Printf("Received message: %v\n", message.Values)
	}
}

上例实现了一个简单的消息队列,可以实现消息的发送和接收,并保证每条消息只被处理一次 。

阻塞超时

通过修改Block(单位为ms)参数来实现阻塞,详细修改如下

for {
		fmt.Println("Start Receiving", time.Now().String())
		//读取消息
		messages, err := client.XReadGroup(ctx, &redis.XReadGroupArgs{
			Group:    group,
			Consumer: consumer,
			Streams:  []string{stream, start},
			Count:    int64(count),
			Block:    2000,
			NoAck:    false,
		}).Result()
		if err != nil {
			panic(err)
		}
		fmt.Println("Received Length=", len(messages), time.Now().String()) 
    ...
	}

输出如下:

Start Receiving 2023-04-08 21:39:23.9669498 +0800 CST m=+0.529310801
Received Length= 1 2023-04-08 21:39:24.0419963 +0800 CST m=+0.604357301
Received message: map[name:foo]
Start Receiving 2023-04-08 21:39:24.0419963 +0800 CST m=+0.604357301
panic: read tcp 127.0.0.1:63777->127.0.0.1:6379: i/o timeout

可见,XReadGroup一直阻塞等待新的消息,直到 Redis 连接超时或者命令被中断。 ps: 如若不需要阻塞等待可修改Block为<0 的值实现。

发布订阅模式

接着,我们实现上篇提到的发布订阅模式,XGroupCreateMkStream创建另外一个订阅组group2,两个协程XReadGroup接收消息,往stream中XAdd消息,两个订阅者将都收到消息,详细如下:

package main

import (
	"context"
	"fmt"
	"github.com/go-redis/redis/v8"
	"time"
)
func main() {
	var (
		ctx       = context.Background()
		consumer  = "consumer1"
		consumer2 = "consumer2"
		stream    = "mystream"
		group     = "group1"
		group2    = "group2"
		start     = ">"
		count     = 10
	)
	// 创建Redis客户端
	client := redis.NewClient(&redis.Options{
		Addr:        "localhost:6379",
		Password:    "",
		DB:          0,
		ReadTimeout: time.Second * 10,
	})
	// 创建消费者组
	err := client.XGroupCreateMkStream(ctx, stream, group, "0-0").Err()
	if err != nil {
		//panic(err)
	}
	// 创建消费者组2
	err = client.XGroupCreateMkStream(ctx, stream, group2, "0-0").Err()
	if err != nil {
		//panic(err)
	}
	//添加消息
	_, err = client.XAdd(ctx, &redis.XAddArgs{
		Stream: stream,
		ID:     "*",
		Values: map[string]interface{}{
			"name": "foo",
		},
	}).Result()
	if err != nil {
		panic(err)
	}
	go func() {
		for {
			fmt.Println("Start Receiving", time.Now().String())
			//读取消息
			messages, err := client.XReadGroup(ctx, &redis.XReadGroupArgs{
				Group:    group,
				Consumer: consumer,
				Streams:  []string{stream, start},
				Count:    int64(count),
				Block:    2000,
				NoAck:    false,
			}).Result()
			if err != nil {
				panic(err)
			}
			fmt.Println("Received Length=", len(messages), time.Now().String())
			for _, message := range messages[0].Messages {
				fmt.Printf("Received message: %v\n", message.Values)
			}

		}
	}()
	//读取消息2
	go func() {
		for {
			fmt.Println("2 Start Receiving ", time.Now().String())
			messages, err := client.XReadGroup(ctx, &redis.XReadGroupArgs{
				Group:    group2,
				Consumer: consumer2,
				Streams:  []string{stream, start},
				Count:    int64(count),
				Block:    2000,
				NoAck:    false,
			}).Result()
			if err != nil {
				panic(err)
			}
			fmt.Println("2 Received Length=", len(messages), time.Now().String())
			for _, message := range messages[0].Messages {
				fmt.Printf("2Received message: %v\n", message.Values)
			}
		}
	}()
	for {
	}
}

输出如下:

Start Receiving 2023-04-08 22:19:41.9981051 +0800 CST m=+0.347161101
2 Start Receiving  2023-04-08 22:19:41.9981051 +0800 CST m=+0.347161101
Received Length= 1 2023-04-08 22:19:42.0571709 +0800 CST m=+0.406226901
Received message: map[name:foo]
Start Receiving 2023-04-08 22:19:42.0571709 +0800 CST m=+0.406226901
2 Received Length= 1 2023-04-08 22:19:42.3628412 +0800 CST m=+0.711897201
2Received message: map[name:foo]

参考

在 Postgres 中基于 MVCC(多版本并发控制)技术 ,使用一种称为“版本链”的技术来维护对数据库中行的并发访问, 来实现事务隔离级别的。

MVCC-多版本并发控制

在 Postgres 中,每当一行被修改时,Postgres 就会创建一个新版本的该行,并将其添加到版本链的末尾。版本链包含了所有当前和以前的行版本,这样可以避免并发读写时的数据竞争和数据不一致问题。

MVCC避免了传统的数据库系统的锁定方法,将锁争夺最小化来允许多用户环境中的合理性能。

常见并发问题

  1. 脏读: 一个事务读取了另一个并行未提交事务写入的数据
  2. 不可重复读 一个事务重新读取之前读取过的数据,发现该数据已经被另一个事务(在初始读之后提交)修改。
  3. 幻读 一个事务重新执行一个返回符合一个搜索条件的行集合的查询, 发现满足条件的行集合因为另一个最近提交的事务而发生了改变。
  4. 序列化异常 成功提交一组事务的结果与这些事务所有可能的串行执行结果都不一致。

事务隔离级(解决方案)

事务隔离级别指的是多个并发事务之间的可见性和可操作性,SQL标准定义了四种隔离级别,分别是:

  1. Read Uncommitted(读未提交) 这是最低的事务隔离级别,它允许一个事务读取其他事务未提交的数据。这可能会导致脏读、不可重复读和幻读问题。
  2. Read Committed(读提交) 在该隔离级别下,事务只能看到已经提交的数据。它解决了脏读问题,但仍然存在不可重复读和幻读问题。
  3. Repeatable Read(可重复读) 在该隔离级别下,事务在执行期间看到的所有数据都是一致的。它解决了不可重复读问题,但仍然存在幻读问题。
  4. Serializable(可串行化) 在该隔离级别下,事务看到的所有数据都是一致的,并且完全避免了脏读、不可重复读和幻读问题。这是最高的事务隔离级别,但也是最慢的,因为它会导致大量的锁竞争。

在PostgreSQL中,你可以请求四种标准事务隔离级别中的任意一种,但是内部只实现了三种不同的隔离级别,即 PostgreSQL 的读未提交模式的行为和读已提交相同。

golang lib/pq设置隔离等级

在golang中,使用lib/pq包,通过执行“SET TRANSACTION ISOLATION LEVEL …”来设置隔离等级。以下测试脏读的例子:

package main

import (
	"database/sql"
	"fmt"
	_ "github.com/lib/pq"
	"time"
)

func main() {
	// 连接数据库
	db, err := sql.Open("postgres", dsn)
	if err != nil {
		panic(err)
	}
	defer db.Close()

	// 设置隔离级别为读未提交
	_, err = db.Exec("SET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED")
	//_, err = db.Exec("SET TRANSACTION ISOLATION LEVEL READ COMMITTED")
	if err != nil {
		panic(err)
	}
	// 开始第一个事务,修改用户 "Bob" 的年龄
	tx1, err := db.Begin()
	if err != nil {
		panic(err)
	}
	go func() {
		_, err := tx1.Exec("UPDATE users SET age = 6 WHERE name = 'Bob'")
		if err != nil {
			panic(err)
		}
		rows, err := tx1.Query("SELECT age FROM users WHERE name = 'Bob'")
		if err != nil {
			panic(err)
		}
		var age int
		if rows.Next() {
			err = rows.Scan(&age)
			if err != nil {
				panic(err)
			}
		}
		fmt.Printf("tx1 Bob's age is %d\n", age)
	}()

	// 暂停 1 秒钟,以便让第二个事务读取到未提交的数据
	time.Sleep(time.Second)
	// 开始第二个事务,读取用户 "Bob" 的年龄
	tx2, err := db.Begin()
	if err != nil {
		panic(err)
	}
	rows, err := tx2.Query("SELECT age FROM users WHERE name = 'Bob'")
	if err != nil {
		panic(err)
	}
	var age int
	if rows.Next() {
		err = rows.Scan(&age)
		if err != nil {
			panic(err)
		}
	}
	fmt.Printf("tx2 Bob's age is %d\n", age)
	// 提交第一个事务
	err = tx1.Commit()
	if err != nil {
		panic(err)
	}
	// 提交第二个事务
	err = tx2.Commit()
	if err != nil {
		panic(err)
	}
}

执行输出如下:

tx1 Bob's age is 6
tx2 Bob's age is 5

上例中,分别开启两个隔离级别为“读未提交”事务tx1、tx2,其中tx1对一行数据(Bob)的字段(Age)进行了修改,接着两个事务分别读取,即使在tx1提交之前,tx2也不会读取到未提交的数据,有效避免脏读等并发问题。

小结

PostgreSQL 的事务隔离级别和 MVCC 技术为开发人员提供了强大的并发性能和数据一致性保证。同时MVCC并发控制模型,降低了锁竞争和死锁的风险,允许并发事务访问相同的数据而不会相互干扰。对查询(读)数据的锁请求与写数据的锁请求不冲突,所以读不会阻塞写,而写也从不阻塞读。但是,在选择事务隔离级别时,需要考虑不同事务隔离级别的性能以及应用程序的需求和性能特征,并根据实际情况进行权衡,充分利用 MVCC 技术的优点。

参考

github.com/robfig/cron/v3 是一个功能强大且易于使用的定时任务管理库。本文进一步介绍robfig/cron在定时任务一些主要功能、如何使用它以及一些实际应用场景的例子。主要包括

  1. 添加任务方法AddJob
  2. 指定执行时间
  3. 动态添加和删除任务
  4. Option选项
  5. JobWrapper与DefaultWrapper

AddJob添加任务

Cron实例可以通过调用AddJob() 方法用于添加一个实现了 Job 接口的对象作为任务,Example:

package main

import (
	"fmt"
	"github.com/robfig/cron/v3"
	"time"
)

type GreetingJob struct {
	Name string
}

func (g GreetingJob) Run() {
	fmt.Println("Hi: ", g.Name, "now:", time.Now().String())
}

func main() {
	c := cron.New()
	entityID, err := c.AddJob("@every 2s", GreetingJob{"Greeter"})
	if err != nil {
		fmt.Errorf("error : %v", err)
		return
	}
	fmt.Println("entityID:", entityID)
	c.Start()
	defer c.Stop()
	select {}
}

//output:
//entityID: 1
//Hi:  Greeter now: 2023-03-04 17:50:07.0169185 +0800 CST m=+1.716253301
//Hi:  Greeter now: 2023-03-04 17:50:09.0068064 +0800 CST m=+3.706141201

此例中,GreetingJob实现了cron.Job 接口,cron实例调用AddJob,加入一个每2秒执行的任务,务并传递参数。

指定执行时间

可以通过修改 cron 表达式来指定任务的执行时间。Example:

c := cron.New(cron.WithSeconds()) 
entityID, err := c.AddFunc("0 05 18 * * *", func() {
	fmt.Println("hi now:", time.Now().String())
})

此例中,将在每天的傍晚18点5分执行指定的函数。

动态添加和删除任务

cron/v3 允许开发人员在运行时动态添加和删除任务。Example:

package main

import (
	"fmt"
	"github.com/robfig/cron/v3"
	"time"
)

func main() {
	c := cron.New(cron.WithSeconds())
	c.Start()
	defer c.Stop()
	count := 0
     // 添加第一个任务
	entityID1, err := c.AddFunc("*/2 * * * * *", func() {
		count++
		fmt.Println("Job1: ", count, "now:", time.Now().String())
	})
	must(err)
	fmt.Println("entityID1:", entityID1)
     // 等待 6 秒钟,让第一个任务执行3次
	time.Sleep(time.Second * 6)
    // 添加第二个任务
	entityID2, err := c.AddFunc("*/2 * * * * *", func() {
		count++
		fmt.Println("Job2: ", count, "now:", time.Now().String())
	})
	must(err)
     // 等待 10秒钟,让两个任务交替执行几次
	time.Sleep(time.Second * 10)
     // 删除第2个任务
	c.Remove(entityID2)
     // 等待 10秒钟,发现只有任务1在执行了
	time.Sleep(time.Second * 10)
     // 删除第2个任务
	c.Remove(cron.EntryID(1))
	fmt.Println("entityID2", entityID2)
    //发现只有没有任务在执行了 
	select {}
}

func must(err error) {
	if err != nil {
		panic(any(err))
	}
}

Option选项

Option 是一种用于配置 Cron 实例的结构体类型。Option 类型有多个可选字段,可用于配置定时任务的行为,上例中有使用到的WithSeconds。

以下是 Option 类型的一些字段及其说明:

  • WithSeconds():在 cron 表达式中包含秒(0-59),默认为不包含秒。
  • WithLocation():设置时区,可以使用标准时区名称或时区偏移量。
  • WithChain():将多个函数连接成单个函数。
  • WithParser():指定 cron 表达式的解析器,默认为 StandardParser。
  • WithLogger():指定日志记录器,默认为 DefaultLogger。

下面 WithLogger Option Cron Example:

package main

import (
	"fmt"
	"github.com/robfig/cron/v3"
	"log"
	"os"
	"time"
)

func main() {
	c := cron.New(
		cron.WithLogger(
			cron.VerbosePrintfLogger(log.New(os.Stdout, "cron: ", log.LstdFlags))))
	c.AddFunc("@every 1s", func() {
		fmt.Println("hello world")
	})
	c.Start()
	defer c.Stop()

	time.Sleep(3 * time.Second)
}
//output:
//cron: 2023/03/04 21:31:17 start
//cron: 2023/03/04 21:31:17 schedule, now=2023-03-04T21:31:17+08:00, entry=1, next=2023-03-04T21:31:18+08:00
//cron: 2023/03/04 21:31:18 wake, now=2023-03-04T21:31:18+08:00
//cron: 2023/03/04 21:31:18 run, now=2023-03-04T21:31:18+08:00, entry=1, next=2023-03-04T21:31:19+08:00
//hello world

上例中,调用cron.VerbosPrintfLogger()包装log.Logger,这个logger会详细记录cron内部的调度过程

JobWrapper与DefaultWrapper

//NewChain返回一个由给定JobWrappers组成的Chain,类似中间件。

type JobWrapper func(Job) Job

type Chain struct {
	wrappers []JobWrapper
}

func NewChain(c ...JobWrapper) Chain {
	return Chain{c}
}

cron内置了 3 个用得比较多的JobWrapper

  • Recover 捕获内部Job产生的 panic
  • DelayIfStillRunning 序列化作业,延迟后续的运行直到前一个是完整的。
  • SkipIfStillRunning 跳过Job的调用,如果之前的调用是仍在运行。 以Reover为例:
package main

import (
	"fmt"
	"github.com/robfig/cron/v3"
)

func main() {
	c := cron.New(
		cron.WithSeconds(),
		cron.WithChain(
			cron.Recover(cron.DefaultLogger),
		),
	)

	c.AddFunc("@every 2s", func() {
		fmt.Println("Start...")
		panic(any("ohno...."))
		fmt.Println("End...")
	})

	c.Start()
	defer c.Stop()

	select {}
}

output:

Start...
cron: 2023/03/04 21:59:32 panic, error=ohno...., stack=...
goroutine 7 [running]:
github.com/robfig/cron/v3.Recover.func1.1.1()
	E:/gopath/pkg/mod/github.com/robfig/cron/v3@v3.0.1/chain.go:45 +0xa5
panic({0x7370c0, 0x75b930})
	...

在上例中,使用 cron.Recover() 方法来捕获任务执行过程中的 panic,并记录日志。如果不进行 panic 捕获的话,程序将会因为 panic 而退出。需要注意的是,当使用 Recover() 方法时,不应该让任务函数返回一个 error,否则它将不会被正确地捕获。如果任务函数可能会返回 error,建议使用 Try() 方法进行捕获。

以上。

参考

分布式任务任务调度与管理在微服务开发中是很有必要的。例如,当需要执行一些计算密集型或网络I/O密集型操作时,为了不影响主线程的性能,我们可以将这些任务放到后台异步执行。此外,异步任务处理还可以改善应用程序的可伸缩性和可靠性,因为它可以将任务分布到多个处理器上并允许任务的重试。 Asynq 介绍 Asynq Simple, reliable, and efficient distributed task queue in Go Asynq 是一个 Go 库,用于排队任务并与 worker 异步处理它们。它由Redis提供支持,旨在实现可扩展且易于上手。 特性: 保证至少执行一次任务 任务调度 失败任务的重试 工作人员崩溃时自动恢复任务 加权优先级队列 严格的优先队列 添加任务的延迟低,因为 Redis 中的写入速度很快 使用唯一选项对任务进行重复数据删除 允许每个任务超时和截止日期 允许聚合任务组以批处理多个连续操作 支持中间件的灵活处理程序接口 能够暂停队列以停止处理队列中的任务 定期任务 支持 Redis Cluster实现自动分片和高可用 支持 Redis Sentinels以实现高可用性 与Prometheus集成以收集和可视化队列指标 用于检查和远程控制队列和任务的Web UI CLI检查和远程控制队列和任务 总的来说: 分布式任务队列:Asynq提供了一个任务队列,可以分布式地处理异步任务,使得任务可以在多个处理器之间分配。 可靠性:Asynq具有高可靠性,可以确保任务不会丢失或重复执行。 异常处理:Asynq提供了对任务异常的处理机制,以便在任务执行失败时进行重试或处理。 优先级和延迟任务:Asynq允许您为任务设置优先级和延迟执行,以便您可以控制任务的执行顺序。 Web UI和CLI:Asynq提供了一个易于使用的Web UI和CLI工具,可以方便地监控和管理异步任务。 本文主要记录Asynq的入门、基本使用和工作原理。 安装 使用go get命令安装Asynq库 go get -u github.com/hibiken/asynq 工作原理 高级概述: Client客户端将任务放入队列 Server服务器从队列中拉取任务并为每个任务启动一个工作协程 任务由多个worker同时处理 Client 创建异步任务 asynq.

Kratos Middleware是Kratos的核心一个中间件层,它是Kratos架构的核心部分,负责处理所有微服务间的通信和数据流。本文将深入了解Kratos Middleware的工作原理,更好地理解和使用Kratos。

使用示例

Kratos 内置了一系列的 middleware(中间件)用于处理 logging、 metrics 等通用场景, 您也可以通过实现 Middleware 接口,开发自定义 middleware,进行通用的业务处理,比如用户登录鉴权等。

// http
// 定义opts
var opts = []http.ServerOption{
    http.Middleware(
        recovery.Recovery(), // 把middleware按照需要的顺序加入
        tracing.Server(),
        logging.Server(),
    ),
}
// 创建server
http.NewServer(opts...)

自定义中间件的例子:

func Middleware1() middleware.Middleware {
    return func(handler middleware.Handler) middleware.Handler {
        return func(ctx context.Context, req interface{}) (reply interface{}, err error) {
            if tr, ok := transport.FromServerContext(ctx); ok {
                // Do something on entering
                defer func() {
                // Do something on exiting
                 }()
            }
            return handler(ctx, req)
        }
    }
}

链式调用

Kratos Middleware是通过链式结构实现的,其中每一个中间件都代表一个独立的处理单元,可以在请求的生命周期中的不同阶段执行不同的操作,如请求前的预处理、请求后的后处理等。

每一个中间件都是一个函数,具有相同的签名:

// Handler定义中间件调用的处理程序
type Handler func(ctx context.Context, req interface{}) (interface{}, error)

再看看/go-kratos/kratos/blob/main/middleware/middleware包中Middleware和Chain的实现,Middleware 是HTTP/gRPC传输中间件。 Chain返回一个中间件,它指定endpoint.的链式处理程序。

type Middleware func(Handler) Handler

func Chain(m ...Middleware) Middleware {
	return func(next Handler) Handler {
		for i := len(m) - 1; i >= 0; i-- {
			next = m[i](next)
		}
		return next
	}
}

Example,以https://github.com/go-kratos/kratos/blob/main/middleware/middleware_test.go为例:

func TestChain(t *testing.T) {
	next := func(ctx context.Context, req interface{}) (interface{}, error) {
		t.Log(req)
		i += 10
		return "reply", nil
	}

	got, err := Chain(test1Middleware, test2Middleware, test3Middleware)(next)(context.Background(), "hello kratos!")
	if err != nil {
		t.Errorf("expect %v, got %v", nil, err)
        }
  }
  
func test1Middleware(handler Handler) Handler {
	return func(ctx context.Context, req interface{}) (reply interface{}, err error) {
		fmt.Println("test1 before")
		i++
		reply, err = handler(ctx, req)
		fmt.Println("test1 after")
		return
	}
}  
... 篇幅限制省略部分代码
OutPut
test1 before
test2 before
test3 before
    middleware_test.go:14: hello kratos!
test3 after
test2 after
test1 after

调用过程

对链式调用的使用有所了解后,接着来看Middleware 调用组织过程,大致归纳为:

  1. option方式配置中间件
  2. url路由匹配中间件
  3. 组装中间件添加到调用链
  4. 注册到gorilla,其中gorilla实现net.hander,设置为服务路由中间件

以Transport.Server Http服务为例,重点关注middleware和router 。

// Server is an HTTP server wrapper.
type Server struct {
	*http.Server
	middleware  matcher.Matcher
	router      *mux.Router
	...
}
// Matcher is a middleware matcher.
type Matcher interface {
	Use(ms ...middleware.Middleware)
	Add(selector string, ms ...middleware.Middleware)
	Match(operation string) []middleware.Middleware
}
  • Matcher是一个中间件匹配器,operation 为path,实现路由查找。
  • router 为gorilla.Mux, Http路由器和Url匹配器

一个完成的调用api-service:

func _Greeter_SayHello0_HTTP_Handler(srv GreeterHTTPServer) func(ctx http.Context) error {
	return func(ctx http.Context) error {
		var in HelloRequest
		if err := ctx.BindQuery(&in); err != nil {
			return err
		}
		if err := ctx.BindVars(&in); err != nil {
			return err
		}
		http.SetOperation(ctx, "/helloworld.v1.Greeter/SayHello")
		h := ctx.Middleware(func(ctx context.Context, req interface{}) (interface{}, error) {
			return srv.SayHello(ctx, req.(*HelloRequest))
		})
		out, err := h(ctx, &in)
		if err != nil {
			return err
		}
		reply := out.(*HelloReply)
		return ctx.Result(200, reply)
	}
}

其中ctx.Middleware的实现是:

func (c *wrapper) Middleware(h middleware.Handler) middleware.Handler {
	if tr, ok := transport.FromServerContext(c.req.Context()); ok {
		return middleware.Chain(c.router.srv.middleware.Match(tr.Operation())...)(h)
	}
	return middleware.Chain(c.router.srv.middleware.Match(c.req.URL.Path)...)(h)
} 

func (m *matcher) Match(operation string) []middleware.Middleware {
	ms := make([]middleware.Middleware, 0, len(m.defaults))
	if len(m.defaults) > 0 {
		ms = append(ms, m.defaults...)
	}
	if next, ok := m.matchs[operation]; ok {
		return append(ms, next...)
	}
	for _, prefix := range m.prefix {
		if strings.HasPrefix(operation, prefix) {
			return append(ms, m.matchs[prefix]...)
		}
	}
	return ms
}

以optionnal方式定义的中间件存储m.defaults,通过matcher.Match给不同的路由匹配不同的中间件。最终加入到 middleware.Chain的调用链实现链式调用。

小结

文本记录深入理解Kratos框架和Kratos Middleware,核心使用middleware.Chain的链式调用具有相同的签名的中间件函数。框架通过option的方式提供设置中间件, Matcher中间件匹配器匹配不同路由规则的中间件,来实现链式调用。

参考

gorilla/mux一个良好质量的Http路由器和Url匹配器,很好补充net/http包HTTP协议复杂的路由请求。本文主要记录gorilla的基础使用、和常用功能提取网址参数、路径前缀和子路由器、中间件。

基础使用

安装

go get -u github.com/gorilla/mux

创建路由器

通过 mux.NewRouter() 创建路由器,稍后将作为参数传递给服务器。它将接收所有 HTTP 连接并将其传递给您将在其上注册的请求处理程序。

package main

import (
	"fmt"
	"github.com/gorilla/mux"
	"net/http"
)

func main() {
	//创建一个新的路由器
	r := mux.NewRouter()

	//等效于http.HandleFunc()工作原理
	r.HandleFunc("/foo", func(w http.ResponseWriter, r *http.Request) {
		fmt.Fprintf(w, "foo")
	})
	//等效于http.Handler()工作原理
	handler := http.Handler(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		fmt.Println("bar")
		fmt.Fprintf(w, "bar")
	}))
	r.Handle("/foo", handler)
    //作为服务参数传入
   http.ListenAndServe(":80", r) 
  }  
  //请求http://yourip:80/foo  http://yourip:80/bar 
  //output
  //foo bar 

网址参数

gorilla/mux的优势之一是:从url中提前参数,例如一个restful api:/country/{country}/province/{province} ,从request参数

oneHander := func(w http.ResponseWriter, r *http.Request) {
		vars := mux.Vars(r)
		c := vars["country"]
		p := vars["province"]
		fmt.Printf("Welcome to  %s %s\n", c, p)
		fmt.Fprintf(w, "Welcome to  %s %s\n", c, p)
	}
	//URL 中提取段/变量
	r.HandleFunc("/country/{country}/province/{province}", oneHander)
    //请求 http://yourip/country/china/province/fujian   
    //output
    //Welcome to china fujian

路径前缀和子路由器

有些场景将请求处理程序限制为特定路径前缀,比如api、admin、cheat 或 v1、v2、v3 或 beta 、prod。

	allHanler := func(w http.ResponseWriter, r *http.Request) {
		vars := mux.Vars(r)
		c := vars["country"]
		p := vars["province"]
		fmt.Fprintf(w, "Welcome to %s %s\n", c, p)
	}
	//路径前缀和子路由器
	countryRouter := r.PathPrefix("/country").Subrouter()
	countryRouter.HandleFunc("/", allHanler)
	countryRouter.HandleFunc("/{province}", oneHander)
    //请求 http://yourip/country/china  
    //output
    //Welcome to china

中间件

Mux 支持向Router添加中间件,如果找到匹配项,中间件将按照添加的顺序执行,包括其子路由器。

func Middleware1(r *mux.Router) mux.MiddlewareFunc {
	return func(next http.Handler) http.Handler {
		return http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
			fmt.Println("do something before handler")
			next.ServeHTTP(w, req)
			fmt.Println("do something after handler")
		})
	}
}

	//中间件
	middleware1 := Middleware1(r)
	r.Use([]mux.MiddlewareFunc{middleware1}...)
    //请求 http://yourip/country/china/province/fujian   
    //output
    //do something before handler
    //Welcome to  china fujian
    //do something after handler

参考

写在前面

随着分布式系统的快速发展和广泛应用,如何处理共享资源的互斥访问场景呢?即分布式锁,常见采用有开源的 MySQL,Redis,ZooKeeper,Etcd 等三方组件来实现。今天主要介绍Golang基于Redis实现分布式互斥锁。

分布式锁特性

那么分布式锁需要处理哪些问题场景呢,或者说特性有哪些? 这是redis.io的原文:

安全和活力保证:

  1. 安全特性:互斥。在任何给定时刻,只有一个客户端可以持有一把锁。
  2. 活性属性 A:无死锁。最终总是有可能获得锁,即使锁定资源的客户端崩溃或分区。
  3. 活性属性 B:容错性。只要大多数 Redis 节点正常运行,客户端就可以获取和释放锁

结合实际场景,翻译一下:

  1. 互斥性。锁的基础特性,只能被第一个持有者持有;
  2. 防死锁。在持有方发生异常时,设置锁的超时释放机制,规避死锁风险
  3. 高可用、高性能。

go-redsyn

Redsync 为 Go 提供了基于 Redis 的分布式互斥锁实现 安装:

$ go get github.com/go-redsync/redsync/v4

例子:

package main

import (
	goredislib "github.com/go-redis/redis/v8"
	"github.com/go-redsync/redsync/v4"
	"github.com/go-redsync/redsync/v4/redis/goredis/v8"
)

func main() {
	//使用go-redis(或redigo)创建一个池,该池是redisync will
//在与Redis通信时使用这也可以是任何一个池子
//实现' redis. properties '池的接口。
	client := goredislib.NewClient(&goredislib.Options{
		Addr: "localhost:6379",
	})
    
    //redis集群
//client := goredislib.NewClusterClient(&goredislib.ClusterOptions{        //Addr: []string{"localhost:6379"},    })

	pool := goredis.NewPool(client) // or, pool := redigo.NewPool(...)

	//创建redisync实例,用于获得互斥锁。
	rs := redsync.New(pool)

	//为所有需要互斥对象的实例使用相同的名称来获取一个新的互斥对象
//相同的锁。
	mutexname := "my-global-mutex"
	mutex := rs.NewMutex(mutexname)

	//获取互斥锁在这个成功之后,没有其他人
//可以获得相同的锁(相同的互斥锁名),直到我们解锁它。
	if err := mutex.Lock(); err != nil {
		panic(err)
	}

	//做你需要锁的工作。
    
//释放锁,以便其他进程或线程可以获得锁。
	if ok, err := mutex.Unlock(); !ok || err != nil {
		panic("unlock failed")
	}
}

实现原理

加锁

在New一个互斥锁mutex后,通过mutex.Lock()加锁,查看源码实现:

func (m *Mutex) acquire(ctx context.Context, pool redis.Pool, value string) (bool, error) {
	conn, err := pool.Get(ctx)
	if err != nil {
		return false, err
	}
	defer conn.Close()
	reply, err := conn.SetNX(m.name, value, m.expiry)
	if err != nil {
		return false, err
	}
	return reply, nil
}

可见,是通过Redis命令Setnx操作来实现。

Redis Setnx(SET if Not eXists) 命令在指定的 key 不存在时,为 key 设置指定的值。设置成功,返回 1 。 设置失败,返回 0 。相对于Set,Setnx具有原子性。

释放锁

func (m *Mutex) release(ctx context.Context, pool redis.Pool, value string) (bool, error) {
	conn, err := pool.Get(ctx)
	if err != nil {
		return false, err
	}
	defer conn.Close()
	status, err := conn.Eval(deleteScript, m.name, value)
	if err != nil {
		return false, err
	}
	return status != int64(0), nil
}

var deleteScript = redis.NewScript(1, `
	if redis.call("GET", KEYS[1]) == ARGV[1] then
		return redis.call("DEL", KEYS[1])
	else
		return 0
	end
`)

可见,Redis使用Lua脚本命令来Delete Key操作。

这里需要关注到Del的时候,需要传入value,value是在加锁时设置的随机唯一值。在删除前进行校验value是否一致,防止误删

锁超时-防死锁

如果持有锁的系统宕机、异常了导致没法release锁,那么其他方就无法活动该锁,导致死锁。

// NewMutex returns a new distributed mutex with given name.
func (r *Redsync) NewMutex(name string, options ...Option) *Mutex {
	m := &Mutex{
		name:   name,
		expiry: 8 * time.Second,
		tries:  32,
		delayFunc: func(tries int) time.Duration {
			return time.Duration(rand.Intn(maxRetryDelayMilliSec-minRetryDelayMilliSec)+minRetryDelayMilliSec) * time.Millisecond
		},
		genValueFunc:  genValue,
		driftFactor:   0.01,
		timeoutFactor: 0.05,
		quorum:        len(r.pools)/2 + 1,
		pools:         r.pools,
	}
	for _, o := range options {
		o.Apply(m)
	}
	return m
}

可见,Mutex.expiy 设置了超时时间,默认为8秒,可以通过Option修改超时时间

// WithExpiry can be used to set the expiry of a mutex to the given value.
func WithExpiry(expiry time.Duration) Option {
	return OptionFunc(func(m *Mutex) {
		m.expiry = expiry
	})
}

Example(设置超时时间、重复加锁):

//设置锁超时3秒
mutex := rs.NewMutex(mutexname,redsync.WithExpiry(time.Second*3)
	if err := mutex.Lock(); err != nil {
		panic(err)
	}
    //重复加锁
	if err := mutex.Lock(); err != nil {
		panic(err)
	}
	if err := mutex.Lock(); err != nil {
		panic(err)
	}
OutPut

panic: lock already taken, locked nodes: [0]

Example(设置超时时间、超时释放锁):

//设置锁超时10秒
mutex := rs.NewMutex(mutexname,redsync.WithExpiry(time.Second*10)
	if err := mutex.Lock(); err != nil {
		panic(err)
	}
    //11秒后锁超时,释放
    time.Sleep(11 * time.Second)
    //重复加锁但已释放、无异常
	if err := mutex.Lock(); err != nil {
		panic(err)
	}
	if err := mutex.Lock(); err != nil {
		panic(err)
	}
OutPut

总结

本文主要介绍分布式锁的基础概念、特性互斥性、防死锁、高可用高性能。以及golang基于redis,使用go-redsync库实现分布式锁。通过例子简单介绍go-redsync的入门使用,以及通过源码阅读来刨析简介实现原理: 1.利用Redis Setnx原子性实现互斥性 ,2.利用Redis执行lua脚本、检查value值避免误删操作, 3.通过设置ExpiryOption设置超时时间,实现锁超时、避免死锁。

参考

本文简要介绍go实现lint和golangci-lint使用。

what

linter为静态代码检查、分析工具,golang常见的有govet\golint\errcheck 等。通过工具其一,可以提前发现一些语法问题,比如变量作用域问题、数组下标越界、内存泄露等;其二可以根据团队规范定制lint规则,提升可读性,可维护性。

golang的linters比较多,比如 awesome-go-linters中 golint检查变量、函数名、注释等。errcheck为未检查错误等。golangci-lint是一个 Go linters 聚合器,通过配置来选择需要的linters。其特征如下:

  • ⚡非常快:并行运行linters,重用Go构建缓存和缓存分析结果。
  • ⚙️基于yaml的配置
  • 🖥集成VS Code, Sublime Text, GoLand, GNU Emacs, Vim, Atom, GitHub动作。
  • 🥇包含了很多linters,不需要安装它们。
  • 📈由于调优的默认设置导致的最小误报数。
  • 🔥漂亮的输出颜色,源代码行和标记的标识符。

install

mac

macOS你可以使用brew在macOS上安装二进制版本:

brew install golangci-lint
brew upgrade golangci-lint

Install from Source

源码安装:

> go install github.com/golangci/golangci-lint/cmd/golangci-lint@v1.50.1

查看安装是否成功:

>golangci-lint --version
>golangci-lint has version v1.50.1 built from (unknown, mod sum: "h1:C829clMcZXEORakZlwpk7M4iDw2XiwxxKaG504SZ9zY=") on (unknown)

how

GolangCI-Lint 可以零配置使用。默认情况下启用以下 linters

>golangci-lint help linters
>Enabled by default linters:
errcheck: Errcheck is a program for checking for unchecked errors in go programs. These unchecked errors can be critical bugs in some cases [fast: false, auto-fix: false]
gosimple (megacheck): Linter for Go source code that specializes in simplifying code [fast: false, auto-fix: false]
govet (vet, vetshadow): Vet examines Go source code and reports suspicious constructs, such as Printf calls whose arguments do not align with the format string [fast: false, auto-fix: false]
ineffassign: Detects when assignments to existing variables are not used [fast: true, auto-fix: false]
staticcheck (megacheck): It's a set of rules from staticcheck. It's not the same thing as the staticcheck binary. The author of staticcheck doesn't support or approve the use of staticcheck as a library inside golangci-lint. [fast: false, auto-fix: false]
typecheck: Like the front-end of a Go compiler, parses and type-checks Go code [fast: false, auto-fix: false]
unused (megacheck): Checks Go code for unused constants, variables, functions and types [fast: false, auto-fix: false]

如何执行golangci-lint呢?在需要执行的项目目录下:

golangci-lint run

这相当于执行

golangci-lint run ./...

您可以选择要分析的目录和文件:

golangci-lint run dir1 dir2/... dir3/file1.go

接着,我们创建一个Example代码:linter_example.go

package main

import "fmt"

type People struct {
	Name string
}

func (p *People) String() string {
	return fmt.Sprintf("print: %v", p)
}

func main() {
	p := &People{}
	p.String()
}


执行lint,如果没有相关的配置文件,会走默认的lint配置

> golangci-lint run
level=warning msg="[runner] The linter 'golint' is deprecated (since v1.41.0) due to: The repository of the linter has been archived b
y the owner.  Replaced by revive."
linter_example.go:5:6: exported type `People` should have comment or be unexported (golint)
type People struct {
     ^
linter_example.go:10:9: printf: fmt.Sprintf format %v with arg p causes recursive (*demos/linter-demo.People).String method call (gove
t)
        return fmt.Sprintf("print: %v", p)
               ^
linter_example.go:15:10: unusedresult: result of (*demos/linter-demo.People).String call not used (govet)
        p.String()
                ^

我们可以根据lint提示修改

 printf: fmt。Sprintf格式%v与arg p导致递归( demos/ lininter -demo. people)。字符串方法调用(govet)。

在使⽤  fmt 包中的打印⽅法时,如果类型实现了这个接⼝,会直接调⽤。⽽中打印  p 的时候会直接调⽤  p 实现的  String() ⽅法,然后就产⽣了循环调⽤。

Custom Configuration

GolangCI-Lint 通过当前工作目录的以下路径中增加配置文件:

  • .golangci.yml
  • .golangci.yaml
  • .golangci.toml
  • .golangci.json

来实现自定义linters

例如我们添加了.golangci.yml

run:
  timeout: 5m
  modules-download-mode: readonly

linters:
  disable-all: true
  enable: # 下面是开启的lint列表
    - errcheck
    - goimports
    - golint
   # - govet 为了测试是否生效,关闭
    - staticcheck

linters-settings:
  golint:
    # minimal confidence for issues, default is 0.8
    min-confidence: 0.8

issues:
  exclude-use-default: false
  max-issues-per-linter: 0
  max-same-issues: 0

这里我们把govet 关闭了,在执行run

golangci-lint run
level=warning msg="[runner] The linter 'golint' is deprecated (since v1.41.0) due to: The repository of the linter has been archived b
y the owner.  Replaced by revive."
linter_example.go:5:6: exported type `People` should have comment or be unexported (golint)
type People struct {
     ^

可见关闭govet生效了。

reference

jefffff

Stay hungry. Stay Foolish COOL

Go backend developer

China Amoy