Map

map 是 Go 内置的哈希表(键值对)数据结构,通过 key 快速定位 value,读写复杂度接近 O(1)。它是 Web3 开发中最常用的数据结构之一,例如用 map[string]string 维护「代币符号 → 合约地址」的映射。

基本特性

  • map 是无序的,每次遍历获取的顺序都不固定,且不能通过 index 获取,只能通过 key 获取
  • map 是一种引用类型,复制出的值的改变会影响本来的数据
  • map 的值可以直接通过 map["key"] = new value 修改
  • map 不是线程安全的,在多个 go-routine 存取时,必须使用 mutex lock 机制(或使用 sync.Map)
  • 可以通过 delete 关键字删除 map 数据 delete(map, key)
  • 初始化 map 必须分配内存空间,用 make 或者直接赋值分配;对 nil map 写入会 panic

定义与初始化

// make 初始化,指定预估容量可以减少扩容次数
contracts := make(map[string]string, 10)

// 字面量初始化
colors := map[string]int{
	"red":   1,
	"green": 2,
}

map[string]any:值可以是任意类型

前面两种写法的 value 类型是固定的(string 或 int)。如果希望同一个 map 里能放不同类型的 value,可以把 value 类型声明为 any:

// any 表示「任意类型」,value 不再局限于 string 或 int
params := map[string]any{
	"token":    "abc123", // string
	"protocol": 3,        // int
	"verbose":  true,     // bool
	"tags":     []string{"a", "b"},
}

any 是 interface{} 的别名(Go 1.18 引入),两者完全等价。它表示「这里可以放任何类型的值」,因此一个 map 里可以同时存在字符串、整数、布尔、切片等不同 value。

典型用法:构造请求参数

map[string]any 最常见的用途是拼装要序列化的请求参数——因为不同字段的类型天然不同,用结构体逐个定义反而繁琐。下面这段来自 WebSocket 连接握手(详见 WebSocket):

connectParams, _ := json.Marshal(map[string]any{
	"token":    resolveGatewayToken(),        // string
	"protocol": protocol.ProtocolVersion,     // int
})

json.Marshal 把 map 序列化成 JSON 字节:{"protocol":3,"token":"abc123"}。注意 map 无序,JSON 字段顺序与书写顺序无关,但解析方按 key 取值,不受影响。

取值需要类型断言

因为 any 抹掉了具体类型,取出来的值是 any,不能直接当 string 或 int 用,必须做类型断言还原:

v := params["protocol"]
// num := v + 1        // 编译错误:invalid operation

num, ok := v.(int)     // 类型断言,ok 为 false 表示实际类型不是 int
if ok {
	fmt.Println(num + 1) // 4
}

// 不确定类型时用 switch 分支处理
switch val := v.(type) {
case string:
	fmt.Println("string:", val)
case int:
	fmt.Println("int:", val)
default:
	fmt.Println("unknown type")
}

什么时候用 any

  • 适合:构造 JSON 请求参数、透传配置、临时聚合异构数据——这些场景类型本来就不固定
  • 不适合:业务核心数据结构。会丢失编译期类型检查,写错字段名或类型要到运行时才发现;能用结构体(或泛型,见 func)时优先用它们

完整示例

下面的示例覆盖了 map 的常见操作:初始化、写入、读取(comma-ok 断言)、遍历、删除,以及结构体作为 value 的用法:

package main

import "fmt"

type Limit struct {
	LimitationForOnce int64
	Whitelist         [2]string
}

func main() {
	// 1. make 初始化
	contracts := make(map[string]string)
	contracts["USDT"] = "0xdAC17F958D2ee523a2206206994597C13D831ec7"
	contracts["WETH"] = "0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2"

	// 2. 读取:comma-ok 断言,key 不存在时 ok 为 false
	contract, ok := contracts["key"]
	if !ok {
		fmt.Println("key 不存在")
		_ = contract
	}

	// 3. 遍历(顺序不固定)
	for symbol, addr := range contracts {
		fmt.Printf("%s => %s\n", symbol, addr)
	}

	// 4. 删除
	delete(contracts, "WETH")
	fmt.Println(len(contracts)) // 1

	// 5. 结构体作为 value
	var LimitsFromSymbol = map[string]Limit{
		"symbol": {
			300,
			[2]string{
				"wl1",
				"wl2",
			},
		},
	}
	fmt.Println(LimitsFromSymbol["symbol"].LimitationForOnce) // 300
}

引用类型特性

map 赋值给新变量后,两者指向同一份底层数据,一方的修改对另一方可见:

a := map[string]int{"x": 1}
b := a
b["x"] = 100
fmt.Println(a["x"]) // 100,a 也被修改了

并发安全

多个 goroutine 同时读写同一个 map 会触发 fatal error: concurrent map read and map write(这种错误无法被 recover 捕获)。两种解决方案:

// 方案1:sync.RWMutex 读写锁
var (
	mu sync.RWMutex
	m  = make(map[string]int)
)

func read(key string) int {
	mu.RLock()
	defer mu.RUnlock()
	return m[key]
}

func write(key string, v int) {
	mu.Lock()
	defer mu.Unlock()
	m[key] = v
}
// 方案2:sync.Map,适合读多写少的场景
var sm sync.Map
sm.Store("USDT", 1)
v, ok := sm.Load("USDT") // v=1, ok=true
sm.Delete("USDT")