Skip to main content

11 posts tagged with "golang"

View All Tags

· 6 min read
wen

让我为您深入浅出地讲解 Golang 的 Channel 概念。

1. 生活类比 🌟

想象一个咖啡店的点单流程:

  • Channel 就像咖啡店的取餐窗口
  • 咖啡师(生产者)将做好的咖啡放在窗口
  • 服务员(消费者)从窗口取走咖啡给客人
  • 窗口有限制:最多放 3-4 杯咖啡
  • 如果窗口满了,咖啡师需要等待
  • 如果窗口空了,服务员需要等待

Golang Channels in Life

2. 技术定义 📚

Channel 是 Go 语言中的一个核心概念,它提供了 goroutine 之间的通信机制,实现了 CSP(Communicating Sequential Processes)模型。

基础示例:

package main

import (
"fmt"
"time"
)

func main() {
// 创建一个容量为 3 的 channel
coffee := make(chan string, 3)

// 生产者 goroutine
go func() {
drinks := []string{"拿铁", "美式", "卡布奇诺"}
for _, drink := range drinks {
fmt.Printf("咖啡师制作了 %s\n", drink)
coffee <- drink // 将咖啡放入 channel
time.Sleep(time.Second)
}
close(coffee) // 关闭 channel
}()

// 消费者(主 goroutine)
for drink := range coffee {
fmt.Printf("服务员取走了 %s\n", drink)
}
}

3. 核心特性表 📊

特性说明示例
缓冲性可以创建带缓冲的 channelch := make(chan int, 3)
阻塞性当 channel 满/空时会阻塞满时发送阻塞,空时接收阻塞
方向性可以限制 channel 的方向chan<- (仅发送) <-chan (仅接收)
关闭性channel 可以被关闭close(ch)

4. 实践案例 💡

让我们实现一个更实用的例子:一个简单的任务处理系统

package main

import (
"fmt"
"time"
)

// Task 代表一个待处理的任务
type Task struct {
ID int
Data string
}

// Worker 代表一个工作协程
func Worker(id int, tasks <-chan Task, results chan<- string) {
for task := range tasks {
// 模拟处理任务
fmt.Printf("Worker %d 开始处理任务 %d\n", id, task.ID)
time.Sleep(time.Second)

// 发送结果
results <- fmt.Sprintf("任务 %d 已被 Worker %d 完成", task.ID, id)
}
}

func main() {
tasks := make(chan Task, 10)
results := make(chan string, 10)

// 启动 3 个 worker
for i := 1; i <= 3; i++ {
go Worker(i, tasks, results)
}

// 发送 5 个任务
for i := 1; i <= 5; i++ {
tasks <- Task{ID: i, Data: fmt.Sprintf("数据-%d", i)}
}
close(tasks)

// 收集所有结果
for i := 1; i <= 5; i++ {
fmt.Println(<-results)
}
}

5. 最佳实践 ⭐

我来介绍 Golang channel 处理超时的几个常用例子。

  1. 使用 time.After

    • 最简单直接的方法
    • 适合一次性的超时检查
    • 使用 selecttime.After 实现
// Example 1: Basic timeout using select and time.After
func example1() {
ch := make(chan string)

// Simulate slow operation
go func() {
time.Sleep(2 * time.Second)
ch <- "data"
}()

select {
case result := <-ch:
fmt.Println("Received:", result)
case <-time.After(1 * time.Second):
fmt.Println("Operation timed out")
}
}
  1. 使用 context.WithTimeout

    • Go 推荐的标准方式
    • 可以传递超时信息到多个 goroutine
    • 支持取消操作
    • 资源会自动清理
// Example 2: Using context for timeout
func example2() {
// Create context with timeout
ctx, cancel := context.WithTimeout(context.Background(), 1*time.Second)
defer cancel()

ch := make(chan string)

// Simulate slow operation
go func() {
time.Sleep(2 * time.Second)
ch <- "data"
}()

select {
case result := <-ch:
fmt.Println("Received:", result)
case <-ctx.Done():
fmt.Println("Operation timed out:", ctx.Err())
}
}
  1. 使用超时通道
    • 使用专门的超时 channel
    • 更灵活但需要手动管理
    • 适合需要自定义超时行为的场景
