跳到主要内容

Go语言JSON结构体标签

· 阅读需 2 分钟

结构体标签详解​

type User struct {
ID int `json:"id"` // 字段重命名(服务端返回"id"字段)
Name string `json:"name"` // 保持字段原名
Email string `json:"email,omitempty"` // 空值时自动隐藏(避免空字符串污染JSON)
Secret string `json:"-"` // 彻底隐藏敏感字段(如密码)
Created time.Time `json:"created_at"` // 时间类型自动格式化为RFC3339字符串
}

序列化最佳实践​

// 带缩进的友好格式输出
data, _ := json.MarshalIndent(user, "", " ")
/*
{
"id": 1,
"name": "Alice",
"created_at": "2023-09-15T10:00:00Z"
}
*/

// 原始紧凑格式(适合网络传输)
binaryData, _ := json.Marshal(user) // []byte类型数据

反序列化注意事项​

var newUser User
err := json.Unmarshal(data, &newUser)

// 处理日期字段(需显式转换)
type CustomTime struct {
time.Time
}
func (ct *CustomTime) UnmarshalJSON(b []byte) error {
// 自定义日期解析逻辑...
}

常见问题排查​

  1. 字段丢失:检查结构体是否导出(首字母大写)
  2. 时间格式错误:使用time.RFC3339格式字符串
  3. 零值问题:结合omitempty与指针类型*string
  4. 循环引用:避免结构体嵌套循环

高级技巧​

// 动态字段处理
type FlexibleStruct struct {
Extra map[string]interface{} `json:"-"` // 收集未定义字段
}

// 条件序列化
func (u User) MarshalJSON() ([]byte, error) {
// 自定义序列化逻辑...
}

动态类型处理​

// 解析未知结构 JSON
data := []byte(`{"name":"Bob","age":30}`)
var result map[string]interface{}
json.Unmarshal(data, &result)
fmt.Println(result["name"].(string))

// 部分解析(使用匿名结构)
var partial struct {
Name string `json:"name"`
}
json.Unmarshal(data, &partial)

自定义序列化​

// 实现 json.Marshaler 接口
type CustomTime time.Time

func (ct CustomTime) MarshalJSON() ([]byte, error) {
return []byte(fmt.Sprintf(`"%s"`, time.Time(ct).Format("2006/01/02"))), nil
}

// 实现 json.Unmarshaler 接口
func (ct *CustomTime) UnmarshalJSON(data []byte) error {
// 自定义解析逻辑...
}

处理 HTML 转义​

buf := new(bytes.Buffer)
enc := json.NewEncoder(buf)
enc.SetEscapeHTML(false) // 禁用 &, <, > 转义
enc.Encode(data)

Go 内存分配机制

· 阅读需 2 分钟

问题引入​

在阅读 New-API 源码的时候,发现在 struct 中定义一个 *string 类型的字段

type ClaudeMediaMessage struct {
Type string `json:"type,omitempty"`
Text *string `json:"text,omitempty"`
Model string `json:"model,omitempty"`
Source *ClaudeMessageSource `json:"source,omitempty"`
Usage *ClaudeUsage `json:"usage,omitempty"`
StopReason *string `json:"stop_reason,omitempty"`
PartialJson *string `json:"partial_json,omitempty"`
Role string `json:"role,omitempty"`
Thinking string `json:"thinking,omitempty"`
Signature string `json:"signature,omitempty"`
Delta string `json:"delta,omitempty"`
// tool_calls
Id string `json:"id,omitempty"`
Name string `json:"name,omitempty"`
Input any `json:"input,omitempty"`
Content json.RawMessage `json:"content,omitempty"`
ToolUseId string `json:"tool_use_id,omitempty"`
}

func (c *ClaudeMediaMessage) SetText(s string) {
c.Text = &s
}

在 struct 中定义一个 *string 类型的字段的目的​

  1. 可以区分此字段是否被设置,或者被设置为 ""。

    • 原理是:*string 是一个字符串指针,默认值是 nil, 而 string 的默认值是 ""
  2. 在序列换 JSON 的时候,如果字段是 *string 类型,且值为 nil,则在序列化时会被忽略。

  3. API 设计时,可能很好的表达出可选字段。

  4. 避免频繁出现空字符串。

    • var s string , s 会被初始化为 "",变量没有逃逸,则会被分配在栈上,底层实现是 StringHeader, 大小 16 字节(指向底层字节数组的指针(8 字节,64 位系统)、字符串长度(8 字节,64 位系统))
    • var sp *string , sp 默认值是 nil, 声明时立即分配内存(通常是在栈上),大小 8 字节(由操作系统决定,64位,8字节)

文章引用​

Go语言时间处理

· 阅读需 1 分钟

时间格式化基础​

自定义格式输出​

Go使用特定时间戳(2006-01-02 15:04:05)作为布局字符串:

t := time.Now()
fmt.Println(t.Format("2006-01-02 15:04:05")) // 2023-10-08 14:30:45
fmt.Println(t.Format("2006/01/02 15:04:05")) // 2023/10/08 14:30:45
fmt.Println(t.Format("2006.01.02 15:04:05")) // 2023.10.08 14:30:45
fmt.Println(t.Format("2006年01月02日 15时04分05秒")) // 2023年10月08日 14时30分45秒

