├── gopher.png ├── gopher.xcf ├── Makefile ├── suite_test.go ├── .gitignore ├── .github └── workflows │ └── go.yml ├── go.mod ├── bench_test.go ├── LICENSE ├── options_test.go ├── options.go ├── buffer.go ├── go.sum ├── README.md └── buffer_test.go /gopher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/globocom/go-buffer/HEAD/gopher.png -------------------------------------------------------------------------------- /gopher.xcf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/globocom/go-buffer/HEAD/gopher.xcf -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | test: 2 | go run github.com/onsi/ginkgo/ginkgo -keepGoing -progress -timeout 1m -race --randomizeAllSpecs --randomizeSuites 3 | 4 | bench: 5 | go test -bench=. -run=Benchmark 6 | -------------------------------------------------------------------------------- /suite_test.go: -------------------------------------------------------------------------------- 1 | package buffer_test 2 | 3 | import ( 4 | "testing" 5 | 6 | . "github.com/onsi/ginkgo" 7 | . "github.com/onsi/gomega" 8 | ) 9 | 10 | func TestGoBuffer(t *testing.T) { 11 | RegisterFailHandler(Fail) 12 | RunSpecs(t, "go-buffer suite") 13 | } 14 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Binaries for programs and plugins 2 | *.exe 3 | *.exe~ 4 | *.dll 5 | *.so 6 | *.dylib 7 | 8 | # Test binary, built with `go test -c` 9 | *.test 10 | 11 | # Output of the go coverage tool, specifically when used with LiteIDE 12 | *.out 13 | .idea 14 | # Dependency directories (remove the comment below to include it) 15 | # vendor/ 16 | -------------------------------------------------------------------------------- /.github/workflows/go.yml: -------------------------------------------------------------------------------- 1 | name: Go 2 | 3 | on: 4 | push: 5 | branches: [ master ] 6 | pull_request: 7 | branches: [ master ] 8 | 9 | jobs: 10 | 11 | build: 12 | name: Build 13 | runs-on: ubuntu-latest 14 | steps: 15 | 16 | - name: Set up Go 1.x 17 | uses: actions/setup-go@v2 18 | with: 19 | go-version: 1.24 20 | id: go 21 | 22 | - name: Check out code into the Go module directory 23 | uses: actions/checkout@v2 24 | 25 | - name: Get dependencies 26 | run: | 27 | go get -v -t -d ./... 28 | - name: Build 29 | run: go build -v . 30 | 31 | - name: Test 32 | run: make test 33 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/globocom/go-buffer/v3 2 | 3 | retract ( 4 | v3.0.0 // Published prematurely 5 | v3.0.1 // Contains retractions only 6 | ) 7 | 8 | go 1.24 9 | 10 | require ( 11 | github.com/onsi/ginkgo v1.13.0 12 | github.com/onsi/gomega v1.10.1 13 | ) 14 | 15 | require ( 16 | github.com/fsnotify/fsnotify v1.4.9 // indirect 17 | github.com/nxadm/tail v1.4.4 // indirect 18 | golang.org/x/net v0.0.0-20200520004742-59133d7f0dd7 // indirect 19 | golang.org/x/sys v0.0.0-20200519105757-fe76b779f299 // indirect 20 | golang.org/x/text v0.3.2 // indirect 21 | golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 // indirect 22 | gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 // indirect 23 | gopkg.in/yaml.v2 v2.3.0 // indirect 24 | ) 25 | -------------------------------------------------------------------------------- /bench_test.go: -------------------------------------------------------------------------------- 1 | package buffer_test 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/globocom/go-buffer/v3" 7 | ) 8 | 9 | func BenchmarkBuffer(b *testing.B) { 10 | noop := func([]int) {} 11 | 12 | b.Run("push only", func(b *testing.B) { 13 | sut := buffer.New( 14 | noop, 15 | buffer.WithSize(uint(b.N)+1), 16 | ) 17 | defer sut.Close() 18 | 19 | for b.Loop() { 20 | err := sut.Push(1) 21 | if err != nil { 22 | b.Fail() 23 | } 24 | } 25 | }) 26 | 27 | b.Run("push and flush", func(b *testing.B) { 28 | sut := buffer.New( 29 | noop, 30 | buffer.WithSize(1), 31 | ) 32 | defer sut.Close() 33 | 34 | for b.Loop() { 35 | err := sut.Push(1) 36 | if err != nil { 37 | b.Fail() 38 | } 39 | } 40 | }) 41 | } 42 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Globo.com 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /options_test.go: -------------------------------------------------------------------------------- 1 | package buffer_test 2 | 3 | import ( 4 | "time" 5 | 6 | . "github.com/onsi/ginkgo" 7 | . "github.com/onsi/gomega" 8 | 9 | "github.com/globocom/go-buffer/v3" 10 | ) 11 | 12 | var _ = Describe("Options", func() { 13 | It("sets up size", func() { 14 | // arrange 15 | opts := &buffer.Options{} 16 | 17 | // act 18 | buffer.WithSize(10)(opts) 19 | 20 | // assert 21 | Expect(opts.Size).To(BeIdenticalTo(uint(10))) 22 | }) 23 | 24 | It("sets up flush interval", func() { 25 | // arrange 26 | opts := &buffer.Options{} 27 | 28 | // act 29 | buffer.WithFlushInterval(5 * time.Second)(opts) 30 | 31 | // assert 32 | Expect(opts.FlushInterval).To(Equal(5 * time.Second)) 33 | }) 34 | 35 | It("sets up push timeout", func() { 36 | // arrange 37 | opts := &buffer.Options{} 38 | 39 | // act 40 | buffer.WithPushTimeout(10 * time.Second)(opts) 41 | 42 | // assert 43 | Expect(opts.PushTimeout).To(Equal(10 * time.Second)) 44 | }) 45 | 46 | It("sets up flush timeout", func() { 47 | // arrange 48 | opts := &buffer.Options{} 49 | 50 | // act 51 | buffer.WithFlushTimeout(15 * time.Second)(opts) 52 | 53 | // assert 54 | Expect(opts.FlushTimeout).To(Equal(15 * time.Second)) 55 | }) 56 | 57 | It("sets up close timeout", func() { 58 | // arrange 59 | opts := &buffer.Options{} 60 | 61 | // act 62 | buffer.WithCloseTimeout(3 * time.Second)(opts) 63 | 64 | // assert 65 | Expect(opts.CloseTimeout).To(Equal(3 * time.Second)) 66 | }) 67 | }) 68 | -------------------------------------------------------------------------------- /options.go: -------------------------------------------------------------------------------- 1 | package buffer 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | "time" 7 | ) 8 | 9 | const ( 10 | invalidSize = "size cannot be zero" 11 | invalidInterval = "interval must be greater than zero (%s)" 12 | invalidTimeout = "timeout cannot be negative (%s)" 13 | ) 14 | 15 | type ( 16 | // Configuration options. 17 | Options struct { 18 | Size uint 19 | FlushInterval time.Duration 20 | PushTimeout time.Duration 21 | FlushTimeout time.Duration 22 | CloseTimeout time.Duration 23 | } 24 | 25 | // Option setter. 26 | Option func(*Options) 27 | ) 28 | 29 | // WithSize sets the size of the buffer. 30 | func WithSize(size uint) Option { 31 | return func(options *Options) { 32 | options.Size = size 33 | } 34 | } 35 | 36 | // WithFlushInterval sets the interval between automatic flushes. 37 | func WithFlushInterval(interval time.Duration) Option { 38 | return func(options *Options) { 39 | options.FlushInterval = interval 40 | } 41 | } 42 | 43 | // WithPushTimeout sets how long a push should wait before giving up. 44 | func WithPushTimeout(timeout time.Duration) Option { 45 | return func(options *Options) { 46 | options.PushTimeout = timeout 47 | } 48 | } 49 | 50 | // WithFlushTimeout sets how long a manual flush should wait before giving up. 51 | func WithFlushTimeout(timeout time.Duration) Option { 52 | return func(options *Options) { 53 | options.FlushTimeout = timeout 54 | } 55 | } 56 | 57 | // WithCloseTimeout sets how long 58 | func WithCloseTimeout(timeout time.Duration) Option { 59 | return func(options *Options) { 60 | options.CloseTimeout = timeout 61 | } 62 | } 63 | 64 | func validateOptions(options *Options) error { 65 | if options.Size == 0 { 66 | return errors.New(invalidSize) 67 | } 68 | if options.FlushInterval < 0 { 69 | return fmt.Errorf(invalidInterval, "FlushInterval") 70 | } 71 | if options.PushTimeout < 0 { 72 | return fmt.Errorf(invalidTimeout, "PushTimeout") 73 | } 74 | if options.FlushTimeout < 0 { 75 | return fmt.Errorf(invalidTimeout, "FlushTimeout") 76 | } 77 | if options.CloseTimeout < 0 { 78 | return fmt.Errorf(invalidTimeout, "CloseTimeout") 79 | } 80 | 81 | return nil 82 | } 83 | 84 | func resolveOptions(opts ...Option) *Options { 85 | options := &Options{ 86 | Size: 0, 87 | FlushInterval: 0, 88 | PushTimeout: time.Second, 89 | FlushTimeout: time.Second, 90 | CloseTimeout: time.Second, 91 | } 92 | 93 | for _, opt := range opts { 94 | opt(options) 95 | } 96 | 97 | if err := validateOptions(options); err != nil { 98 | panic(err) 99 | } 100 | 101 | return options 102 | } 103 | -------------------------------------------------------------------------------- /buffer.go: -------------------------------------------------------------------------------- 1 | package buffer 2 | 3 | import ( 4 | "errors" 5 | "io" 6 | "time" 7 | ) 8 | 9 | var ( 10 | // ErrTimeout indicates an operation has timed out. 11 | ErrTimeout = errors.New("operation timed-out") 12 | // ErrClosed indicates the buffer is closed and can no longer be used. 13 | ErrClosed = errors.New("buffer is closed") 14 | ) 15 | 16 | type ( 17 | // Buffer represents a data buffer that is asynchronously flushed, either manually or automatically. 18 | Buffer[T any] struct { 19 | io.Closer 20 | flushFunc func([]T) 21 | dataCh chan T 22 | flushCh chan struct{} 23 | closeCh chan struct{} 24 | doneCh chan struct{} 25 | options *Options 26 | } 27 | ) 28 | 29 | // New creates a new buffer instance with the provided flush function and options. 30 | // It panics if provided with a nil flush function. 31 | func New[T any](flushFunc func([]T), opts ...Option) *Buffer[T] { 32 | if flushFunc == nil { 33 | panic("flush function cannot be nil") 34 | } 35 | 36 | buffer := &Buffer[T]{ 37 | flushFunc: flushFunc, 38 | dataCh: make(chan T), 39 | flushCh: make(chan struct{}), 40 | closeCh: make(chan struct{}), 41 | doneCh: make(chan struct{}), 42 | options: resolveOptions(opts...), 43 | } 44 | go buffer.consume() 45 | 46 | return buffer 47 | } 48 | 49 | // Push appends an item to the end of the buffer. 50 | // 51 | // It returns an ErrTimeout if it cannot be performed in a timely fashion, and 52 | // an ErrClosed if the buffer has been closed. 53 | func (buffer *Buffer[T]) Push(item T) error { 54 | if buffer.closed() { 55 | return ErrClosed 56 | } 57 | 58 | select { 59 | case buffer.dataCh <- item: 60 | return nil 61 | case <-time.After(buffer.options.PushTimeout): 62 | return ErrTimeout 63 | } 64 | } 65 | 66 | // Flush outputs the buffer to a permanent destination. 67 | // 68 | // It returns an ErrTimeout if if cannot be performed in a timely fashion, and 69 | // an ErrClosed if the buffer has been closed. 70 | func (buffer *Buffer[T]) Flush() error { 71 | if buffer.closed() { 72 | return ErrClosed 73 | } 74 | 75 | select { 76 | case buffer.flushCh <- struct{}{}: 77 | return nil 78 | case <-time.After(buffer.options.FlushTimeout): 79 | return ErrTimeout 80 | } 81 | } 82 | 83 | // Close flushes the buffer and prevents it from being further used. 84 | // 85 | // It returns an ErrTimeout if if cannot be performed in a timely fashion, and 86 | // an ErrClosed if the buffer has already been closed. 87 | // 88 | // An ErrTimeout can either mean that a flush could not be triggered, or it can 89 | // mean that a flush was triggered but it has not finished yet. In any case it is 90 | // safe to call Close again. 91 | func (buffer *Buffer[T]) Close() error { 92 | if buffer.closed() { 93 | return ErrClosed 94 | } 95 | 96 | select { 97 | case buffer.closeCh <- struct{}{}: 98 | // noop 99 | case <-time.After(buffer.options.CloseTimeout): 100 | return ErrTimeout 101 | } 102 | 103 | select { 104 | case <-buffer.doneCh: 105 | close(buffer.dataCh) 106 | close(buffer.flushCh) 107 | close(buffer.closeCh) 108 | return nil 109 | case <-time.After(buffer.options.CloseTimeout): 110 | return ErrTimeout 111 | } 112 | } 113 | 114 | func (buffer *Buffer[T]) closed() bool { 115 | select { 116 | case <-buffer.doneCh: 117 | return true 118 | default: 119 | return false 120 | } 121 | } 122 | 123 | func (buffer *Buffer[T]) consume() { 124 | count := 0 125 | items := make([]T, buffer.options.Size) 126 | mustFlush := false 127 | ticker, stopTicker := newTicker(buffer.options.FlushInterval) 128 | 129 | isOpen := true 130 | for isOpen { 131 | select { 132 | case item := <-buffer.dataCh: 133 | items[count] = item 134 | count++ 135 | mustFlush = count >= len(items) 136 | case <-ticker: 137 | mustFlush = count > 0 138 | case <-buffer.flushCh: 139 | mustFlush = count > 0 140 | case <-buffer.closeCh: 141 | isOpen = false 142 | mustFlush = count > 0 143 | } 144 | 145 | if mustFlush { 146 | stopTicker() 147 | buffer.flushFunc(items[:count]) 148 | 149 | count = 0 150 | items = make([]T, buffer.options.Size) 151 | mustFlush = false 152 | ticker, stopTicker = newTicker(buffer.options.FlushInterval) 153 | } 154 | } 155 | 156 | stopTicker() 157 | close(buffer.doneCh) 158 | } 159 | 160 | func newTicker(interval time.Duration) (<-chan time.Time, func()) { 161 | if interval == 0 { 162 | return nil, func() {} 163 | } 164 | 165 | ticker := time.NewTicker(interval) 166 | return ticker.C, ticker.Stop 167 | } 168 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= 2 | github.com/fsnotify/fsnotify v1.4.9 h1:hsms1Qyu0jgnwNXIxa+/V/PDsU6CfLf6CNO8H7IWoS4= 3 | github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ= 4 | github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= 5 | github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8= 6 | github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA= 7 | github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs= 8 | github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w= 9 | github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0= 10 | github.com/golang/protobuf v1.4.2 h1:+Z5KGCizgyZCbGh1KZqA0fcLLkwbsjIzS4aV2v7wJX0= 11 | github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= 12 | github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= 13 | github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= 14 | github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= 15 | github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU= 16 | github.com/nxadm/tail v1.4.4 h1:DQuhQpB1tVlglWS2hLQ5OV6B5r8aGxSrPc5Qo6uTN78= 17 | github.com/nxadm/tail v1.4.4/go.mod h1:kenIhsEOeOJmVchQTgglprH7qJGnHDVpk1VPCcaMI8A= 18 | github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= 19 | github.com/onsi/ginkgo v1.12.1/go.mod h1:zj2OWP4+oCPe1qIXoGWkgMRwljMUYCdkwsT2108oapk= 20 | github.com/onsi/ginkgo v1.13.0 h1:M76yO2HkZASFjXL0HSoZJ1AYEmQxNJmY41Jx1zNUq1Y= 21 | github.com/onsi/ginkgo v1.13.0/go.mod h1:+REjRxOmWfHCjfv9TTWB1jD1Frx4XydAD3zm1lskyM0= 22 | github.com/onsi/gomega v1.7.1/go.mod h1:XdKZgCCFLUoM/7CFJVPcG8C1xQ1AJ0vpAezJrB7JYyY= 23 | github.com/onsi/gomega v1.10.1 h1:o0+MgICZLuZ7xjH7Vx6zS/zcu93/BEp1VwkIW1mEXCE= 24 | github.com/onsi/gomega v1.10.1/go.mod h1:iN09h71vgCQne3DLsj+A5owkum+a2tYe+TOCB1ybHNo= 25 | github.com/sclevine/agouti v3.0.0+incompatible/go.mod h1:b4WX9W9L1sfQKXeJf1mUTLZKJ48R1S7H23Ji7oFO5Bw= 26 | golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= 27 | golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 28 | golang.org/x/net v0.0.0-20200520004742-59133d7f0dd7 h1:AeiKBIuRw3UomYXSbLy0Mc2dDLfdtbT/IVn4keq83P0= 29 | golang.org/x/net v0.0.0-20200520004742-59133d7f0dd7/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= 30 | golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 31 | golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 32 | golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 33 | golang.org/x/sys v0.0.0-20190904154756-749cb33beabd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 34 | golang.org/x/sys v0.0.0-20191005200804-aed5e4c7ecf9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 35 | golang.org/x/sys v0.0.0-20191120155948-bd437916bb0e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 36 | golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 37 | golang.org/x/sys v0.0.0-20200519105757-fe76b779f299 h1:DYfZAGf2WMFjMxbgTjaC+2HC7NkNAQs+6Q8b9WEB/F4= 38 | golang.org/x/sys v0.0.0-20200519105757-fe76b779f299/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 39 | golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 40 | golang.org/x/text v0.3.2 h1:tW2bmiBqwgJj/UpqtC8EpXEZVYOwU0yG4iWbprSVAcs= 41 | golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= 42 | golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 43 | golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 h1:E7g+9GITq07hpfrRu66IVDexMakfv52eLZ2CXBWiKr4= 44 | golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 45 | google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= 46 | google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= 47 | google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM= 48 | google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE= 49 | google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo= 50 | google.golang.org/protobuf v1.23.0 h1:4MY060fB1DLGMB/7MBTLnwQUY6+F09GEiz6SsrNqyzM= 51 | google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= 52 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= 53 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 54 | gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys= 55 | gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ= 56 | gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw= 57 | gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 58 | gopkg.in/yaml.v2 v2.3.0 h1:clyUAQHOM3G0M3f5vQj7LuJrETvjVot3Z5el9nffUtU= 59 | gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 60 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |
2 |
3 |