Golang中按值过滤切片与按引用过滤切片的对比

Golang中按值过滤切片与按引用过滤切片的对比 我有一个包含持续时间结构体切片的“事件”结构体。我需要过滤掉已过期或已经发生的持续时间。

这个用例是在一个高流量的微服务中运行Gin。它不是面向用户的,而是由一个面向用户的RESTful API服务消费。我预计这些切片不会超过大约20个元素,每个请求可能包含大约20个事件结构体。

考虑到这个用例,我不确定在通过引用传递并利用切片底层数组的示例中,内存占用是否值得。而通过值传递并为过滤后的持续时间分配一个新数组的示例,读起来更简单,可能也不容易出错。我知道切片中的结构体很轻量,因为它们只是 int 类型。

问题在于,在我的用例中,考虑到服务器流量很高,节省内存是否值得。考虑到数据的预期大小以及它处于服务器上下文中,我不确定通过引用传递和重用底层数组是否值得。还有一个一致的API问题。假设事件有更复杂的数据,我预计会进行多个后处理步骤,根据其他属性进行过滤等,因此很可能添加其他执行类似操作的函数。我并不是过分担心每一比特内存,但这是我第一次在生产代码中使用带有指针的语言。有什么想法吗?

示例: playground

package main

import "fmt"

type duration struct {
	ValidFrom  int64
	ValidUntil int64
}
type event struct {
	Recurrences []duration
}

func filterExpired(evt event, now int64) event {
	var recurrences []duration
	for _, r := range evt.Recurrences {
		if r.ValidUntil >= now {
			recurrences = append(recurrences, r)
		}
	}
	evt.Recurrences = recurrences
	return evt
}
func filterExpired2(evt *event, now int64) {
	// Take a zero-length slice of the underlying array.
	temp := evt.Recurrences[:0]
	for _, r := range evt.Recurrences {
		if r.ValidUntil >= now {
			temp = append(temp, r)
		}
	}
	evt.Recurrences = temp
}

func main() {
	evt1 := event{Recurrences: []duration{{ValidFrom: 100, ValidUntil: 200}, {ValidFrom: 250, ValidUntil: 300}}}
	fmt.Println(filterExpired(evt1, 201))
	evt2 := event{Recurrences: []duration{{ValidFrom: 100, ValidUntil: 200}, {ValidFrom: 250, ValidUntil: 300}}}
	filterExpired2(&evt2, 201)
	fmt.Println(evt2)
}

更多关于Golang中按值过滤切片与按引用过滤切片的对比的实战教程也可以访问 https://www.itying.com/category-94-b0.html

5 回复

我也混淆了通过引用传递和不分配额外数组这两点。据我所知,在这个上下文中这两者都不是必需的。

更多关于Golang中按值过滤切片与按引用过滤切片的对比的实战系列教程也可以访问 https://www.itying.com/category-94-b0.html


我有点困惑,这可能和“var recurrences duration”一样,而且在Go语言中有一个容易误解的地方。当你将值(切片或映射)作为参数传递时,它总是按引用传递,无论你使用的是“evt event”还是“evt *event”。

kahunacohen:

temp := evt.Recurrences[:0]

我会遵循你的直觉,创建一个新的切片,直到垃圾回收压力成为一个真正的问题。你甚至提到了可能需要进行多个过滤步骤。如果你的多个步骤并发运行,那肯定会让重用切片变得复杂。

这是我第一次在生产代码中使用带指针的语言。

虽然不是Go语言,但在我职业生涯的早期,我曾花费许多漫长的时间来调试由指针操作引起的核心转储错误。现在,我会尽我所能编写出既正确又对未来变化具有韧性的代码。

类似这样吗?

func filterExpired3(evt *event, now int64) {
	newSize := 0
	for i := 0; i < len(evt.Recurrences); i++ {
		if evt.Recurrences[i].ValidUntil >= now {
			if newSize != i {
				evt.Recurrences[newSize] = evt.Recurrences[i]
			}
			newSize++
		}
	}
	evt.Recurrences = evt.Recurrences[:newSize]
}

对于你的高流量微服务场景,按值过滤和按引用过滤在性能上的差异可以忽略不计。以下是具体分析:

性能分析

// 基准测试对比
func BenchmarkFilterByValue(b *testing.B) {
    evt := event{Recurrences: make([]duration, 20)}
    now := int64(250)
    
    b.ResetTimer()
    for i := 0; i < b.N; i++ {
        _ = filterExpired(evt, now)
    }
}

func BenchmarkFilterByReference(b *testing.B) {
    evt := event{Recurrences: make([]duration, 20)}
    now := int64(250)
    
    b.ResetTimer()
    for i := 0; i < b.N; i++ {
        filterExpired2(&evt, now)
    }
}

内存分配对比

// 内存分配分析
func TestMemoryAllocation(t *testing.T) {
    evt := event{Recurrences: []duration{
        {ValidFrom: 100, ValidUntil: 200},
        {ValidFrom: 250, ValidUntil: 300},
        {ValidFrom: 350, ValidUntil: 400},
    }}
    
    // 按值传递:分配新切片
    result1 := filterExpired(evt, 201)
    // 按引用传递:重用底层数组
    filterExpired2(&evt, 201)
    
    fmt.Printf("按值过滤结果: %v\n", result1)
    fmt.Printf("按引用过滤结果: %v\n", evt)
}

实际建议

对于你的用例(20个元素 × 20个事件 = 400个duration结构体),两种方法的内存差异微乎其微。每个duration只有两个int64(16字节),400个元素总共6.4KB。

// 推荐使用按值传递的版本,原因如下:
func filterExpired(evt event, now int64) event {
    var recurrences []duration
    for _, r := range evt.Recurrences {
        if r.ValidUntil >= now {
            recurrences = append(recurrences, r)
        }
    }
    evt.Recurrences = recurrences
    return evt
}

// 使用示例
func processRequest(events []event, now int64) []event {
    filtered := make([]event, len(events))
    for i, evt := range events {
        filtered[i] = filterExpired(evt, now)
    }
    return filtered
}

可读性和维护性考虑

// 按值传递支持链式调用
func processEvent(evt event, now int64) event {
    return filterExpired(
        filterByOtherCriteria(evt, someParam),
        now,
    )
}

// 而按引用传递会修改原始数据
func processEvent2(evt *event, now int64) {
    filterExpired2(evt, now)
    filterByOtherCriteria2(evt, someParam)
    // 原始evt已被修改
}

在你的场景中,按值过滤的版本更合适。它提供了更好的不可变性、更清晰的API,并且性能差异在实际应用中不可察觉。Go的垃圾回收器能高效处理这种小规模临时分配。

回到顶部