// Example 3: Custom timeout channel
func example3() {
ch := make(chan string)
timeout := make(chan bool, 1)

// Set timeout
go func() {
time.Sleep(1 * time.Second)
timeout <- true
}()

// Simulate slow operation
go func() {
time.Sleep(2 * time.Second)
ch <- "data"
}()

select {
case result := <-ch:
fmt.Println("Received:", result)
case <-timeout:
fmt.Println("Operation timed out")
}
}

运行这段代码,你会看到三个例子都会因为超时(1 秒)而终止,因为模拟的操作需要 2 秒才能完成。

建议在实际应用中:

  • 对于简单场景,使用 time.After
  • 对于复杂应用,优先使用 context.WithTimeout
  • 只在特殊需求下使用自定义超时通道

6. 常见陷阱 ⚠️

  1. 向已关闭的 channel 发送数据会导致 panic
  2. 重复关闭 channel 会导致 panic
  3. 在没有接收者的情况下关闭 channel 可能导致 goroutine 泄漏

· 4 min read
wen

1. Slice 是什么?

Slice 是 Go 语言中的一种类似数组的数据结构,是对数组的一个封装。

和数组相比,slice 的长度是可以动态变化的,可以通过内置函数 append 来动态增加切片的长度。

img

2. Slice 的注意事项和常见错误以及陷阱

2.1. 未初始化的切片

未初始化的切片是 nil,对其进行操作会导致运行时错误。

package main

import "fmt"

func main() {
var fruits []string
// fruits[0] = "🍎" // 这会导致运行时错误:panic: runtime error: index out of range [0] with length 0

// 正确做法1:初始化切片
fruits := make([]string, 1)
fruits[0] = "🍎"

// 正确做法2:使用字面量初始化切片
fruits := []string{"🍎"}

// 正确做法3:使用 append 函数初始化切片。(如果不是特别注重性能,这种方式是最简单的。)
fruits = append(fruits, "🍎")
}

2.2. 使用 append 函数时,注意重新赋值

append 函数会在容量不足时重新分配一个更大的底层数组,并将原来的数据复制到新的数组中,然后返回一个新的切片,

所以注意在使用 append 函数时,如果需要使用原来的切片变量,就需要重新赋值。

    fruits := []string{"🍎"}
fruits = append(fruits, "🍌")

2.3. 切片作为函数参数时的引用问题

切片是引用类型,所以在函数参数中传递切片时,实际上是传递了切片的引用。

func modifyFruits(fruits []string) {
fruits[0] = "🍌"
}

func main() {
fruits := []string{"🍎", "🍌"}
fmt.Println("Before modification:", fruits) // 输出: Before modification: [🍎 🍌]

modifyFruits(fruits)
fmt.Println("After modification:", fruits) // 输出: After modification: [🍌 🍌]
}

2.4. 切片的截取

切片的截取操作是左闭右开的,即 a[1:3] 表示从下标 1 开始到下标 3 结束,但不包括下标 3。

    fruits := []string{"🍎", "🍌", "🍇", "🍉"}
fmt.Println(fruits[1:3]) // 输出: [🍌 🍇]

2.5. 切片的长度和容量混淆

切片的长度是指切片中元素的个数,容量是指切片底层数组的长度。

    fruits := make([]string, 2, 5)
fmt.Println("Length:", len(fruits), "Capacity:", cap(fruits)) // 输出: Length: 2 Capacity: 5

// fruits[3] = "🍇" // 这会导致运行时错误:panic: runtime error: index out of range [3] with length 2

// 正确做法:使用 append 来添加元素
fruits = append(fruits, "🍇")

2.6. 切片的复制

切片的复制是浅拷贝,即复制的是切片的引用,而不是切片的底层数组。

这个和函数参数传递的引用是类似的。

    fruits := []string{"🍎", "🍌"}
fruitsCopy := fruits
fruitsCopy[0] = "🍌"
fmt.Println("Original slice:", fruits) // 输出: Original slice: [🍌 🍌]

· 3 min read
wen

在 Go (Golang) 编程中,map 是一种强大且灵活的数据结构,用于存储键值对。

然而,为了确保高效和正确的使用,有几个重要的点需要注意。

本文将详细介绍这些关键考虑因素,并通过示例代码展示如何在 Go 中有效地使用 map。

1. 初始化

在使用 map 之前,必须对其进行初始化。可以使用 make 函数或使用 map 字面量来初始化。

// 使用 make
m := make(map[string]int)

