Skip to main content

· 2 min read
wen

0. Manim 是什么?

Manim 是一个用于创建数学动画的库,它是由 3Blue1BrownGrant Sanderson @X开发的。

Manim 的目标是提供一个用于创建数学动画的简单、快速且强大的工具。

它是一个用 Python 编写的开源项目,它的代码托管在 GitHub 上。

1. 最简单快速的安装方法: 利用 GitHub Codespaces

1). 打开(或者 fork)我配置好的仓库 wifecooky/manim-devcontainer,

1️⃣ 点击右上角的 Code 按钮, 2️⃣ 选择 Open with Codespaces,等待构建完成后,就可以在浏览器中使用 Manim 了。

img

2). 测试一下

1️⃣ : 打开 example_scenes.py 文件。

2️⃣ : 在 terminal 运行 manim -ql example_scenes.py SquareToCircle

3️⃣ : 等待运行完成,就可以在 media/videos 目录下看到生成的视频了。

img

2. 本地最快速的安装方法: 利用 Docker

0). 安装 Docker

已经安装过的请跳过这一步。

1). 拉取 Manim 镜像

docker pull manimcommunity/manim

2). 使用 Docker 启动本地 JupyterLab

docker run -it -p 8888:8888 manimcommunity/manim jupyter lab --ip=0.0.0.0

3). 打开 JupyterLab 链接并创建一个新的 Notebook

img

img

4). 测试一下

1️⃣ : 在 Notebook 中运行以下代码

%%manim -qm -v WARNING SquareToCircle

class SquareToCircle(Scene):
def construct(self):
square = Square()
circle = Circle()
circle.set_fill(PINK, opacity=0.5)
self.play(Create(square))
self.play(Transform(square, circle))
self.wait()
tip

%%manim 是 Manim 的 Jupyter magic 命令,用于链接 Binder 来运行 Manim。

-qm 参数表示 quality medium,即生成的视频质量为中等。

-v WARNING 参数表示只显示警告信息。

img

Reference

Manim easy Installation for all operating systems (Windows, Linux, Mac OS)

docker manimcommunity/manim

· One min read
wen

Problem

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

Docuaurus v3 's docs sidebar is sorted by the creation date of the file by default.

It's not very convenient for me to manage and find docs, so I want to reverse the order of the sidebar items.

img

Solution

Modify the docusaurus.config.js file as follows:

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

· One min read
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) '

· 3 min read
wen

Background

img

  • It is a common requirement to get the common elements between two slices, but I found that some people still use double loops to implement it during Code Review. (The time complexity is O(n^2), and the efficiency is relatively low)
  • I have been using Golang recently, so I will share how to get the common elements between two slices in Golang.

❌ Code example implemented with double loops:

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

// Double loop O(n^2)
for _, v1 := range nums1 { // O(n) outer loop
for _, v2 := range nums2 { // O(n) inner loop
if v1 == v2 {
result = append(result, v1)
}
}
}
return result
}

Improvement

Change the element lookup in the inner loop in the above example to be implemented using set*,

so that the time complexity of the inner loop part can be reduced to O(1), and the overall time complexity can be reduced to O(n).

Set in Javascript

Set objects are collections of values. A value in the set may only occur once; it is unique in the set's collection.

It could be represented internally as a hash table (with O(1) lookup),

a search tree (with O(log(N)) lookup), or any other data structure, as long as the complexity is better than O(N).

Reference

Implementation example using set

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

// Convert nums2 to set, so that the time complexity of looking up elements in nums2 becomes O(1).
// Consider performance optimization, you can compare the number of elements in nums1 and nums2, and convert the slice with more elements into set.
set := make(map[int]struct{}) // There is no set in Golang, use map to implement. struct{} is an empty structure to save memory.
for _, v := range nums2 {
set[v] = struct{}{}
}

// Traverse nums1, if the element in nums1 exists in nums2, add it to result
for _, v := range nums1 {
if _, ok := set[v]; ok {
result = append(result, v)
}
}
}

Implementation using third-party libraries

If you consider performance optimization and various types of slices, we can use the following third-party libraries to implement.

deckarep/golang-set

As the name suggests, it is a set implementation in Golang.

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")

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

// Besides intersection, it also supports union, difference, symmetric difference, etc.
// 并集 union
unionSet := set1.Union(set2)
fmt.Println(unionSet) // Set{a, b, c, d, e}

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

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

samber/lo

If you need to sort, group, etc. in addition to intersecting slices, you can consider using the samber/lo library.

It is similar to lodash in Javascript.

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

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

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

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

· 2 min read
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 min read
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 min read
wen

0. Background

My blog is hosted on Cloudflare Pages and the source code is on Github.

But Cloudflare Pages does not provide deployment status notification, so I thought of using Github Actions to implement this function.

1. Prerequisites

  • Already created your website's repository on Github
  • Already set up your Github repository on Cloudflare Pages
    • For example, I set it to deploy automatically when the main branch is updated

2. The idea

3. Implementation

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

arddluma/cloudflare-pages-slack-notification Github Action has already implemented the function we want.

So we only need to prepare the variables used in this 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 Get variables

1️⃣ 2️⃣ 3️⃣ are the parameters that Github Actions needs to get the deployment status through the Cloudflare Pages API. 4️⃣ is the webhook URL that Github Actions needs to send messages to Slack.

3.2 Set variables

In your website's Github repository, click Settings -> Secrets and variables -> New repository secret, and then set the variables above.

img

4. Github Actions code

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 }}

The screenshots

  • Github Action Build Progress: img

  • Slack notification: img

Reference

Setup Cloudflare Pages Slack notifications

· 2 min read
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 min read
wen

背景

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

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

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

tip

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 min read
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