Golang中遍历切片时元素与索引的区别

Golang中遍历切片时元素与索引的区别 大家好,

我学习Go语言的时间不长,大约六周前才开始接触。在此之前,我使用Python已有三年,并且完全是自学(我的注意力缺陷多动障碍让我很难参加正式课程或完整地读完一本书)。

无论如何,我正在为工作构建我的第一个Go项目,遇到了一个可能相当简单的问题,但我一直没能找到关于这个具体问题的帖子。

在遍历结构体切片时,是遍历元素更高效,还是遍历索引更高效?

首先,介绍一下我的项目需求和我采取的方法: 我在一家全国性零售商工作,正在构建一个脚本,根据上一个完整季度的销售数据,将门店归入相似的组别。比较的因素包括总销售额、热销商品、来源仓库和相对位置。

为此,我构建了一系列结构体以及这些结构体的类型化切片。(由于公司知识产权规定,我删减了部分代码。)

下面是我的一些代码,以及两个展示不同方法的函数。

除了关于哪种方法内存效率更高的想法外,对我的代码的任何一般性反馈也将不胜感激!这个项目我已经进行得太深,无法做出重大更改,但我一定会记下来,以便在未来的项目中应用。

type storeMaster struct {
	store      int
	location   storeLocation
	department storeDepartment
}

type storeLocation struct {
	warehouse int
	postal int
}

type storeDepartment struct {
	department int        //部门ID(用于在部门级别对门店进行分组)
	weeks      int        //有活跃销售的周数
	retail     float64    // $$
	avgRetail  float64    // $$ 零售额 / float64(weeks)
	lastWeek   int        //最近有销售的周
	items      storeItemSlice 
}

type storeItem struct {
	upc       int64
	weeks     int        //商品有活跃销售的周数
	retail    float64    // $$
	avgRetail float64    // $$ 零售额 / float64(weeks)
	lastWeek  int        //商品最近有销售的周
}

type storeMasterSlice []storeMaster
type storeItemSlice []storeItem

func (u storeItemSlice) Len() int           { return len(u) }
func (u storeItemSlice) Less(i, j int) bool { return u[i].avgRetail > u[j].avgRetail }
func (u storeItemSlice) Swap(i, j int)      { u[i], u[j] = u[j], u[i] }
func (u storeItemSlice) Sort()              { sort.Sort(u) }

// 遍历元素的函数
func (m *storeMaster) topItems() (top10, top25, top50 []int64) {
	var items storeItemSlice

	weeks, lastWeek := (*m).maxWeek()

	weeks = int(math.Round(float64(weeks) * .75))
	lastWeek = lastWeek - 3

	for _, item := range (*m).department.items {
		if item.weeks >= weeks && item.lastWeek >= lastWeek {
			items = append(items, item)
		}
	}

	items.Sort()

	for i := 0; i < 50; i++ {
		top50 = append(top50, items[i].upc)
	}
	top25 = top50[:25]
	top10 = top50[:10]

	return top10, top25, top50
}

// 遍历索引的函数
func (m *storeMaster) maxWeek() (weeks, lastWeek int) {
	for i := range (*m).department.items {
		if (*m).department.items[i].weeks > weeks {
			weeks = *m.department.items[i].weeks
		}
		if (*m).department.items[i].lastWeek > lastWeek {
			lastWeek = (*m).department.items[i].lastWeek
		}
	}

	return weeks, lastWeek
}

提前感谢您能提供的任何反馈!


更多关于Golang中遍历切片时元素与索引的区别的实战教程也可以访问 https://www.itying.com/category-94-b0.html

10 回复

我也想特别感谢你的提醒。我确实容易过多地陷入对“如果……会怎样”的担忧中。

更多关于Golang中遍历切片时元素与索引的区别的实战系列教程也可以访问 https://www.itying.com/category-94-b0.html


并且,一如既往,从最清晰、最直接的代码开始。如果你之后通过测量和分析发现它存在性能问题,那时再考虑替代方案。

啊,好的,在学习 make() 时我肯定漏掉了那部分,这样就说得通了。另外,我已经把 go-critic 加入了我的收藏列表。谢谢!

我想我之所以开始这样做,是因为之前有一个函数一直报错,直到我给指针加上了括号,于是我就开始对所有传递的指针都这么做了,不确定为什么我从未尝试过直接去掉 * 号。

谢谢,我会记住的!

RandomBrain:

在遍历结构体切片时,遍历元素还是遍历索引更高效?

这取决于具体情况。Go语言没有针对元素引用的range形式,因此所有值都会被复制。所以:

  1. 如果不想修改元素且元素尺寸较小,请使用元素遍历。
  2. 否则,请使用索引遍历(或指针 x := &data[i])。

关于代码。最好提供最小但完整的代码,但是:

  1. 如果元素少于50个怎么办?
  2. 在可能的情况下,应始终预分配内存。
  3. 不要在不会重新切片或重新分配切片的情况下对切片使用指针接收器。请参考接收器类型建议

(*m).department.items

