Golang中encoding/json包的nil指针解引用错误问题探讨

Golang中encoding/json包的nil指针解引用错误问题探讨 大家好,

我们正在使用 Go 语言(版本 1.21)的 json 编码器,有时在序列化一个非常简单的结构体时会遇到以下空指针错误:

goroutine 1619784 [running]:
panic: runtime error: invalid memory address or nil pointer dereference [recovered]
panic: runtime error: invalid memory address or nil pointer dereference
encoding/json.(*encodeState).marshal.func1()
[signal SIGSEGV: segmentation violation code=0x1 addr=0x0 pc=0x5580e728fe0b]
    /usr/lib/golang/src/encoding/json/encode.go:293 +0x6d
    /usr/lib/golang/src/encoding/json/encode.go:321 +0x73
encoding/json.stringEncoder(0xc00378c080, {0x5580e7e1aea0?, 0xc003d06200?, 0xc002b1600a?}, {0x19?, 0x0?})
    /usr/lib/golang/src/encoding/json/encode.go:704 +0x21e
    /usr/lib/golang/src/encoding/json/encode.go:321 +0x73
encoding/json.ptrEncoder.encode({0x5580e7d912c8?}, 0xc00378c080, {0x5580e7dfc600?, 0xc000e22cc0?, 0x5580e7dfc600?}, {0x10?, 0x0?})
    /usr/lib/golang/src/encoding/json/encode.go:876 +0x23c
    /usr/lib/golang/src/encoding/json/encode.go:847 +0xcf
encoding/json.structEncoder.encode({{{0xc0018d8240, 0x1, 0x1}, 0xc001880480, 0xc0018804b0}}, 0xc00378c080, {0x5580e7ead360?, 0xc003d06200?, 0xc003d06200?}, {0x0, ...})
encoding/json.(*encodeState).marshal(0xc001b319c8?, {0x5580e7dfc600?, 0xc000e22cc0?}, {0x2f?, 0x0?})
encoding/json.ptrEncoder.encode({0x7f06d8fc28b8?}, 0xc00378c080, {0x5580e7df0540?, 0xc003d06200?, 0x5580e7df0540?}, {0x60?, 0xce?})
    /usr/lib/golang/src/encoding/json/encode.go:876 +0x23c
encoding/json.arrayEncoder.encode({0xc0021ba360?}, 0xc00378c080, {0x5580e7e2f0c0?, 0xc000e22cd0?, 0x5580e7e1aea0?}, {0x9?, 0x0?})
    /usr/lib/golang/src/encoding/json/encode.go:658 +0xba
    /usr/lib/golang/src/encoding/json/encode.go:960 +0xab
panic({0x5580e7e7e7a0?, 0x5580e86125a0?})
    /usr/lib/golang/src/runtime/panic.go:770 +0x132
encoding/json.(*encodeState).reflectValue(0xc00378c080, {0x5580e7df0540?, 0xc003d06200?, 0x5580e727c825?}, {0x40?, 0x16?})
encoding/json.interfaceEncoder(0xc00378c080, {0x5580e7e50fe0?, 0xc000e22cd0?, 0x1?}, {0xa0?, 0xae?})
encoding/json.structEncoder.encode({{{0xc000b40008, 0x3, 0x4}, 0xc0008a6f00, 0xc0008a71a0}}, 0xc00378c080, {0x5580e7f02cc0?, 0xc000e22cc0?, 0xc000e22cc0?}, {0x0, ...})
    /usr/lib/golang/src/encoding/json/encode.go:589 +0x3c7
encoding/json.appendString[...]({0xc001eec0b6?, 0x10?, 0x7f0720605f48?}, {0x0?, 0x40}, 0x1)
encoding/json.(*encodeState).reflectValue(0xc00378c080, {0x5580e7dfc600?, 0xc000e22cc0?, 0x12?}, {0x0?, 0xa8?})
    /usr/lib/golang/src/encoding/json/encode.go:704 +0x21e
    /usr/lib/golang/src/encoding/json/encode.go:704 +0x21e
github.com/gorilla/rpc/json.EncodeClientRequest({0x5580e79f5565, 0x17}, {0x5580e7df0540, 0xc003d06200})

