メインコンテンツまでスキップ

· 約1分
wen

问题

当你在同一台电脑上使用多个 Git 用户的时候,你可能会遇到在 commit push 之后才发现自己没有切换到正确的用户的问题。

为了避免这种情况,我们可以在终端显示当前的 Git 用户信息。

img

解决方案

终端的显示信息(Shell Prompt) ,我推荐 Starship 配置来实现。

安装 Starship 可以参考官方文档, 这里就不再赘述。

配置

安装完后在 Starship 的配置文件 ~/.config/starship.toml 中添加以下配置, 格式可以按照自己的喜好修改 format = 部分 。

~/.config/starship.toml
format = """
...
${custom.git_username}\
...

[custom.git_username]
command = "git config user.name"
when = "[ -d .git ] && echo .git || git rev-parse --git-dir > /dev/null 2>&1"
format = ' [$symbol($output)@git]($style) '

· 約2分
wen

问题

Docusaurus(3.0) 的文档侧边栏默认是按文件的创建日期的升序排列的。

这样的排序方式对于我管理和查找文档来说并不是很方便。

我更喜欢按照日期倒序排列,这样我就可以更容易地找到最新的文档。

img

解决方案

修改 docusaurus.config.js 文件,如下所示:

docusaurus.config.js
+// Reverse the sidebar items ordering (including nested category items)
+function reverseSidebarItems(items) {
+ // Reverse items in categories
+ const result = items.map((item) => {
+ if (item.type === 'category') {
+ return {...item, items: reverseSidebarItems(item.items)};
+ }
+ return item;
+ });
+ // Reverse items at current level
+ result.reverse();
+ return result;
+}

/** @type {import('@docusaurus/types').Config} */
const config = {
title: 'thewang',
...
presets: [
[
'classic',
/** @type {import('@docusaurus/preset-classic').Options} */
({
docs: {
routeBasePath: 'weekly',
sidebarPath: require.resolve('./sidebars.js'),
showLastUpdateTime: true,
showLastUpdateAuthor: true,
sidebarCollapsed: false,
+ async sidebarItemsGenerator({defaultSidebarItemsGenerator, ...args}) {
+ const sidebarItems = await defaultSidebarItemsGenerator(args);
+ return reverseSidebarItems(sidebarItems);
+ return sidebarItems;
+ },
},
...

References

Customize the sidebar items generator

· 約5分
wen

背景

img

  • 获取两个切片之间的公共元素还是一个比较常见的需求,但是在 Code Review 的过程中,我发现还是会有一些人会用双重循环来实现。(这样的时间复杂度是 O(n^2),效率比较低)
  • 最近 Golang 用的多,顺便分享一下 Golang 中如何获取两个切片之间的公共元素的方法。

❌ 用双重循环实现的代码例子:

func intersection(nums1 []int, nums2 []int) []int {
var result []int

// 双重循环 O(n^2)
for _, v1 := range nums1 { // O(n) 外循环
for _, v2 := range nums2 { // O(n) 内循环
if v1 == v2 {
result = append(result, v1)
}
}
}
return result
}

改善方案

把上面例子中的内循环中的元素查找改成用 set* 来实现,这样内循环部分的时间复杂度就可以降低到 O(1),整体的时间复杂度就可以降低到 O(n)

Javascript 中的 set 解释

Set 对象是值的合集(collection)。集合(set)中的元素只会出现一次,即集合中的元素是唯一的。

规范要求集合的实现是"对合集中的元素的平均访问时间与集合中元素的数量呈次线性关系"。

因此,它可以在内部表示为哈希表(查找的时间复杂度为 O(1))、搜索树(查找的时间复杂度为 O(log(N)))或任何其他的时间复杂度低于 O(N) 的数据结构。

参考链接

set 来实现的例子

func intersection(nums1 []int, nums2 []int) []int {
var result []int

// 把 nums2 转换成 set,这样在 nums2 中查找元素的时间复杂度就变成了 O(1)。
// 考虑性能优化的话,可以把 nums1 和 nums2 中的元素数量进行比较,把数量多的那个切片转换成 set。
set := make(map[int]struct{}) // golang 中的 没有 set,用 map 来实现。struct{} 是一个空结构体,用来节省内存。
for _, v := range nums2 {
set[v] = struct{}{}
}

// 遍历 nums1,如果 nums1 中的元素在 nums2 中存在,就把它加入到 result 中
for _, v := range nums1 {
if _, ok := set[v]; ok {
result = append(result, v)
}
}
}

使用第三方库实现

考虑性能优化以及各种类型的切片,我们可以使用下面的第三方库来实现。

deckarep/golang-set

如其名,Golang 的 set 实现。

import (
"fmt"
mapset "github.com/deckarep/golang-set/v2"
)

func main() {
set1 := mapset.NewSet[string]()
set1.Add("a")
set1.Add("b")
set1.Add("c")

set2 := mapset.NewSet[string]()
set2.Add("c")
set2.Add("d")
set2.Add("e")

// 交集
intersectionSet := set1.Intersect(set2)
fmt.Println(intersectionSet) // Set{c}

// 除了交集,还支持并集、差集、对称差集等操作
// 并集
unionSet := set1.Union(set2)
fmt.Println(unionSet) // Set{a, b, c, d, e}

// 差集
diffSet := set1.Difference(set2)
fmt.Println(diffSet) // Set{a, b}

// 对称差集
symDiffSet := set1.SymmetricDifference(set2)
fmt.Println(symDiffSet) // Set{a, b, d, e}
}

samber/lo

如果你除了要对切片进行交集操作,还需要对切片等进行排序、分组等操作,那么可以考虑使用 samber/lo 这个库。

你可以把它理解成 lodash 的 Golang 版本。

import (
"github.com/samber/lo"
)

func main() {
// 交集
lo.Intersection([]int{1, 2, 3}, []int{2, 3, 4}) // return []int{2, 3}

// 并集
lo.Union([]int{1, 2, 3}, []int{2, 3, 4}) //return []int{1, 2, 3, 4}

// 差集
lo.Difference([]int{1, 2, 3}, []int{2, 3, 4}) // return []int{1}, []int{4}
}

· 約2分
wen

Symmetric API testing 是啥?

这个概念应该是源自于 Gopher Academy Blog

作者在维护一个 Golang 的 Twitter API 客户端,为了对 Twitter 的 API 进行测试,所以作者提出了 Symmetric API testing 的概念。

简单地说就是保存 API 的返回结果,然后在测试的时候,用保存的结果来进行测试。

这样就不用编写 mock 和 测试用例了。

至于名字为什么叫 Symmetric, 是相对于传统的需要编写 mock 和 测试用例的方式 Asymmetric 而言的。

其实个人觉得把它叫做 SnapShot Testing 更为合适。

怎么实现?

除了手动保存 API 的返回结果,还可以使用 go-vcr 这个库来实现。

大概的代码如下:

r, err := recorder.New("<filename>")
if err != nil {
return err
}
defer r.Stop()
client.Transport = r
res, err := client.Get("http://api.twitter.com/...")
if err != nil {
return err
}

这里提供了完整的 示例代码

Reference

Symmetric API Testing

Symmetric API Testing という、手間なく堅牢に外部 API Client をテストする手法

go-vcr を使った Symmetric API Testing のメモ

· 約3分
wen

背景&需求

在 Golang 中,我们经常会遇到需要解析 JSON 数据的场景,比如从 HTTP 请求中获取 JSON 数据,或者从文件中读取 JSON 数据。

通常我们会提前定义好对应的结构体,然后才能将 JSON 数据解析到结构体中。

比如:

type User struct {
Name string `json:"name"`
Age int `json:"age"`
}

func main() {
jsonStr := `{"name": "wen", "age": 18}`
var user User
json.Unmarshal([]byte(jsonStr), &user)
fmt.Println(user)
}

但是有时候我们并不知道 JSON 数据的结构,或者 JSON 数据的结构会经常变化,这时候我们就无法提前定义好对应的结构体。

解决方案

可以使用 map[string]any (Golang1.18 之前的话 map[string]interface{} ) 来解析 JSON 数据,这样就不需要提前定义结构体了。

func main() {
jsonStr := `{"name": "wen", "age": 18}`
var user map[string]any
json.Unmarshal([]byte(jsonStr), &user)
fmt.Println(user)

// 获取具体的值
fmt.Println(user["name"])
fmt.Println(user["age"])
}

扩展

如果觉得 map[string]any 这种方式解析速度比较慢,可以使用 jsonparser 这个库来解析,速度会快很多。

我用 User 结构体来测试了一下,解析速度快了 8-9 倍左右 🚀

其他的比较大的 JSON 数据,解析速度也会快很多,具体可以看下这里的 benchmark

NameIterationsns/op
BenchmarkEncodingJsonInterfaceUser-122540230460.6 ns/op
BenchmarkJsonParserUser-122141329655.91 ns/op
查看测试代码
// Just for emulating field access, so it will not throw "evaluated but not used"
func nothing(_ ...interface{}) {}

// 使用 jsonparser
func BenchmarkJsonParserUser(b *testing.B) {
for i := 0; i < b.N; i++ {
jsonparser.Get(user, "name")
jsonparser.Get(user, "age")
nothing()
}
}

// 使用 map[string]any
func BenchmarkEncodingJsonInterfaceUser(b *testing.B) {
for i := 0; i <details b.N; i++ {
var data interface{}
json.Unmarshal(user, &data)
m := data.(map[string]interface{})

nothing(m["name"].(string), m["age"])
}
}

· 約3分
wen

0. 背景&需求

我的博客是使用 Cloudflare Pages 部署的,

但是 Cloudflare Pages 并没有提供部署状态通知的功能,所以我想到了使用 Github Actions 来实现这个功能。

1. 前提

  • 已经在 Github 上创建了你的网站的仓库
  • 已经在 Cloudflare Pages 上设置好了你的 Github 仓库
    • 比如我是设置成了 main 分支更新时自动部署

2. 方法

3. 实现

arddluma/cloudflare-pages-slack-notification 这个 Github Action 已经实现这个了我们所要的功能。

我们只需要准备好这个 Github Action 中使用的变量。

- name: Await CF Pages and send Slack notification
uses: arddluma/cloudflare-pages-slack-notification@v4
with:
# Cloudflare API token
apiToken: ${{ secrets.CF_API_TOKEN }}
# CloudFlare account ID
accountId: ${{ secrets.CF_ACC_ID }}
# CloudFlare Pages project name
project: ${{ secrets.CF_PAGES_PROJECT }}
# Create Slack Incoming webhook and add as variable https://hooks.slack.com/...
slackWebHook: ${{ secrets.SLACK_WEBHOOK }}
# Add this if you want to wait for a deployment triggered by a specfied commit
commitHash: ${{ steps.push-changes.outputs.commit-hash }}

3.1 获得变量

1️⃣ 2️⃣ 3️⃣ 是 Github Actions 通过 Cloudflare Pages 的 API 来获取部署状态所需要的参数。

4️⃣ 是 Github Actions 发送消息给 Slack 所需要的 webhook URL。

3.2 设置变量

在你的网站的 Github 仓库中,点击 Settings -> Secrets and variables -> New repository secret,然后设置上面的变量。

img

4. Github Actions 代码

github/workflows/cloudflare-pages.yml
name: Detect Cloudflare pages deployment status and notify to Slack
on:
push:
branches:
- main
paths-ignore:
- .github/**
pull_request:
branches:
- main
types: [closed]
paths-ignore:
- .github/**
workflow_dispatch:
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Await CF Pages and send Slack notification
id: cf-pages
uses: arddluma/cloudflare-pages-slack-notification@v4
with:
# Clouodflare API token
apiToken: ${{ secrets.CF_API_TOKEN }}
# CloudFlare account ID
accountId: ${{ secrets.CF_ACC_ID }}
# CloudFlare Pages project name
project: ${{ secrets.CF_PAGES_PROJECT }}
# Create Slack Incoming webhook and add as variable https://hooks.slack.com/...
slackWebHook: ${{ secrets.SLACK_WEBHOOK }}
# Add this if you want to wait for a deployment triggered by a specfied commit
commitHash: ${{ steps.push-changes.outputs.commit-hash }}

效果

  • Github Action Build Progress: img

  • Slack notification: img

Reference

Setup Cloudflare Pages Slack notifications

· 約2分
wen

问题

Docuaurus 的文档(Docs)默认的 URL 路径名是 /docs,如:https://thewang.net/docs

如果你想把它改成 /api,如:https://thewang.net/api ,该怎么做呢?

解决方法

docusaurus.config.js 中,

  • 找到 docs 的配置项,添加 routeBasePath 属性,值为你想要的 URL 路径名,如:api
  • 找到 navbar 的配置项,
docusaurus.config.js
  presets: [
[
'classic',
/** @type {import('@docusaurus/preset-classic').Options} */
({
docs: {
+ routeBasePath: 'api',
sidebarPath: require.resolve('./sidebars.js'),
...
docusaurus.config.js
     navbar: {
...
items: [
{ to: '/blog', label: 'Blog', position: 'left' },
// API docs
{
to: '/api',
type: 'docSidebar',
label: 'API',
...
},

补充

那那些已经公开的文档(Docs)的 URL 路径不能访问了該怎么办呢?

可以安装 docusaurus/plugin-client-redirects 插件,来实现重定向解决。

  npm install @docusaurus/[email protected] // 注意和你的 docusaurus 版本对应
docusaurus.config.js
...
+ plugins: [
+ [
+ '@docusaurus/plugin-client-redirects',
+ {
+ createRedirects(existingPath) {
+ if (existingPath.includes('/api')) {
+ // Redirect from /docs to /weekly
+ return [
+ existingPath.replace('/api', '/docs'),
+ ];
+ }
+ return undefined; // Return a falsy value: no redirect created
+ },
+ },
+ ]
+ ],

presets: [
...
PRODUCTION ONLY

This plugin is always inactive in development and only active in production because it works on the build output.

@docusaurus/plugin-client-redirects 只在生产环境下生效。

· 約2分
wen

背景

以前经常用 plantuml 来生成各种示意图。

自从 Github 支持了 mermaid 语法后,就慢慢的转向了 mermaid。

今天就来记录一下如何使用 mermaid 来绘制 Gitflow 的示意图。

ヒント

Mermaid 是一个基于 JavaScript 的图表绘制工具,

它使用简单的文本描述来定义图表,然后将文本转换为图表。

Mermaid 支持流程图、序列图、类图、状态图、Git 图、甘特图等多种图表。

官方还提供了一个在线编辑器

1. Gitflow 的 Mermaid 代码

gitGraph LR:
commit id: "1:init"
branch "hotfix/{ticket_number}" order: 1
branch "release/{x}" order: 2
branch develop order: 3
commit id: "2:init"
branch "feature/{ticket_number}" order: 4
checkout "feature/{ticket_number}"
commit id: "3:commit-x"
checkout develop
merge "feature/{ticket_number}" id: "4:merge-feat" type: HIGHLIGHT

# release
checkout "release/{x}"
merge develop id: "5:release-x"
checkout main
merge "release/{x}" id: "6:merge-release" tag: "v.1.0.0" type: HIGHLIGHT

# hotfix
checkout "hotfix/{ticket_number}"
commit id: "7:commit-hotfix"
checkout develop
merge "hotfix/{ticket_number}" id: "8:merge-hotfix" type: HIGHLIGHT

2. Mermaid 代码的渲染结果

似乎 本博客( Docusaurus )的渲染结果 和官方提供的在线编辑器 生成的结果(↓)稍微有点不一样。

不过,大家可以通过修改 Mermaid 的配置 调整显示效果。

img

· 約4分
wen

需求

  • Golang 中将结构体的数组保存到 CSV 文件中
  • CSV 文件的列顺序和结构体中的字段顺序不同
user.go
type User struct {
Name string
Age int
}

↓↓↓

AgeName
20"Musk"

调查

比较有人气的的 Golang CSV 库有:

但是两者都不支持指定结构体字段的顺序,而且也没有提供相应的 writer 接口,所以也无法自己覆盖接口实现。

本来想 fork 上面的其中一个库改造下,但搜索发现了 shigetaichi/xsv 这个库,在 gocsv 的代码的基础上实现了指定结构体字段顺序的功能。

字段的顺序可以通过 xsvSortOrder 属性指定一个数组,数组中的值是结构体中相应字段的索引(starting from 0)。

比如, 如果想要将结构体按照 Age, Name 的顺序输出,可以这样指定:

type User struct {
Name string `csv:"name"`
Age int `csv:"age"`
}

xsvWrite.SortOrder = []int{1, 0}

但是个人觉得字段顺序的指定方式不太友好。 既然已经有了 csv tag,为什么不直接使用 csv tag 来指定字段的顺序呢?

比如:

type User struct {
Name string `csv:"name,order:1"`
Age int `csv:"age,order:0"`
}

我的方案

代码

代码
package main

import (
"fmt"
"log"
"os"
"reflect"
"strconv"
"strings"

"github.com/shigetaichi/xsv"
)

type User struct {
Name string `csv:"name, order:1"`
Age int `csv:"age, order:0"`
}

func main() {
users := []*User{
{Name: "Alice", Age: 20},
{Name: "Bob", Age: 30},
}

// Create a csv file to write
file, err := os.OpenFile("users.csv", os.O_RDWR|os.O_CREATE|os.O_TRUNC, os.ModePerm)
if err != nil {
panic(err)
}
defer file.Close()

xsvWrite := xsv.NewXsvWrite[*User]()
// Get the order of the fields in the struct and set it to the writer
orders := getOrderOfFields(reflect.TypeOf(User{}))
xsvWrite.SortOrder = orders

// Write the users to the csv file
err = xsvWrite.SetFileWriter(file).Write(users)
if err != nil {
log.Println(err)
return
}
}

// Get the order of the fields in specified struct
func getOrderOfFields(structType reflect.Type) []int {
// Create a slice of int with the same length as the number of fields in the struct
var res []int
// Iterate through the struct fields
for i := 0; i < structType.NumField(); i++ {
// Get the field
field := structType.Field(i)

// Get the "order" tag value
order := getTagValue(field, "csv", "order")
if order >= 0 {
res = append(res, order)
}
}

return res
}

// getTagValue は指定されたフィールドの指定されたタグの値を取得する
func getTagValue(field reflect.StructField, tag string, tagField string) int {
tagValue, _ := field.Tag.Lookup(tag)
// Split the tag string by ","
tagParts := strings.Split(tagValue, ",")

// Iterate through the tag parts to find the "order" value
res := -1
for _, part := range tagParts {
part = strings.TrimSpace(part)
// Check if the part starts with "order:"
prefix := fmt.Sprintf("%s:", tagField)
if strings.HasPrefix(part, prefix) {
// Extract the numeric value after "order:"
valueStr := strings.TrimPrefix(part, prefix)
value, err := strconv.Atoi(valueStr)
if err == nil {
res = value
}
break
}
}

return res
}

保存的 CSV 文件

agename
20Alice
30Bob

· 約4分
wen

背景

在使用 Golang 的时候,经常会遇到需要将结构体转换为 JSON 的情况,

但是在转换的时候,JSON 的字段顺序并不是我们想要的,这时候就需要我们自己来指定 JSON 的字段顺序。

解决方案

一个比较简单的方案是利用结构体的 tag 来指定 JSON 的字段顺序,

然后在转换的时候,将结构体的字段按照 tag 中的顺序进行排序。

type User struct {
Name string `json:"name,order:2"`
Age int `json:"age,order:1"`
}

代码实现

package main

import (
"encoding/json"
"fmt"
"reflect"
"sort"
"strconv"
"strings"

//orderedmap "github.com/wk8/go-ordered-map/v2"
"github.com/iancoleman/orderedmap"
)

type User struct {
Name string `json:"name,order:3"`
Age int `json:"age,order:2"`
Score int `json:"score,order:1"`
}

type Address struct {
City string `json:"city,order:10"`
Street string `json:"street,order:9"`
ZipCode string `json:"zip_code,order:8"`
}

func main() {
user := User{
Name: "Wen",
Age: 30,
Score: 100,
}
address := Address{
City: "Hangzhou",
Street: "XiHuDaDao",
ZipCode: "10001",
}

// User構造体を指定した順序でJSONに変換
userJSON, err := MarshalJSONWithOrder(user)
if err != nil {
fmt.Println("Error:", err)
return
}
fmt.Println("User JSON:", string(userJSON))

// Address構造体を指定した順序でJSONに変換
addressJSON, err := MarshalJSONWithOrder(address)
if err != nil {
fmt.Println("Error:", err)
return
}

fmt.Println("Address JSON:", string(addressJSON))
}

// MarshalJSONWithOrder は構造体を指定した順序でJSONに変換する
// 構造体での指定方法: `json:"{struct field name},order:{integer}"`
// 指定例: `json:"name,order:10"`
func MarshalJSONWithOrder(obj interface{}) ([]byte, error) {
val := reflect.ValueOf(obj)
typ := reflect.TypeOf(obj)

// ソート用のスライス
var fields []fieldWithOrder

// フィールドの数だけループ
for i := 0; i < val.NumField(); i++ {
fieldName := typ.Field(i).Name

order := getTagValue(typ.Field(i), "json", "order")
fields = append(fields, fieldWithOrder{
Name: fieldName,
Order: order,
})
}

// フィールドのソート
sort.Slice(fields, func(i, j int) bool {
return fields[i].Order < fields[j].Order
})

// ソート後の順序に従ってJSONを生成
//result := orderedmap.New[string, any]()
result := orderedmap.New()
for _, f := range fields {
//result[f.Name] = val.FieldByName(f.Name).Interface()
result.Set(f.Name, val.FieldByName(f.Name).Interface())
}

// マーシャリング
return json.Marshal(result)
}

// getTagValue は指定されたフィールドの指定されたタグの値を取得する
func getTagValue(field reflect.StructField, tag string, tagField string) int {
tagValue, _ := field.Tag.Lookup(tag)

// Split the tag string by ","
tagParts := strings.Split(tagValue, ",")

// Iterate through the tag parts to find the "order" value
res := -1
for _, part := range tagParts {
// Check if the part starts with "order:"
prefix := fmt.Sprintf("%s:", tagField)
if strings.HasPrefix(part, prefix) {
// Extract the numeric value after "order:"
orderStr := strings.TrimPrefix(part, prefix)
order, err := strconv.Atoi(orderStr)
if err == nil {
res = order
}
}
}

return res
}

// fieldWithOrder はソート用の構造体
type fieldWithOrder struct {
Name string
Order int
}

NOTE

  • Ordered Map 的使用

    Golang 中的 map 是无序的,如果需要有序的 map, 可以使用 wk8/go-ordered-map 或者 iancoleman/orderedmap。 由于后者的性能似乎比较好,代码中采用的是后者。

  • ChatGPT 的使用

    代码大部分是用 ChatGPT 生成的,但是生成的代码中有几个问题,例如上面的 Ordered Map,我尝试让 ChatGPT 修改了几次,都没有成功。 最后还是自己手动修改了代码。

    感觉 ChatGPT 对于代码的细节部分的理解还有待改善,而且有时候还胡说八道(现阶段生成式 AI 的通病)。