本文根据笔者的工程实践经验,列举一些基于结构体类型数组的查询方法。并给出实例。
引言
几年前,笔者写了个golang工程,是和数据库打交道的,在读取到数据表数据之后,还会涉及到数据的查询。基于朴素的想法,起初是使用数组遍历查询,简单明了且不易出错。但是,当业务功能复杂之后,查询次数多了之后,总体的性能就下降了,特别是涉及到数据查询、处理、组装和前端页面渲染等。造成的结果的是,在页面点击按钮后,要等待6、7秒才有内容更新。最近系统地解决了问题,整改查询接口,整体替换。页面能在3、4秒内返回结果。
本文主要关注查询的方法。
实践
简单遍历的查询方法
根据不同的结构体实现遍历查询方法。伪代码如下:
type foo_t struct {
ID int
Name string
}
func find_simple(arr []foo_t, id int) (foo_t, bool){
for _, item:=range arr {
if item.ID == id {
return item, ok
}
}
}
不同的结构体,均需要使用类似的接口形式进行查询,代码臃肿不堪,效率低下。
通用遍历的查询方法
于是寻求通用的方法,利益于golang的泛型机制,可以使用统一的接口,代码如下:
// 基于泛型实现的通用查询函数 - 返回同类型切片
// 查询不到,返回空,即[]
// 示例 results, _, _ := FindInArray(people, "City", "Beijing", "Age", 25)
func FindInArray[T any](arr []T, conditions ...interface{}) ([]T, bool, error) {
if len(conditions)%2 != 0 {
return nil, false, fmt.Errorf("conditions must be key-value pairs")
}
var results []T
found := false
for _, item := range arr {
v := reflect.ValueOf(item)
// 如果是指针,获取指向的值
if v.Kind() == reflect.Ptr {
v = v.Elem()
}
if v.Kind() != reflect.Struct {
return nil, false, fmt.Errorf("array element must be a struct")
}
// 检查所有条件
match := true
for i := 0; i < len(conditions); i += 2 {
fieldName, ok := conditions[i].(string)
if !ok {
return nil, false, fmt.Errorf("condition key must be string")
}
expectedValue := conditions[i+1]
field := v.FieldByName(fieldName)
if !field.IsValid() {
match = false
break
}
// 比较值
if !reflect.DeepEqual(field.Interface(), expectedValue) {
match = false
break
}
}
if match {
results = append(results, item)
found = true
}
}
return results, found, nil
}
测试代码:
func TestFind1(t *testing.T) {
type Person struct {
ID int
Name string
Age int
City string
Got bool
}
people := []Person{
{ID: 1, Name: "Alice", Age: 20, City: "Beijing", Got: true},
{ID: 2, Name: "Bob", Age: 25, City: "Shanghai"},
{ID: 3, Name: "Alice", Age: 30, City: "Beijing", Got: true},
{ID: 3, Name: "Tom", Age: 30, City: "Beijing", Got: false},
{ID: 4, Name: "Charlie", Age: 25, City: "Guangzhou"},
}
// 单条件查询
cityName := "Beijing"
beijingPeople, _, _ := FindInArray(people, "City", cityName)
fmt.Printf("city: %v: %v\n", cityName, beijingPeople)
// [{1 Alice 20 Beijing} {3 Alice 30 Beijing}]
// 多条件查询
age := 30
results, _, _ := FindInArray(people, "City", cityName, "Age", age)
fmt.Printf("city: %v & Age=%v: %v\n", cityName, age, results)
// 条件不符合查询
// [] (没有匹配的)
cityName = "Nanning"
results, _, _ = FindInArray(people, "City", cityName, "Age", age)
fmt.Printf("city: %v & Age=%v: %v\n", cityName, age, results)
// 条件不符合查询 字段名不匹配
results, _, _ = FindInArray(people, "city", cityName, "age", age)
fmt.Printf("city: %v & Age=%v: %v\n", cityName, age, results)
}
测试结果:
$ go test -v -run TestFind1
1 city: Beijing: [{1 Alice 20 Beijing true} {3 Alice 30 Beijing true} {3 Tom 30 Beijing false}]
2 city: Beijing & Age=30: [{3 Alice 30 Beijing true} {3 Tom 30 Beijing false}]
3 city: Nanning & Age=30: []
4 city: Nanning & Age=30: []
仿sql风格的通用遍历的查询方法
代码如下,支持链式调用。
// SQL风格查询
type QueryBuilder[T any] struct {
data []T
}
// 新建查询
func NewQuery[T any](data []T) *QueryBuilder[T] {
return &QueryBuilder[T]{data: data}
}
// Where 条件查询
func (q *QueryBuilder[T]) Where(conditions ...interface{}) *QueryBuilder[T] {
if len(conditions)%2 != 0 {
panic("conditions must be key-value pairs")
}
var results []T
for _, item := range q.data {
v := reflect.ValueOf(item)
if v.Kind() == reflect.Ptr {
v = v.Elem()
}
match := true
for i := 0; i < len(conditions); i += 2 {
fieldName := conditions[i].(string)
expectedValue := conditions[i+1]
field := v.FieldByName(fieldName)
if !field.IsValid() || !reflect.DeepEqual(field.Interface(), expectedValue) {
match = false
break
}
}
if match {
results = append(results, item)
}
}
q.data = results
return q
}
// 获取结果
func (q *QueryBuilder[T]) Get() []T {
return q.data
}
// 获取第一个
func (q *QueryBuilder[T]) First() (T, bool) {
if len(q.data) == 0 {
var zero T
return zero, false
}
return q.data[0], true
}
// 计数
func (q *QueryBuilder[T]) Count() int {
return len(q.data)
}
测试代码:
func TestFind2(t *testing.T) {
type Person struct {
ID int
Name string
Age int
City string
Got bool
}
people := []Person{
{ID: 1, Name: "Alice", Age: 20, City: "Beijing", Got: true},
{ID: 2, Name: "Bob", Age: 25, City: "Shanghai"},
{ID: 3, Name: "Alice", Age: 30, City: "Beijing", Got: true},
{ID: 3, Name: "Tom", Age: 30, City: "Beijing", Got: false},
{ID: 4, Name: "Charlie", Age: 25, City: "Guangzhou"},
}
// 单条件查询
cityName := "Beijing"
beijingPeople := NewQuery(people).
Where("City", cityName).
Get()
fmt.Printf("1 city: %v: %v\n", cityName, beijingPeople)
// 多条件查询
age := 20
results := NewQuery(people).
Where("Got", true).
Where("City", cityName).
Where("Age", age).
Get()
fmt.Printf("2 city: %v: %v\n", cityName, results)
// 条件不符合查询
cityName = "Nanning"
results = NewQuery(people).
Where("Got", true).
Where("City", cityName).
Where("Age", age).
Get()
fmt.Printf("3 city: %v & Age=%v: %v\n", cityName, age, results)
// 字段名不符合查询
cityName = "Nanning"
results = NewQuery(people).
Where("city", cityName).
Where("age", age).
Get()
fmt.Printf("4 city: %v & Age=%v: %v\n", cityName, age, results)
}
测试结果:
$ go test -v -run TestFind2
=== RUN TestFind2
1 city: Beijing: [{1 Alice 20 Beijing true} {3 Alice 30 Beijing true} {3 Tom 30 Beijing false}]
2 city: Beijing: [{1 Alice 20 Beijing true}]
3 city: Nanning & Age=20: []
4 city: Nanning & Age=20: []
--- PASS: TestFind2 (0.00s)
基于索引的查询方法
上述方法,还是基于数组的遍历机制,不高效。高效的方法是使用map,或者说,引入数据库的概念,创建索引,并用之查询。
代码:
// 实用快速查询器
type QuickQuery[T any] struct {
data []T
indices map[string]map[interface{}][]int // 核心索引
mu sync.RWMutex
}
// 新建查询器并建立索引
func NewQuickQuery[T any](data []T, indexFields ...string) *QuickQuery[T] {
qq := &QuickQuery[T]{
data: data,
indices: make(map[string]map[interface{}][]int),
}
for _, field := range indexFields {
qq.buildFieldIndex(field)
}
return qq
}
// 建立字段索引
func (qq *QuickQuery[T]) buildFieldIndex(field string) {
qq.mu.Lock()
defer qq.mu.Unlock()
index := make(map[interface{}][]int)
for i, item := range qq.data {
value := qq.getFieldValue(item, field)
if value != nil {
index[value] = append(index[value], i)
}
}
qq.indices[field] = index
}
// 获取字段值
func (qq *QuickQuery[T]) getFieldValue(item T, field string) interface{} {
v := reflect.ValueOf(item)
if v.Kind() == reflect.Ptr {
v = v.Elem()
}
fieldValue := v.FieldByName(field)
if !fieldValue.IsValid() {
return nil
}
return fieldValue.Interface()
}
// ============== 增强版 Find 方法 ==============
// Find 查询方法 - 支持单条件和多条件
// 返回值: (查询结果, 是否找到数据, 错误)
// 用法:
//
// results, found, err := qq.Find("ID", 1) // 单条件
// results, found, err := qq.Find("City", "Beijing", "Age", 25) // 多条件
func (qq *QuickQuery[T]) Find(conditions ...interface{}) ([]T, bool, error) {
if len(conditions) == 0 {
return qq.data, len(qq.data) > 0, nil
}
// 检查参数数量
if len(conditions)%2 != 0 {
return []T{}, false, fmt.Errorf("Find: conditions must be key-value pairs")
}
// 将参数转换为map
condMap := make(map[string]interface{})
for i := 0; i < len(conditions); i += 2 {
field, ok := conditions[i].(string)
if !ok {
return []T{}, false, fmt.Errorf("Find: field name must be string, got %T", conditions[i])
}
condMap[field] = conditions[i+1]
}
return qq.findByMap(condMap)
}
// FindByMap 使用map查询
// 返回值: (查询结果, 是否找到数据, 错误)
func (qq *QuickQuery[T]) FindByMap(conditions map[string]interface{}) ([]T, bool, error) {
return qq.findByMap(conditions)
}
// FindFirst 查找第一个匹配项
// 返回值: (查询结果, 是否找到数据, 错误)
func (qq *QuickQuery[T]) FindFirst(conditions ...interface{}) (T, bool, error) {
results, found, err := qq.Find(conditions...)
if err != nil {
var zero T
return zero, false, err
}
if !found {
var zero T
return zero, false, nil
}
return results[0], true, nil
}
// FindFirstByMap 使用map查找第一个匹配项
// 返回值: (查询结果, 是否找到数据, 错误)
func (qq *QuickQuery[T]) FindFirstByMap(conditions map[string]interface{}) (T, bool, error) {
results, found, err := qq.FindByMap(conditions)
if err != nil {
var zero T
return zero, false, err
}
if !found {
var zero T
return zero, false, nil
}
return results[0], true, nil
}
// FindOr 简单OR查询 - 符合任意条件即可,调用方式同Find函数
func (qq *QuickQuery[T]) FindOr(conditions ...interface{}) ([]T, bool, error) {
if len(conditions) == 0 {
return qq.data, len(qq.data) > 0, nil
}
// 检查参数数量
if len(conditions)%2 != 0 {
return []T{}, false, fmt.Errorf("FindOr: conditions must be key-value pairs")
}
// 使用set去重(Go没有内置set,用map模拟)
seen := make(map[string]struct{})
var results []T
// 处理每个条件
for i := 0; i < len(conditions); i += 2 {
field, ok := conditions[i].(string)
if !ok {
return []T{}, false, fmt.Errorf("FindOr: field name must be string")
}
value := conditions[i+1]
// 查询满足当前条件的记录
cond := map[string]interface{}{field: value}
items, found, err := qq.findByMap(cond)
if err != nil || !found {
continue
}
// 去重并添加结果
for _, item := range items {
// 生成唯一键:简单使用指针地址+字段值
key := fmt.Sprintf("%p-%v", &item, item)
if _, exists := seen[key]; !exists {
seen[key] = struct{}{}
results = append(results, item)
}
}
}
if len(results) == 0 {
return []T{}, false, nil
}
return results, true, nil
}
// 内部方法:根据map条件查询
func (qq *QuickQuery[T]) findByMap(conditions map[string]interface{}) ([]T, bool, error) {
qq.mu.RLock()
defer qq.mu.RUnlock()
if len(conditions) == 0 {
return qq.data, len(qq.data) > 0, nil
}
// 检查所有条件字段是否都有索引
allIndexed := true
for field := range conditions {
if _, hasIndex := qq.indices[field]; !hasIndex {
allIndexed = false
break
}
}
var results []T
var found bool
if allIndexed {
// 所有字段都有索引,使用索引交集
results = qq.findWithAllIndexed(conditions)
found = len(results) > 0
} else if len(conditions) == 1 {
// 单条件但字段没有索引,回退遍历
for field, value := range conditions {
results = qq.findWithoutIndexSingle(field, value)
found = len(results) > 0
}
} else {
// 多条件且有字段没有索引,回退遍历
results = qq.findWithoutIndexMultiple(conditions)
found = len(results) > 0
}
return results, found, nil
}
// 所有条件字段都有索引时的查询
func (qq *QuickQuery[T]) findWithAllIndexed(conditions map[string]interface{}) []T {
var matchedIndices []int
first := true
for field, value := range conditions {
index := qq.indices[field]
if indices, ok := index[value]; ok {
if first {
// 第一个条件,直接使用
matchedIndices = indices
first = false
} else {
// 后续条件,取交集
matchedIndices = intersect(matchedIndices, indices)
if len(matchedIndices) == 0 {
return []T{} // 提前终止
}
}
} else {
// 某个值没有匹配项
return []T{}
}
}
// 根据索引获取结果
return qq.getRecordsByIndices(matchedIndices)
}
// 获取指定索引的记录
func (qq *QuickQuery[T]) getRecordsByIndices(indices []int) []T {
result := make([]T, 0, len(indices))
for _, idx := range indices {
if idx < len(qq.data) {
result = append(result, qq.data[idx])
}
}
return result
}
// 单字段无索引查询
func (qq *QuickQuery[T]) findWithoutIndexSingle(field string, value interface{}) []T {
var result []T
for _, item := range qq.data {
fieldValue := qq.getFieldValue(item, field)
if reflect.DeepEqual(fieldValue, value) {
result = append(result, item)
}
}
return result
}
// 多字段无索引查询
func (qq *QuickQuery[T]) findWithoutIndexMultiple(conditions map[string]interface{}) []T {
var result []T
for _, item := range qq.data {
matchesAll := true
for field, expectedValue := range conditions {
fieldValue := qq.getFieldValue(item, field)
if !reflect.DeepEqual(fieldValue, expectedValue) {
matchesAll = false
break
}
}
if matchesAll {
result = append(result, item)
}
}
return result
}
// 交集计算(优化版)
func intersect(a, b []int) []int {
if len(a) == 0 || len(b) == 0 {
return []int{}
}
// 总是用较短的数组建立map
if len(a) > len(b) {
a, b = b, a
}
set := make(map[int]bool, len(a))
for _, v := range a {
set[v] = true
}
var result []int
for _, v := range b {
if set[v] {
result = append(result, v)
}
}
return result
}
// 更新数据(重建索引)
func (qq *QuickQuery[T]) Update(data []T) error {
qq.mu.Lock()
defer qq.mu.Unlock()
qq.data = data
// 重建所有索引
for field := range qq.indices {
qq.buildFieldIndex(field)
}
return nil
}
// 添加数据(增量更新索引)
func (qq *QuickQuery[T]) Add(items ...T) error {
qq.mu.Lock()
defer qq.mu.Unlock()
for _, item := range items {
idx := len(qq.data)
qq.data = append(qq.data, item)
// 更新所有索引
for field, index := range qq.indices {
value := qq.getFieldValue(item, field)
if value != nil {
index[value] = append(index[value], idx)
}
}
}
return nil
}
上述代码提供几个接口:
- Find:查询,支持多条件,且条件为交集,即同时满足所传入的字段。
- FindByMap:使用map组装查询条件,支持多条件。
- FindOr:支持多条件,且条件为并集,满足其中一个条件即可查询到。(注:笔者部分场景常用到)
- Add:新增数据,较少用。
测试代码:
func TestFind3(t *testing.T) {
type Person struct {
ID int
Name string
Age int
City string
Got bool
}
people := []Person{
{ID: 1, Name: "Alice", Age: 20, City: "Beijing", Got: true},
{ID: 2, Name: "Bob", Age: 25, City: "Shanghai"},
{ID: 3, Name: "Alice", Age: 30, City: "Beijing", Got: true},
{ID: 3, Name: "Tom", Age: 30, City: "Beijing", Got: false},
{ID: 4, Name: "Charlie", Age: 25, City: "Guangzhou"},
}
// 建立查询器,为常用字段建立索引
qq := NewQuickQuery(people, "ID", "Name", "Age", "City")
// 单条件查询
cityName := "Beijing"
beijingPeople, _, _ := qq.Find("City", cityName)
fmt.Printf("1 city: %v: %v\n", cityName, beijingPeople)
// 多条件查询
age := 20
results, _, _ := qq.Find("City", cityName, "Age", age)
fmt.Printf("2 city: %v: %v\n", cityName, results)
// 使用map进行查询
results, _, _ = qq.FindByMap(map[string]interface{}{
"City": "Beijing",
"Age": 30,
})
fmt.Printf("3 city: %v: %v\n", cityName, results)
// 添加新数据并验证
newName := "Grace"
qq.Add(Person{ID: 7, Name: newName, Age: 25, City: cityName})
newResults, _, _ := qq.Find("Name", newName)
fmt.Printf("查询新增 %v 的数据: %v\n", newName, newResults)
results5, _, _ := qq.Find("Got", true)
fmt.Printf("查询没有索引的Got为true数据: %v\n", results5)
// 查询不存在的值
noResults, _, _ := qq.Find("City", "New York")
fmt.Printf("查询不存在的城市: %v (应为空)\n", noResults)
}
测试结果:
$ go test -v -run TestFind3
=== RUN TestFind3
1 city: Beijing: [{1 Alice 20 Beijing true} {3 Alice 30 Beijing true} {3 Tom 30 Beijing false}]
2 city: Beijing: [{1 Alice 20 Beijing true}]
3 city: Beijing: [{3 Alice 30 Beijing true} {3 Tom 30 Beijing false}]
查询新增 Grace 的数据: [{7 Grace 25 Beijing false}]
查询没有索引的Got为true数据: [{1 Alice 20 Beijing true} {3 Alice 30 Beijing true}]
查询不存在的城市: [] (应为空)
--- PASS: TestFind3 (0.00s)
小结
上述测试代码没有做性能测试,但直接在工程代码中验证,基于索引的查询方法的确大大提升效率。本文大部分代码使用AI生成并做了部分的人为修改,虽然初步测试功能正常,但是否存在问题,有待扩大范围进一步验证。
从使用体验上看,如果数据量少的话,直接用基于数组的查询方法。如果数据量大且查询频繁,建议使用索引查询方法。