Go struct 可比较吗?能当 map key 吗?
相信很多 Gopher 都踩过类似的坑——Go 的 struct 到底能不能比较?又能不能作为 map 的 key?
这篇文章就把结论和底层逻辑和实战技巧给大家讲透。
一、Go struct 是否可比?
Go 里不是所有 struct 都能比较,关键看结构体的所有字段是否都属于“可比较类型”。
这是 Go 的类型系统规则,先把可比较和不可比较类型分清楚。
(一)可比较的 struct
可比较的 struct:所有字段均为可比较类型。
像 int、string、bool、指针这些基础类型都是“可比较类型”;
如果 struct 的所有字段都属于这类,那这个 struct 就能用==或!=比较,比较时会逐字段校验是否相等。
比如做用户信息校验时,判断两个用户结构体是否完全一致,就可以直接比较:
package main
import "fmt"
// User 所有字段均为可比较类型(int、string、bool)
type User struct {
ID int
Username string
IsVip bool
}
func main() {
// 两个字段完全相同的结构体
u1 := User{ID: 1, Username: "gopher", IsVip: true}
u2 := User{ID: 1, Username: "gopher", IsVip: true}
// 直接用==比较
if u1 == u2 {
fmt.Println("u1和u2完全相等")
} else {
fmt.Println("u1和u2不相等")
}
// 字段不完全相同的情况
u3 := User{ID: 2, Username: "gopher", IsVip: true}
if u1 == u3 {
fmt.Println("u1和u3完全相等")
} else {
fmt.Println("u1和u3不相等(ID不同)")
}
}运行结果会输出 “u1 和 u2 完全相等”以及“u1 和 u3 不相等( ID 不同)”,符合预期。
这种结构体不仅能直接比较,还能用来做 switch 的 case 条件,本质都是依赖其可比性
(二)不可比较的 struct
不可比较的 struct:包含不可比较类型字段。
如果 struct 里有 slice、map、function 这些“不可比较类型”的字段,那这个 struct 就不能用==比较,强行用会直接编译报错。
比如定义一个包含文章内容和标签的结构体,标签用 slice 存储,再尝试比较就会出错:
package main
import "fmt"
// Article 包含不可比较类型字段Tags(slice)
type Article struct {
ID int
Title string
Tags []string // slice是不可比较类型
}
func main() {
a1 := Article{
ID: 1,
Title: "Go struct详解",
Tags: []string{"Go", "结构体"},
}
a2 := Article{
ID: 1,
Title: "Go struct详解",
Tags: []string{"Go", "结构体"},
}
// 尝试比较两个包含slice的结构体,编译直接报错
// 错误信息:invalid operation: a1 == a2 (struct containing []string cannot be compared)
if a1 == a2 {
fmt.Println("a1和a2相等")
}
}遇到这种情况想判断两个结构体是否“语义相等”(比如 slice 元素完全相同),就得自己写比较函数,逐字段校验,尤其是不可比较字段要特殊处理:
package main
import "fmt"
type Article struct {
ID int
Title string
Tags []string
}
// 自定义比较函数,判断两个Article是否语义相等
func ArticleEqual(a, b Article) bool {
// 先比较可比较字段
if a.ID != b.ID || a.Title != b.Title {
return false
}
// 再比较不可比较字段Tags(slice)
if len(a.Tags) != len(b.Tags) {
return false
}
for i := range a.Tags {
if a.Tags[i] != b.Tags[i] {
return false
}
}
return true
}
func main() {
a1 := Article{
ID: 1,
Title: "Go struct详解",
Tags: []string{"Go", "结构体"},
}
a2 := Article{
ID: 1,
Title: "Go struct详解",
Tags: []string{"Go", "结构体"},
}
a3 := Article{
ID: 1,
Title: "Go struct详解",
Tags: []string{"Go", "基础"},
}
if ArticleEqual(a1, a2) {
fmt.Println("a1和a2语义相等")
}
if ArticleEqual(a1, a3) {
fmt.Println("a1和a3语义相等")
} else {
fmt.Println("a1和a3语义不相等(Tags不同)")
}
}二、Go struct 作为 map key ?
map 的 key 有个硬性要求:必须是可比较类型。
所以 struct 能不能当 map key,答案和“能不能比较”完全挂钩——只有可比较的 struct 才能作为 map 的 key。
这一点在做缓存、数据去重时特别常用。
(一)可比较 struct 作为 map key
比如做用户积分缓存,用 User 结构体当 key 存储对应的积分,因为 User 的所有字段都是可比较类型,所以完全可行:
package main
import "fmt"
type User struct {
ID int
Username string
}
func main() {
// 定义map,key为User结构体,value为积分(int)
scoreMap := make(map[User]int)
// 往map里存数据
u1 := User{ID: 1, Username: "gopher"}
scoreMap[u1] = 95
// 根据struct key取数据
if score, ok := scoreMap[u1]; ok {
fmt.Printf("%s的积分是:%d\n", u1.Username, score)
}
// 用新的相同结构体取数据(同样能取到)
u2 := User{ID: 1, Username: "gopher"}
if score, ok := scoreMap[u2]; ok {
fmt.Printf("%s的积分是:%d\n", u2.Username, score)
}
// 不同结构体key取数据(取不到)
u3 := User{ID: 2, Username: "gopher"}
if score, ok := scoreMap[u3]; ok {
fmt.Printf("%s的积分是:%d\n", u3.Username, score)
} else {
fmt.Printf("未找到%s的积分\n", u3.Username)
}
}运行后会正确输出 u1 和 u2 的积分,且 u3 因为 ID 不同取不到数据。
这里要注意:只要两个 struct 的所有字段都相等,它们就是 map 里的同一个 key,不管是不是同一个实例。
(二)不可比较 struct 作为 map key
如果强行用包含 slice、map 等不可比较字段的 struct 当 map key,编译时就会报错,提示 key 类型不可比较:
package main
import "fmt"
type Article struct {
ID int
Tags []string // slice是不可比较类型
}
func main() {
// 尝试定义key为Article的map,编译直接报错
// 错误信息:invalid map key type Article (struct containing []string cannot be compared)
articleMap := make(map[Article]string)
articleMap[Article{ID: 1, Tags: []string{"Go"}}] = "已发布"
}常见问题
Q1. struct 作为 map key 后,修改字段值会怎样?
问题:把 struct 存到 map 里后,如果修改了 struct 的字段值,再用原来的 struct 实例去取数据,还能取到吗?
答案:取不到!因为修改字段后,struct 的“哈希值”变了,map 会认为是新的 key。
package main
import "fmt"
type User struct {
ID int
Username string
}
func main() {
u1 := User{ID: 1, Username: "gopher"}
scoreMap := map[User]int{u1: 95}
// 修改struct的字段值
u1.Username = "go_dev"
// 再用u1取数据(取不到)
if score, ok := scoreMap[u1]; ok {
fmt.Printf("%s的积分是:%d\n", u1.Username, score)
} else {
fmt.Printf("未找到%s的积分\n", u1.Username)
}
// 用原来的字段值构造struct才能取到
u2 := User{ID: 1, Username: "gopher"}
if score, ok := scoreMap[u2]; ok {
fmt.Printf("%s的积分是:%d\n", u2.Username, score)
}
}解决办法:如果 struct 要作为 map key,尽量把它定义为“不可变”的——要么字段都是基本类型且不轻易修改,要么直接用指针当 key(但要注意指针的可比性是比较地址)。
Q2. 匿名 struct 的比较问题
问题:两个匿名 struct 的字段完全一样,能比较吗?
答案:不能!因为匿名 struct 是不同的类型,即使字段完全相同,Go 也会认为是不同类型,类型不同无法比较。
package main
import "fmt"
func main() {
// 两个匿名struct,字段完全相同
a := struct {
Name string
Age int
}{Name: "张三", Age: 20}
b := struct {
Name string
Age int
}{Name: "张三", Age: 20}
// 尝试比较,编译报错:invalid operation: a == b (mismatched types struct { Name string; Age int } and struct { Name string; Age int })
if a == b {
fmt.Println("a和b相等")
}
}解决办法:如果需要比较,就定义一个具名 struct,不要用匿名的;如果只是临时用,且要判断语义相等,就逐字段比较。
Q3. 包含指针字段的 struct 比较
问题:struct 里有指针字段,比较时是比较指针指向的值还是地址?
答案:比较指针的地址,不是指向的值!即使两个指针指向的内容完全相同,只要地址不同,struct 比较就会返回不相等。
package main
import "fmt"
type Person struct {
Name string
Age *int
}
func main() {
age1 := 20
age2 := 20
p1 := Person{Name: "张三", Age: &age1}
p2 := Person{Name: "张三", Age: &age2} // Age指向不同地址
// 虽然Age指向的值相同,但地址不同,所以p1 != p2
if p1 == p2 {
fmt.Println("p1和p2相等")
} else {
fmt.Println("p1和p2不相等(Age指针地址不同)")
}
// Age指向同一个地址时,才相等
p3 := Person{Name: "张三", Age: &age1}
if p1 == p3 {
fmt.Println("p1和p3相等")
}
}解决办法:如果要比较指针指向的内容,需要自定义比较函数,在函数里解引用指针后再比较。
总结
最后总结核心规则:
- 可比不可比,全看字段集:所有字段可比较,struct 就可比较;有一个不可比,整体就不可比。
- map key 要可比,struct 满足才能上:只有可比较的 struct 才能当map key,含 slice、map 的直接 pass。
- key 值修改要谨慎,哈希变化找不到:struct 当 key 后别乱改字段,改了就不是原来的 key了。
- 指针比较比地址,语义相等自定义:含指针字段比地址,要比内容写函数。
其实 struct 的可比性和 map key 使用,本质都是 Go 类型系统的基础规则,只要记住“可比较类型”的范围,再结合实际场景,就能彻底掌握。
如果还有其他踩坑经历,欢迎在评论区交流!
版权声明
未经授权,禁止转载本文章。
如需转载请保留原文链接并注明出处。即视为默认获得授权。
未保留原文链接未注明出处或删除链接将视为侵权,必追究法律责任!
本文原文链接: https://fiveyoboy.com/articles/go-compare-struct/
备用原文链接: https://blog.fiveyoboy.com/articles/go-compare-struct/