// 使用 map 字面量
m := map[string]int{"🍎": 1, "🍌": 2}

2. Nil Map

nil map空 map 不同。

nil map 不能写入,尝试写入会导致运行时恐慌(panic)。

因此,始终要初始化你的 map。

var m map[string]int // m 是 nil
m["🍎"] = 1 // 这会导致恐慌

3. 从 Map 中读取

从 map 中读取时,如果键不存在,会返回值类型的零值。为了区分缺失的键和实际的零值, 可以使用多返回值的方式。

value, ok := m["🍊"]
if ok {
fmt.Println("找到了:", value)
} else {
fmt.Println("未找到")
}

4. 从 Map 中删除

使用 delete 函数从 map 中移除键值对。在键不存在的情况下调用 delete 是安全的。

delete(m, "🍎")

5. 并发访问

Map 不是并发安全的。如果多个 goroutine 同时访问一个 map,并且至少有一个修改了 map,你必须使用互斥锁或通道来同步访问。

var mu sync.Mutex

mu.Lock()
m["🍎"] = 1
mu.Unlock()

6. 迭代顺序

map 的迭代顺序不能保证在程序的不同运行中保持一致。如果你需要稳定的迭代顺序,必须显式地对键进行排序。

keys := make([]string, 0, len(m))
for k := range m {
keys = append(keys, k)
}
sort.Strings(keys)
for _, k := range keys {
fmt.Println(k, m[k])
}

7. 作为 set 使用

Golang 中没有 set 类型,但可以使用 map 来模拟 set 。只需将值类型设置为 struct{} 即可。

struct{} 是一个空结构,不占用任何内存空间。这样可以节省内存,因为 map 的值是空结构,而不是实际的值。

s := make(map[string]struct{})
s["🍎"] = struct{}{}

// 检查键是否存在
_, ok := s["🍎"]

如果觉得麻烦,可以直接使用第三方库 golang-set

· 2 min read
wen

问题

有 2 个数组,互相可能有重复的元素,如何合并这两个数组并去重?

比如有两个数组:

type User struct {
ID int // ID 作为唯一标识 (ID相同则认为是同一个元素)
Name string
}

old := []User{
{ID: 1, Name: "a"}, // only in old
{ID: 2, Name: "b"}, // 重复
}

new := []User{
{ID: 2, Name: "c"}, // 重复
{ID: 3, Name: "d"}, // only in new
}

合并后的结果应该是:

c := []User{
{ID: 1, Name: "a"}, // only in old
{ID: 2, Name: "c"}, // 重复 (保留 new 中的)
{ID: 3, Name: "d"}, // only in new
}

解决方案(一般化)

package main

import "fmt"

type User struct {
ID int
Name string
}

// contains Check if an element exists in a slice.
// keyFunc is used to uniquely identify the elements.
func contains(slice []any, item any, keyFunc func(any) any) bool {
for _, element := range slice {
if keyFunc(element) == keyFunc(item) {
return true
}
}
return false
}

// mergeSlices Merges two slices and removes duplicates.
//
// keyFunc is used to uniquely identify the elements.
// if an element exists in both old and new, the element in new takes precedence.
// old and new are assumed to have no duplicate elements.
// The order is not guaranteed.
func MergeSlices(old, new []any, keyFunc func(any) any) []any {
var merged []any

// copy new to merged
merged = append(merged, new...)

for _, item := range old {
if !contains(merged, item, keyFunc) {
merged = append(merged, item)
}
}

return merged
}

func main() {
old := []any{
User{ID: 1, Name: "a"},
User{ID: 2, Name: "b"}, // 重複
}
new := []any{
User{ID: 2, Name: "c"}, // 重複
User{ID: 3, Name: "d"},
}

mergedUsers := MergeSlices(old, new, func(item any) any {
return item.(User).ID
})
fmt.Printf("Merged Users:%+v", mergedUsers) // Merged Users:[ {ID:1 Name:a} {ID:2 Name:c} {ID:3 Name:d}]
}

· 3 min read
wen

Problem

It's a common problem to count the number of (visible) characters in a string.

In Golang, we can use utf8.RuneCountInString() function to count the number of characters in a string.

It works well for most cases, including multi-byte characters like Chinese.

Code example

package main

import (
"fmt"
"unicode/utf8"
)

func main() {
str := "abc世界"
fmt.Println(utf8.RuneCountInString(str)) // 5
}

