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或者直接赋值分配;对nilmap 写入会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")