预定义标准格式​

Go内置多种标准格式,直接使用time包常量:

fmt.Println(time.Now().Format(time.RFC3339))  // 2023-10-08T14:30:45+08:00
fmt.Println(time.Now().Format(time.RFC1123)) // Mon, 08 Oct 2023 14:30:45 CST

字符串解析实践​

yyyyMM格式解析​

处理无分隔符的日期字符串时需注意布局字符串格式:

func parseYearMonth(str string) (int, time.Month) {
t, err := time.Parse("200601", str) // 布局字符串必须使用"200601"
if err != nil {
log.Fatal("解析失败:", err)
}
return t.Year(), t.Month()
}

// 使用示例
year, month := parseYearMonth("202310")
fmt.Printf("%d年%s月", year, month) // 2023年October月

= 与 := 的区别

· 阅读需 2 分钟

基础用法​

= 赋值操作符​

  1. 仅用于赋值,不声明变量
  2. 可以在任何代码快中使用
var a int
a = 100

:= 短变量声明​

  1. 声明变量的同时赋值
  2. 只能在函数内部使用
  3. 在给多个变量的时候赋值的时候,至少要定义一个新变量
a := 100
a,b := 200,300

使用案例​

闭包​

func closureDemo(){
for i:=0; i< 3;i++ {
go func(){
fmt.Print(i," ") // 闭包
}()
}
}

这段代码可能会输出 3 3 3,因为 i 是在闭包外部定义的变量,所有的 goroutine 都共享同一个 i 变量。

func closureDemo(){
for i:=0; i< 3;i++ {
val :=i
go func(){
fmt.Print(val," ")
}()
}
}

使用局部变量可以避免闭包带来的问题。每次循环都会创建一个新的变量。

i 变量定义在堆中,val 变量定义在栈中。

func closureDemo(){
for i:=0; i< 3;i++ {
go func(v int){
fmt.Print(v," ")
}(i)
}
}

通过参数将 i 的值传递给闭包函数,确保每个 goroutine 都有自己的 i 值。

并发下的安全赋值/原子操作​

func main() {
var counter *int64 = new(int64)
var wg sync.WaitGroup
for i := 0; i < 1000; i++ {
wg.Add(1)
go func(v *int64) {
*v += 1
wg.Done()
}(counter)
}
wg.Wait()
fmt.Println("Counter:", *counter)
}

输出的值小于 1000

func concurrentAssignment() {
var counter int64
var wg sync.WaitGroup

// 使用原子操作保证并发安全
for i := 0; i < 1000; i++ {
wg.Add(1)
go func() {
atomic.AddInt64(&counter, 1) // 使用=修改共享变量
wg.Done()
}()
}
wg.Wait()
fmt.Println("Counter:", counter)

// 使用通道安全传递值
ch := make(chan int)
go func() {
val := <-ch // 使用:=接收新值
fmt.Println("Received:", val)
}()
ch <- 42 // 使用=发送值
}

使用 atomic.AddInt64 来实现原子操作,确保在并发环境下对 counter 的安全修改。

初始化结构体指针

· 阅读需 1 分钟
type User struct {
ID int
Name string
}

直接结构体指针(字面量写法) 🔔推荐​

使用 & 符号直接创建结构体指针,并初始化字段. 推荐使用

优点:

  1. 字段与值显式对应,清晰直观
  2. 自动推导类型,减少冗余代码
  3. 字段顺序无关,结构体修改时更安全
u := &User{
ID: 1,
Name: "Alice",
}

先 new 后赋值​

// 1. 分配内存,返回一个指向零值的指针
u := new(User)

// 2. 手动赋值字段
u.ID = 1
u.Name = "Alice"

按顺序初始化 不推荐​

user := &User{2, "Alice"}

缺点:

  1. 必须严格匹配字段顺序,调整结构体字段顺序会导致初始化错误
  2. 可读性差,尤其当字段数量多或类型相似时容易出错

类型断言

· 阅读需 1 分钟

类型断言用于检查接口值的动态类型是否符合某个特定类型。

value, ok := interfaceVar.(ConcreteType)

  • interfaceVar 是一个接口变量
  • ConcreteType 是你想要断言的具体类型
  • value 是断言成功后转换的值
  • ok 是一个布尔值,表示断言是否成功

案例1 双返回值​

var writer io.Writer = os.Stdout
if closer, ok := writer.(io.Closer); ok {
closer.Close() // 安全调用接口扩展方法
}

案例2 单返回值​

// 直接转换(失败时触发panic)
str := anyVar.(string)

案例3 类型匹配​

var v interface{} = 42

// 基础类型匹配
_, ok1 := v.(int) // ok1 = true
_, ok2 := v.(string) // ok2 = false

// 接口类型匹配
_, ok3 := v.(fmt.Stringer) // 检查是否实现特定接口

案例4 与 switch 关键字搭配使用​

.(type) 必须与 switch 一起使用。

switch v := err.(type) {
case *AppError:
// 处理自定义错误类型
case *os.PathError:
// 处理系统路径错误
case nil:
// 没有错误的情况
default:
// 未知错误类型
}