【问题标题】:How do I (succinctly) remove the first element from a slice in Go?如何(简洁地)从 Go 中的切片中删除第一个元素?
【发布时间】:2014-06-25 06:17:32
【问题描述】:

我在 Go 中构建了一个简单的队列。它使用内部切片来跟踪其元素。元素通过附加到切片被推送到队列中。我想通过删除elements 中的第一个元素来实现.Pop()

在许多其他语言中,“弹出”列表的第一个元素是单行的,这让我相信我下面的实现是草率和冗长的。有没有更好的办法?

type Queue struct {
    elements []interface{}
}

func (queue *Queue) Push(element interface{}) {
    queue.elements = append(queue.elements, element)
}

func (queue *Queue) Pop() interface{} {
    element := queue.elements[0]
    if len(queue.elements) > 1 {
        queue.elements = queue.elements[1:]
    } else {
        queue.elements = make([]interface{}, 0)
    }
    return element
}

请注意,如果len(queue.elements) == 0,我希望Queue 恐慌。我不检查边界并不是疏忽。

【问题讨论】:

    标签: go queue


    【解决方案1】:

    你试过这些吗?

    从队列中弹出

    x, a = a[0], a[1:]
    

    从栈中弹出

    x, a = a[len(a)-1], a[:len(a)-1]
    

    a = append(a, x)
    

    发件人:https://code.google.com/p/go-wiki/wiki/SliceTricks

    【讨论】:

    • 小子,我脸红了。我读了那篇文章,并认为我已经尝试过“从队列中弹出”示例,但我再次尝试,它按我希望的那样工作。谢谢! (很抱歉这个愚蠢的问题。)
    • 如果效率很重要,您可能希望使用环形缓冲区样式来避免过多的重新分配和垃圾收集。类似github.com/eapache/channels/blob/master/queue.go
    • 只有当添加了足够多的新元素以重新分配切片并且可以丢弃删除的元素时,才会对第一个元素进行垃圾收集。在我的环形缓冲区示例代码中,元素在删除时显式地被取消,以允许立即进行垃圾收集。 (如果 append 调整数组大小,则不会复制未使用的元素)。
    • 我认为这种方法的主要问题是每个队列插入都可能存在分配 - playground example
    • 我在github.com/eapache/queue 处将我的基于环形缓冲区的队列提取到了它自己的包中,这样其他人可以在他们想使用它时导入它。
    【解决方案2】:

    如果你想要一个环形缓冲区或 FIFO 结构,那么在@Everton 的回答中使用切片会导致垃圾收集问题,因为底层数组可能会无限增长。

    如果您不介意大小有限,那么执行此操作的最简单方法是使用对并发访问也安全的通道。这是 go 中的一个常见习语,您通常不会费心将其包装成如下所示的类型。

    例如 (Playground)

    package main
    
    import "fmt"
    
    type Queue struct {
        elements chan interface{}
    }
    
    func NewQueue(size int) *Queue {
        return &Queue{
            elements: make(chan interface{}, size),
        }
    }
    
    func (queue *Queue) Push(element interface{}) {
        select {
        case queue.elements <- element:
        default:
            panic("Queue full")
        }
    }
    
    func (queue *Queue) Pop() interface{} {
        select {
        case e := <-queue.elements:
            return e
        default:
            panic("Queue empty")
        }
        return nil
    }
    
    func main() {
        q := NewQueue(128)
    
        q.Push(1)
        q.Push(2)
        q.Push(3)
        fmt.Printf("Pop %d\n", q.Pop())
        fmt.Printf("Pop %d\n", q.Pop())
        fmt.Printf("Pop %d\n", q.Pop())
        fmt.Printf("Pop %d\n", q.Pop())
    
    }
    

    【讨论】:

    • 我还是 Go 新手,所以我可能要花点时间来解决这个问题,但感谢您的回答!等我了解更多有关频道的信息后,我会回来讨论它。
    • Channels 恰好提供了一个有用且高效的工具来制作这样的环形缓冲区。正如 C.A.R.Hoare 最初设想的那样,通道旨在用于进程(goroutines)之间使用;这种模式是相当不同的。请注意,如果通道缓冲区已满,并且唯一的消费线程(调用Pop)也尝试调用Push,则可能会发生死锁。 @Everton 的方法没有这个缺点;垃圾收集问题可以通过不同的方式解决。
    • @Rick-777 使用Push 方法中的另一个select 可以轻松解决死锁。
    • @Rick-777 已更新答案以显示如何检测队列已满。
    • @modocache 我想说,如果你在 go 例程之间进行通信,只需使用通道 - 这就是它们的用途。您不需要队列空或满检测 - 运行时会自动为您暂停和取消暂停 goroutine。否则你可以使用这个类型安全但有界的解决方案,或者你可以使用 Evan 提到的没有任何分配或垃圾收集问题的代码。
    猜你喜欢
    • 2014-11-28
    • 2016-11-11
    • 2016-09-16
    • 1970-01-01
    • 2016-03-10
    • 1970-01-01
    • 2019-02-05
    相关资源
    最近更新 更多