我们使用 encoding/json/json.Marshal() 来序列化一个非常简单的函数局部结构体:

&clientRequest{
		Method: method,
		Params: [1]interface{}{args},
		Id:     uint64(rand.Int63()),
	}

Params 是一个只包含一个元素的数组,该元素是一个内部只有一个字符串的非常简单的结构体(该字符串永远不会是 nil,因为它不是指针)。

我们还同时从多个 Go 协程中调用这个序列化逻辑。

到目前为止,我们无法将此错误的根源定位到我们代码的任何问题上。 因此,我们想看看您是否能就可能导致此类错误的原因给我们一些提示/建议?我们是否误用了 json 编码器 API?这可能是某种数据竞争吗?

提前感谢。


更多关于Golang中encoding/json包的nil指针解引用错误问题探讨的实战教程也可以访问 https://www.itying.com/category-94-b0.html

4 回复

检查 Method 的值。 你也可以使用 go run -race your_program.go 来检查代码中的数据竞争情况…

func main() {
    fmt.Println("hello world")
}

更多关于Golang中encoding/json包的nil指针解引用错误问题探讨的实战系列教程也可以访问 https://www.itying.com/category-94-b0.html


或许需要更多 Go 版本信息?

image

你好 @Rmarian

有两点快速的想法:

  1. 你写道:“Params 是一个只包含一个元素的数组,这个元素是一个非常简单的结构体,里面只有一个字符串。” 你为什么要使用 any(也就是 interface{})类型的数组,而不是实际结构体类型的数组呢?

  2. 考虑添加一个安全检查,例如:

    params := [1]interface{}{args}
    if params[0] == nil {
       ...
    

这可能有助于揭示导致空指针恐慌的原因。

这是一个典型的并发访问导致的nil指针解引用问题。从堆栈跟踪可以看出,错误发生在encoding/json.stringEncoder函数中,这表明在序列化字符串字段时遇到了nil指针。

问题很可能出现在clientRequest结构体的Method字段上。当多个goroutine同时访问和修改同一个结构体实例时,可能会发生数据竞争,导致Method字段在序列化过程中变为nil。

以下是问题复现的示例代码:

package main

import (
	"encoding/json"
	"fmt"
	"sync"
)

type clientRequest struct {
	Method string
	Params [1]interface{}
	Id     uint64
}

func main() {
	var wg sync.WaitGroup
	req := &clientRequest{
		Method: "testMethod",
		Params: [1]interface{}{"testParam"},
		Id:     123,
	}

	// 模拟并发访问
	for i := 0; i < 1000; i++ {
		wg.Add(1)
		go func() {
			defer wg.Done()
			// 并发修改Method字段
			req.Method = "newMethod"
			// 并发序列化
			_, err := json.Marshal(req)
			if err != nil {
				fmt.Printf("Error: %v\n", err)
			}
		}()
	}

	wg.Wait()
}

更常见的情况是,如果clientRequest结构体被多个goroutine共享并修改,就会出现数据竞争。正确的做法是为每个goroutine创建独立的结构体实例:

func safeMarshal(method string, args interface{}) ([]byte, error) {
	req := &clientRequest{
		Method: method,
		Params: [1]interface{}{args},
		Id:     uint64(rand.Int63()),
	}
	return json.Marshal(req)
}

// 在每个goroutine中调用
go func() {
	data, err := safeMarshal("methodName", args)
	// 处理结果
}()

如果必须共享结构体,需要使用互斥锁保护:

type SafeClientRequest struct {
	mu   sync.RWMutex
	req  *clientRequest
}

func (s *SafeClientRequest) MarshalJSON() ([]byte, error) {
	s.mu.RLock()
	defer s.mu.RUnlock()
	return json.Marshal(s.req)
}

检查代码中是否存在以下情况:

  1. 多个goroutine共享同一个clientRequest实例
  2. 在序列化过程中同时修改结构体字段
  3. 使用全局或共享的变量存储clientRequest实例

使用go run -race命令运行程序可以帮助检测数据竞争问题。

回到顶部