有人能建议一个适用于简单且快速FIF/队列的Go容器吗?Go有三种不同的容器:heap
、list
和vector
。哪一种最适合实现队列?
queue := make([]int, 0)
// Push to the queue
queue = append(queue, 1)
// Top (just get next element, don't remove it)
x = queue[0]
// Discard top element
queue = queue[1:]
// Is empty ?
if len(queue) == 0 {
fmt.Println("Queue is empty !")
}
当然,我们假设我们可以信任append和slicing的内部实现,以避免无用的重新分配和调整大小。对于基本用途来说,这已经足够了。
惊讶地发现还没有人建议使用带缓冲通道来实现固定大小的先进先出队列。
//Or however many you might need + buffer.
c := make(chan int, 300)
//Push
c <- value
//Pop
x <- c
大多数队列实现有三种类型:基于切片、基于链表和基于循环缓冲区(环形缓冲区)。
基于环形缓冲区的队列通过将其存储包装在周围来重新使用内存:当队列超出底层切片的一端增长时,它会在切片的另一端添加额外的节点。 请参见deque图表
此外,下面是一些代码示例:
// PushBack appends an element to the back of the queue. Implements FIFO when
// elements are removed with PopFront(), and LIFO when elements are removed
// with PopBack().
func (q *Deque) PushBack(elem interface{}) {
q.growIfFull()
q.buf[q.tail] = elem
// Calculate new tail position.
q.tail = q.next(q.tail)
q.count++
}
// next returns the next buffer position wrapping around buffer.
func (q *Deque) next(i int) int {
return (i + 1) & (len(q.buf) - 1) // bitwise modulus
}
这个特定的实现总是使用2的幂作为缓冲区大小,因此可以更加高效地计算按位模数。
这意味着只有在全部容量被使用时才需要增长切片。通过避免在同一边界上增长和收缩存储的重新调整策略,这使得它非常节省内存。
以下是调整底层切片缓冲区的代码:
// resize resizes the deque to fit exactly twice its current contents. This is
// used to grow the queue when it is full, and also to shrink it when it is
// only a quarter full.
func (q *Deque) resize() {
newBuf := make([]interface{}, q.count<<1)
if q.tail > q.head {
copy(newBuf, q.buf[q.head:q.tail])
} else {
n := copy(newBuf, q.buf[q.head:])
copy(newBuf[n:], q.buf[:q.tail])
}
q.head = 0
q.tail = q.count
q.buf = newBuf
}
另一个需要考虑的事情是是否需要在实现中构建并发安全性。你可能希望避免这一点,以便可以按照最适合你的并发策略进行操作;如果不需要,则肯定不需要它。原因与不希望具有某些内置序列化功能的切片相同。
如果在 godoc 上搜索deque,就会发现有许多基于环形缓冲区的 Go 队列实现。选择最适合你口味的一个。
向量和列表两者都可以使用,但是向量可能更适合。我这么说是因为相对于列表,向量可能更少地进行分配,并且垃圾收集(在当前Go实现中)相当昂贵。在一个小程序中,这可能无关紧要。
编辑,清洁实现一个队列:
package main
import "fmt"
type Queue []interface{}
func (self *Queue) Push(x interface{}) {
*self = append(*self, x)
}
func (self *Queue) Pop() interface{} {
h := *self
var el interface{}
l := len(h)
el, *self = h[0], h[1:l]
// Or use this instead for a Stack
// el, *self = h[l-1], h[0:l-1]
return el
}
func NewQueue() *Queue {
return &Queue{}
}
func main() {
q := NewQueue()
q.Push(1)
q.Push(2)
q.Push(3)
q.Push("L")
fmt.Println(q.Pop())
fmt.Println(q.Pop())
fmt.Println(q.Pop())
fmt.Println(q.Pop())
}
或者只需将"container/list"
嵌入简单实现中并公开接口:
package queue
import "container/list"
// Queue is a queue
type Queue interface {
Front() *list.Element
Len() int
Add(interface{})
Remove()
}
type queueImpl struct {
*list.List
}
func (q *queueImpl) Add(v interface{}) {
q.PushBack(v)
}
func (q *queueImpl) Remove() {
e := q.Front()
q.List.Remove(e)
}
// New is a new instance of a Queue
func New() Queue {
return &queueImpl{list.New()}
}
很遗憾,队列目前不是Go标准库的一部分,因此您需要编写自己的/导入他人的解决方案。这很遗憾,因为在标准库之外编写的容器无法使用泛型。
一个固定容量队列的简单示例如下:
type MyQueueElement struct {
blah int // whatever you want
}
const MAX_QUEUE_SIZE = 16
type Queue struct {
content [MAX_QUEUE_SIZE]MyQueueElement
readHead int
writeHead int
len int
}
func (q *Queue) Push(e MyQueueElement) bool {
if q.len >= MAX_QUEUE_SIZE {
return false
}
q.content[q.writeHead] = e
q.writeHead = (q.writeHead + 1) % MAX_QUEUE_SIZE
q.len++
return true
}
func (q *Queue) Pop() (MyQueueElement, bool) {
if q.len <= 0 {
return MyQueueElement{}, false
}
result := q.content[q.readHead]
q.content[q.readHead] = MyQueueElement{}
q.readHead = (q.readHead + 1) % MAX_QUEUE_SIZE
q.len--
return result, true
}
MyQueueElement
结构体只包含int,则不会有任何区别,但如果结构体包含指针,则会有所不同。我也像上面那样使用切片来实现队列。但是,它不是线程安全的。因此,我决定添加锁(互斥锁)来保证线程安全。
package queue
import (
"sync"
)
type Queue struct {
lock *sync.Mutex
Values []int
}
func Init() Queue {
return Queue{&sync.Mutex{}, make([]int, 0)}
}
func (q *Queue) Enqueue(x int) {
for {
q.lock.Lock()
q.Values = append(q.Values, x)
q.lock.Unlock()
return
}
}
func (q *Queue) Dequeue() *int {
for {
if (len(q.Values) > 0) {
q.lock.Lock()
x := q.Values[0]
q.Values = q.Values[1:]
q.lock.Unlock()
return &x
}
return nil
}
return nil
}
// queue.go
package queue
type Queue struct {
data []int
}
func (q *Queue) Enqueue(d int) {
q.data = append(q.data, d)
}
func (q *Queue) Dequeue() int {
dequeued := q.data[0]
q.data = q.data[1:]
return dequeued
}
func (q *Queue) IsEmpty() bool {
return len(q.data) == 0
}
func NewQueue() *Queue {
return &Queue{
data: make([]int, 0),
}
}
//queue_test.go
package queue
import (
"testing"
"github.com/stretchr/testify/assert"
)
func TestQueue_ShouldInitialiseWithEmpty(t *testing.T) {
q := NewQueue()
assert.Equal(t, true, q.IsEmpty())
}
func TestQueue_ShouldErrorIfDequeuePerformedOnEmpty(t *testing.T) {
q := NewQueue()
_, err := q.Dequeue()
assert.NotNil(t, err)
assert.Equal(t, "nothing to dequeue", err.Error())
}
func TestQueue(t *testing.T) {
q := NewQueue()
q.Enqueue(1)
q.Enqueue(2)
q.Enqueue(3)
q.Enqueue(4)
dequeued1, err1 := q.Dequeue()
assert.Nil(t, err1)
assert.Equal(t, 1, dequeued1)
dequeued2, err2 := q.Dequeue()
assert.Nil(t, err2)
assert.Equal(t, 2, dequeued2)
dequeued3, err3 := q.Dequeue()
assert.Nil(t, err3)
assert.Equal(t, 3, dequeued3)
dequeued4, err4 := q.Dequeue()
assert.Nil(t, err4)
assert.Equal(t, 4, dequeued4)
}
[]int
,其中复制是所需的。如果改为type Foo struct{/*许多字段*/}
,通常会使用[]*Foo
,并且队列将是指针,您不需要复制元素,只需复制指针即可。(然后,您还需要执行queue[0] = nil; queue = queue[1:]
,以舍弃第一个元素并从队列中删除对其的引用)。 - Dave Cappend
将创建一个新的支持数组。因此,只要您丢弃旧切片queue=queue[1:]
,内存使用就不会是无界的。如果切片很大,仍可能需要一段时间才能回收该内存。 - Nuno Cruces