But if the string contains emoji characters like 👉🏻, then in some cases this function will not calculate correctly.

package main

import (
"fmt"
"unicode/utf8"
)

func main() {
emojiWorld := "🌍"
fmt.Println(utf8.RuneCountInString(emojiWorld)) // 1 ✅ no problem

emojiHand := "👉"
fmt.Println(utf8.RuneCountInString(emojiHand)) // 1 ✅ no problem

emojiHandBlack := "👉🏿"
fmt.Println(utf8.RuneCountInString(emojiHandBlack)) // 2 ❌ Got 2, expected 1.

emojiOne := "1️⃣"
fmt.Println(utf8.RuneCountInString(emojiOne)) // 3 ❌ Got 3, expected 1.
}

You can copy the emoji from here emojipedia.

Why

This is because some emoji are composed of multiple unicode characters (Code Points), while the utf8.RuneCountInString() function only counts the number of unicode characters.

TermDescription
BytesThe smallest unit used to measure data storage, typically 8 bits in binary.
Code UnitsFixed-size units used in encoding schemes to represent a character. In UTF-8, a Code Unit is 8 bits, and in UTF-16, it's 16 bits.
Code PointsIn the Unicode standard, each character is assigned a unique code point, which is a numerical identifier for the character. For example, the code point for the Latin letter "A" is U+0041.
Grapheme ClustersRepresent the smallest units perceivable in a language, typically a sequence of one or more Code Points. For instance, a letter with an accent mark might be a Grapheme Cluster.

For example, 1️⃣ this emoji is composed of 3 Code Points, which are:

img

Rendered by Markdown Table

tip

Emoji Code Points can be found here emojipedia.

Solution

So we need to count the number of Grapheme Clusters instead of Code Points.

Use third-party library rivo/uniseg

package main

import (
"fmt"

"github.com/rivo/uniseg"
)

func main() {
emojiWorld := "🌍"
fmt.Println(uniseg.GraphemeClusterCount(emojiWorld)) // 1 ✅ 没有问题

emojiHand := "👉"
fmt.Println(uniseg.GraphemeClusterCount(emojiHand)) // 1 ✅ 没有问题

emojiHandBlack := "👉🏿"
fmt.Println(uniseg.GraphemeClusterCount(emojiHandBlack)) // 1 ✅ 没有问题

emojiOne := "1️⃣"
fmt.Println(uniseg.GraphemeClusterCount(emojiOne)) // 1 ✅ 没有问题
}
tip

Actually, there are not only emoji, but also some Thai and Arabic characters are composed of multiple unicode characters.

Reference

Go で文字数をカウントする 在 Go 中计算字符数

文字数をカウントする 7 つの方法

Go: Unicode と rune 型

· One min read
wen

背景

labstack/gommon 的代码中 看到了这个库 valyala/fasttemplate, 于是就去 调查了一下。

tip

labstack 是 Echo Web Framework 的 Organization。

什么是 fasttemplate

fasttemplate 是一个高效的 Go 模板引擎,比 Go 标准库的模板引擎 text/template 快很多,

而且比 strings.Replace, strings.Replacerfmt.Fprintf 都要快。

img

具体可以看一下 fasttemplate 的 benchmark

fasttemplate 的使用

基础用法

    template := "http://{{host}}/?q={{query}}&foo={{bar}}{{bar}}"
t := fasttemplate.New(template, "{{", "}}")
s := t.ExecuteString(map[string]interface{}{
"host": "google.com",
"query": url.QueryEscape("hello=world"),
"bar": "foobar",
})
fmt.Printf("%s", s)

// Output:
// http://google.com/?q=hello%3Dworld&foo=foobarfoobar

高阶用法

    template := "Hello, [user]! You won [prize]!!! [foobar]"
t, err := fasttemplate.NewTemplate(template, "[", "]")
if err != nil {
log.Fatalf("unexpected error when parsing template: %s", err)
}
s := t.ExecuteFuncString(func(w io.Writer, tag string) (int, error) {
switch tag {
case "user":
return w.Write([]byte("John"))
case "prize":
return w.Write([]byte("$100500"))
default:
return w.Write([]byte(fmt.Sprintf("[unknown tag %q]", tag)))
}
})
fmt.Printf("%s", s)

// Output:
// Hello, John! You won $100500!!! [unknown tag "foobar"]

· 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"])
}
}

· 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