与C语言不同,.操作符在必要时会自动解引用指针。 因此 (*m).department.items 通常写作 m.department.items

所以我会这样写这个函数:

func (m *storeMaster) maxWeek() (weeks, lastWeek int) {
	for i , w := range m.department.items {
		if w.weeks > weeks {
			weeks = w
		}
		if w.lastWeek > lastWeek {
			lastWeek = w.lastWeek
		}
	}

	return weeks, lastWeek
}

RandomBrain:

我知道没有固定的数字,但决定它是否算小的一个好的经验法则是什么? 我对我的几个 storeItemSlices 使用了 unsafe.SizeOf,平均值是 28K(约 700 个项目)。

go-critic 的开发者为此类检查将每次迭代的值设定为 128 字节。

RandomBrain:

这是通过使用一个具有固定容量的数组来实现的吗?

我的意思是为切片提供初始大小:

top50 := make([]int64, 50)

等等。

感谢两位的反馈!特别是关于代码审查评论文章的链接,我已经把它加入收藏了。

@GreyShatter 如果你不介意的话,我还有一些后续问题。

GreyShatter:

  • 如果不想修改元素且其大小较小时,请使用元素。

我知道没有固定的数字,但判断其是否较小的一个好的经验法则是什么? 我对我的几个 storeItemSlices 使用了 unsafe.SizeOf,平均值是 28K(约 700 个项目)。

GreyShatter: 只要可能,就应该始终预分配内存。

这是否通过使用具有设定容量的数组来完成?

GreyShatter: 如果元素少于 50 个怎么办?

这里没有问题,只是想为此感谢你。在我们部门层面,在我们关注的时间段内,我们最小的商店售出的不同商品数量不会低于约 500 个。然而,如果我被要求将其应用到更小的子部门或类别级别时,这是我未曾考虑过的事情。我将在尝试填充 “top” 切片之前添加一个大小检查。

在Go语言中遍历切片时,使用元素遍历(for _, item := range slice)和索引遍历(for i := range slice)在性能上没有本质区别。两者的差异主要体现在使用场景和代码可读性上。

性能分析

两种遍历方式编译后生成的机器指令几乎相同,主要区别在于:

  1. 元素遍历:每次迭代会创建元素的副本
  2. 索引遍历:直接访问原始切片元素

对于大型结构体,索引遍历可能稍微更高效,因为避免了结构体的复制。但在你的storeItem结构体(40字节左右)的情况下,差异可以忽略不计。

代码示例对比

元素遍历(当前代码)

func (m *storeMaster) topItems() (top10, top25, top50 []int64) {
    // ...
    for _, item := range m.department.items {  // 这里可以去掉(*m)
        if item.weeks >= weeks && item.lastWeek >= lastWeek {
            items = append(items, item)  // 这里复制了item
        }
    }
    // ...
}

索引遍历优化版

func (m *storeMaster) topItemsIndex() (top10, top25, top50 []int64) {
    weeks, lastWeek := m.maxWeek()
    weeks = int(math.Round(float64(weeks) * .75))
    lastWeek = lastWeek - 3

    var items storeItemSlice
    departmentItems := m.department.items  // 局部变量减少解引用
    
    for i := range departmentItems {
        item := &departmentItems[i]  // 使用指针避免复制
        if item.weeks >= weeks && item.lastWeek >= lastWeek {
            items = append(items, *item)
        }
    }

    items.Sort()
    
    for i := 0; i < 50 && i < len(items); i++ {
        top50 = append(top50, items[i].upc)
    }
    top25 = top50[:min(25, len(top50))]
    top10 = top50[:min(10, len(top50))]
    
    return top10, top25, top50
}

func min(a, b int) int {
    if a < b {
        return a
    }
    return b
}

针对你的代码的具体建议

1. 简化指针解引用

// 当前写法
for i := range (*m).department.items

// 建议写法
for i := range m.department.items

2. maxWeek函数优化

func (m *storeMaster) maxWeek() (weeks, lastWeek int) {
    items := m.department.items  // 创建局部引用
    for i := range items {
        if items[i].weeks > weeks {
            weeks = items[i].weeks
        }
        if items[i].lastWeek > lastWeek {
            lastWeek = items[i].lastWeek
        }
    }
    return weeks, lastWeek
}

3. 边界检查

topItems函数中,添加切片长度检查:

for i := 0; i < 50 && i < len(items); i++ {
    top50 = append(top50, items[i].upc)
}

推荐使用场景

  1. 需要修改元素时:使用索引遍历
for i := range slice {
    slice[i].value = newValue  // 直接修改
}
  1. 只读访问时:使用元素遍历(代码更简洁)
for _, item := range slice {
    fmt.Println(item.value)  // 只读访问
}
  1. 需要索引位置时:使用索引遍历
for i := range slice {
    if i > 0 {
        // 使用索引位置
    }
}

在你的具体案例中,maxWeek函数使用索引遍历是合适的,因为只需要读取值。topItems函数中,由于需要复制符合条件的元素到新切片,两种方式都可以,但索引遍历配合指针使用可以避免一次结构体复制。

回到顶部