Golang实践录:几种基于结构体类型数组的查询方法

本文根据笔者的工程实践经验,列举一些基于结构体类型数组的查询方法。并给出实例。

引言

几年前,笔者写了个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生成并做了部分的人为修改,虽然初步测试功能正常,但是否存在问题,有待扩大范围进一步验证。

从使用体验上看,如果数据量少的话,直接用基于数组的查询方法。如果数据量大且查询频繁,建议使用索引查询方法。

相关推荐
寻寻觅觅☆5 小时前
东华OJ-基础题-106-大整数相加(C++)
开发语言·c++·算法
l1t6 小时前
在wsl的python 3.14.3容器中使用databend包
开发语言·数据库·python·databend
青云计划6 小时前
知光项目知文发布模块
java·后端·spring·mybatis
赶路人儿6 小时前
Jsoniter(java版本)使用介绍
java·开发语言
Victor3567 小时前
MongoDB(9)什么是MongoDB的副本集(Replica Set)?
后端
Victor3567 小时前
MongoDB(8)什么是聚合(Aggregation)?
后端
ceclar1237 小时前
C++使用format
开发语言·c++·算法
码说AI7 小时前
python快速绘制走势图对比曲线
开发语言·python
Gofarlic_OMS7 小时前
科学计算领域MATLAB许可证管理工具对比推荐
运维·开发语言·算法·matlab·自动化
星空下的月光影子8 小时前
易语言开发从入门到精通:补充篇·网络爬虫与自动化采集分析系统深度实战·HTTP/HTTPS请求·HTML/JSON解析·反爬策略·电商价格监控·新闻资讯采集
开发语言