├── .travis.yml ├── LICENSE ├── README.md ├── apollo.go ├── apollo_test.go ├── chain.go ├── chain_test.go └── fixtures_test.go /.travis.yml: -------------------------------------------------------------------------------- 1 | language: go 2 | 3 | matrix: 4 | include: 5 | - go: 1.7 6 | - go: tip 7 | allow_failures: 8 | - go: tip 9 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2017 This End Out, LLC 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 13 | all 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 21 | THE SOFTWARE. 22 | 23 | The contents of chain.go is a derivate work of github.com/justinas/alice, 24 | distributed under the following license: 25 | 26 | The MIT License (MIT) 27 | 28 | Copyright (c) 2014 Justinas Stankevicius 29 | 30 | Permission is hereby granted, free of charge, to any person obtaining a copy of 31 | this software and associated documentation files (the "Software"), to deal in 32 | the Software without restriction, including without limitation the rights to 33 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 34 | the Software, and to permit persons to whom the Software is furnished to do so, 35 | subject to the following conditions: 36 | 37 | The above copyright notice and this permission notice shall be included in all 38 | copies or substantial portions of the Software. 39 | 40 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 41 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS 42 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 43 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 44 | IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 45 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 46 | 47 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Apollo 2 | ====== 3 | [![GoDoc](https://godoc.org/github.com/thisendout/apollo?status.svg)](https://godoc.org/github.com/thisendout/apollo) [![Build Status](https://travis-ci.org/thisendout/apollo.svg?branch=master)](https://travis-ci.org/thisendout/apollo) 4 | 5 | Apollo is a middleware-chaining helper for Golang web applications using the stdlib `context` package. Apollo is a fork of [Alice](https://github.com/justinas/alice), modified to support passing the `ctx context.Context` param through middleware and HTTP handlers. 6 | 7 | Apollo is meant to chain handler functions with this signature: 8 | ```go 9 | func (context.Context, http.ResponseWriter, *http.Request) 10 | ``` 11 | 12 | # Changelog 13 | 14 | * v2.0.0 - Pending 15 | * Updated to use stdlib `context` in go 1.7+. Use v1.0.0 for projects utilizing `golang.org/x/net/context`. 16 | * Repository transferred from [cyclopsci/apollo](https://github.com/cyclopsci/apollo), which is now a fork of this repo. 17 | 18 | * v1.0.0 19 | * Initial release with chaining for golang.org/x/net/context 20 | 21 | # Usage 22 | 23 | ```go 24 | apollo.New(Middleware1, Middlware2, Middleware3).With(ctx).Then(App) 25 | ``` 26 | 27 | # Integration with http.Handler middleware 28 | 29 | Apollo provides a `Wrap` function to inject normal http.Handler-based middleware into the chain. The context will skip over the injected middleware and pass unharmed to the next context-aware handler in the chain. 30 | ```go 31 | apollo.New(ContextMW1, apollo.Wrap(NormalMiddlware), ContextMW2).With(ctx).Then(App) 32 | ``` 33 | # Motivation 34 | 35 | Given a handler: 36 | ```go 37 | func HandlerOne(w http.ResponseWriter, r *http.Request) {} 38 | ``` 39 | 40 | We can serve it using the following: 41 | ```go 42 | http.HandleFunc("/one", HandlerOne) 43 | // or http.Handle("/one", http.HandlerFunc(HandlerOne)) 44 | ``` 45 | 46 | However, given a handler that expects a `net/context`: 47 | ```go 48 | func HandlerAlpha(ctx context.Context, w http.ResponseWriter, r *http.Request) {} 49 | ``` 50 | 51 | We would need to create a wrapper along the lines of: 52 | ```go 53 | func withContext(ctx context.Context, fn func(context.Context, http.ResponseWriter, *http.Request)) http.HandlerFunc { 54 | return func(w http.ResponseWriter, r *http.Request) { 55 | fn(ctx, w, r) 56 | } 57 | } 58 | ``` 59 | and serve with: 60 | ```go 61 | ctx := context.Background() 62 | http.Handle("/alpha", withContext(ctx, HandlerAlpha)) 63 | ``` 64 | 65 | With this pattern, we can build nested middleware/handler calls that can be used with any `net/http` compatible router/mux. However, we can't use Alice for chaining because we no longer conform to the http.Handler interface that Alice expects. 66 | 67 | Apollo enables Alice-style chaining of context-aware middleware and handlers. 68 | 69 | # Reference 70 | 71 | Relevant and influential articles: 72 | * https://blog.golang.org/context 73 | * https://joeshaw.org/net-context-and-http-handler/ 74 | * https://elithrar.github.io/article/map-string-interface/ 75 | * http://www.alexedwards.net/blog/making-and-using-middleware 76 | * http://laicos.com/writing-handsome-golang-middleware/ 77 | * http://nicolasmerouze.com/share-values-between-middlewares-context-golang/ 78 | * https://elithrar.github.io/article/custom-handlers-avoiding-globals/ 79 | * http://www.jerf.org/iri/post/2929 80 | 81 | -------------------------------------------------------------------------------- /apollo.go: -------------------------------------------------------------------------------- 1 | // Apollo provides `net/context`-aware middleware chaining 2 | package apollo 3 | 4 | import ( 5 | "net/http" 6 | 7 | "context" 8 | ) 9 | 10 | // Handler is a context-aware interface analagous to the `net/http` http.Handler interface 11 | // The only difference is that a context.Context is required as the first parameter in ServeHTTP. 12 | type Handler interface { 13 | ServeHTTP(context.Context, http.ResponseWriter, *http.Request) 14 | } 15 | 16 | // HandlerFunc, similar to http.HandlerFunc, is an adapter to convert ordinary functions 17 | // into handlers. 18 | type HandlerFunc func(context.Context, http.ResponseWriter, *http.Request) 19 | 20 | // ServeHTTP calls the wrapped function h(ctx, w, r) 21 | func (h HandlerFunc) ServeHTTP(ctx context.Context, w http.ResponseWriter, r *http.Request) { 22 | h(ctx, w, r) 23 | } 24 | 25 | // addsContext is an adapter that wraps a Handler and implements the http.Handler interface. 26 | // The resulting object is used as a bridge to integrate existing `net/http` functions with 27 | // a context-aware chain or handler. 28 | // Internally, it is used as an onramp to the chain in Then(), and as an adapter for 29 | // injecting non-context-aware handlers with Wrap() 30 | type addsContext struct { 31 | ctx context.Context 32 | handler Handler 33 | } 34 | 35 | // ServeHTTP calls the stored handler with the stored context, passing through the received 36 | // HTTP Response and Request 37 | func (a *addsContext) ServeHTTP(w http.ResponseWriter, r *http.Request) { 38 | a.handler.ServeHTTP(a.ctx, w, r) 39 | } 40 | 41 | // stripsContext is an adapter that wraps a http.Handler and implements the Handler interface. 42 | // The resulting object can be used to insert a non-context-aware function into a context-aware 43 | // chain. 44 | // Internally, it is used to in Wrap() to inject standard handlers. It could also be used to 45 | // link context-aware middleware to a standard handler. 46 | type stripsContext struct { 47 | handler http.Handler 48 | } 49 | 50 | // ServeHTTP calls the stored handler, dropping the context and passing only the HTTP ResponseWriter and Request 51 | func (s *stripsContext) ServeHTTP(ctx context.Context, w http.ResponseWriter, r *http.Request) { 52 | s.handler.ServeHTTP(w, r) 53 | } 54 | 55 | // Wrap allows injection of normal http.Handler middleware into an 56 | // apollo middleware chain 57 | // The context will be preserved and passed through intact 58 | func Wrap(h func(http.Handler) http.Handler) Constructor { 59 | return func(next Handler) Handler { 60 | return HandlerFunc(func(ctx context.Context, w http.ResponseWriter, r *http.Request) { 61 | stubHandler := &addsContext{ 62 | ctx: ctx, 63 | handler: next, 64 | } 65 | h(stubHandler).ServeHTTP(w, r) 66 | }) 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /apollo_test.go: -------------------------------------------------------------------------------- 1 | // Apollo provides `net/context`-aware middleware chaining 2 | package apollo 3 | 4 | import ( 5 | "io/ioutil" 6 | "net/http" 7 | "net/http/httptest" 8 | "testing" 9 | 10 | "context" 11 | "github.com/stretchr/testify/assert" 12 | ) 13 | 14 | func TestHandlerFunc(t *testing.T) { 15 | assert := assert.New(t) 16 | assert.NotPanics(func() { 17 | ctx := context.Background() 18 | r, _ := http.NewRequest("GET", "http://github.com/", nil) 19 | w := httptest.NewRecorder() 20 | 21 | handler := HandlerFunc(handlerOne) 22 | assert.Implements((*Handler)(nil), handler) 23 | 24 | handler.ServeHTTP(ctx, w, r) 25 | assert.Equal(w.Code, 200) 26 | assert.Equal(w.Body.String(), "h1\n") 27 | }) 28 | } 29 | 30 | func TestAddsContextServe(t *testing.T) { 31 | assert := assert.New(t) 32 | adapter := addsContext{ 33 | ctx: context.Background(), 34 | handler: HandlerFunc(handlerOne), 35 | } 36 | assert.NotPanics(func() { 37 | r, _ := http.NewRequest("GET", "http://github.com/", nil) 38 | w := httptest.NewRecorder() 39 | 40 | adapter.ServeHTTP(w, r) 41 | assert.Equal(w.Code, 200) 42 | assert.Equal(w.Body.String(), "h1\n") 43 | }) 44 | } 45 | 46 | func TestStripsContextServe(t *testing.T) { 47 | assert := assert.New(t) 48 | adapter := stripsContext{http.HandlerFunc(handlerZero)} 49 | assert.NotPanics(func() { 50 | ctx := context.Background() 51 | r, _ := http.NewRequest("GET", "http://github.com/", nil) 52 | w := httptest.NewRecorder() 53 | 54 | adapter.ServeHTTP(ctx, w, r) 55 | assert.Equal(w.Code, 200) 56 | assert.Equal(w.Body.String(), "h0\n") 57 | }) 58 | } 59 | 60 | func TestWrap(t *testing.T) { 61 | assert := assert.New(t) 62 | assert.NotPanics(func() { 63 | con := Wrap(middleZero) 64 | assert.IsType(con, *new(Constructor)) 65 | }) 66 | } 67 | 68 | func TestWrapChains(t *testing.T) { 69 | assert := assert.New(t) 70 | ctx := NewTestContext(context.Background(), 10) 71 | value, _ := FromContext(ctx) 72 | assert.Equal(value, 10) 73 | 74 | chain := New(middleOne, Wrap(middleZero), middleTwo).With(ctx).ThenFunc(handlerContext) 75 | 76 | ts := httptest.NewServer(chain) 77 | defer ts.Close() 78 | 79 | res, err := http.Get(ts.URL) 80 | assert.NoError(err) 81 | 82 | body, err := ioutil.ReadAll(res.Body) 83 | res.Body.Close() 84 | 85 | assert.Equal(200, res.StatusCode) 86 | assert.Equal("m1\nm0\nm2\n10\n", string(body)) 87 | } 88 | -------------------------------------------------------------------------------- /chain.go: -------------------------------------------------------------------------------- 1 | // Apollo provides `net/context`-aware middleware chaining 2 | package apollo 3 | 4 | import ( 5 | "net/http" 6 | 7 | "context" 8 | ) 9 | 10 | // A constructor for a piece of context-aware middleware. 11 | type Constructor func(Handler) Handler 12 | 13 | // Chain acts as a list of apollo.Handler constructors. 14 | // Chain is effectively immutable: 15 | // once created, it will always hold 16 | // the same set of constructors in the same order. 17 | // Chain also holds a copy of the context to be injected 18 | // to the first middleware when .Then() is called. 19 | type Chain struct { 20 | constructors []Constructor 21 | context context.Context 22 | } 23 | 24 | // New creates a new chain, 25 | // memorizing the given list of middleware constructors. 26 | // New serves no other function, 27 | // constructors are only called upon a call to Then(). 28 | func New(constructors ...Constructor) Chain { 29 | c := Chain{} 30 | c.constructors = append(c.constructors, constructors...) 31 | c.context = context.Background() 32 | 33 | return c 34 | } 35 | 36 | // Then chains the middleware and returns the final Handler. 37 | // New(m1, m2, m3).Then(h) 38 | // is equivalent to: 39 | // m1(m2(m3(h))) 40 | // When the request comes in, it will be passed to m1, then m2, then m3 41 | // and finally, the given handler 42 | // (assuming every middleware calls the following one). 43 | // 44 | // A chain can be safely reused by calling Then() several times. 45 | // stdStack := alice.New(ratelimitHandler, csrfHandler) 46 | // indexPipe = stdStack.Then(indexHandler) 47 | // authPipe = stdStack.Then(authHandler) 48 | // Note that constructors are called on every call to Then() 49 | // and thus several instances of the same middleware will be created 50 | // when a chain is reused in this way. 51 | // For proper middleware, this should cause no problems. 52 | // 53 | // Then() treats nil as http.DefaultServeMux. 54 | func (c Chain) Then(h Handler) http.Handler { 55 | var final Handler 56 | if h != nil { 57 | final = h 58 | } else { 59 | final = &stripsContext{ 60 | handler: http.DefaultServeMux, 61 | } 62 | } 63 | 64 | for i := len(c.constructors) - 1; i >= 0; i-- { 65 | final = c.constructors[i](final) 66 | } 67 | 68 | adapter := addsContext{ 69 | ctx: c.context, 70 | handler: final, 71 | } 72 | 73 | return &adapter 74 | } 75 | 76 | // ThenFunc works identically to Then, but takes 77 | // a HandlerFunc instead of a Handler. 78 | // 79 | // The following two statements are equivalent: 80 | // c.Then(http.HandlerFunc(fn)) 81 | // c.ThenFunc(fn) 82 | // 83 | // ThenFunc provides all the guarantees of Then. 84 | func (c Chain) ThenFunc(fn HandlerFunc) http.Handler { 85 | if fn == nil { 86 | return c.Then(nil) 87 | } 88 | return c.Then(HandlerFunc(fn)) 89 | } 90 | 91 | // Append extends a chain, adding the specified constructors 92 | // as the last ones in the request flow. 93 | // 94 | // Append returns a new chain, leaving the original one untouched. 95 | // 96 | // stdChain := alice.New(m1, m2) 97 | // extChain := stdChain.Append(m3, m4) 98 | // // requests in stdChain go m1 -> m2 99 | // // requests in extChain go m1 -> m2 -> m3 -> m4 100 | func (c Chain) Append(constructors ...Constructor) Chain { 101 | newCons := make([]Constructor, len(c.constructors)+len(constructors)) 102 | copy(newCons, c.constructors) 103 | copy(newCons[len(c.constructors):], constructors) 104 | 105 | newChain := New(newCons...) 106 | return newChain.With(c.context) 107 | } 108 | 109 | // With sets the context to be passed to the start of the Chain. 110 | // 111 | // The final handler will use the modified context that has passed 112 | // through the middleware chain. 113 | func (c Chain) With(ctx context.Context) Chain { 114 | c.context = ctx 115 | return c 116 | } 117 | -------------------------------------------------------------------------------- /chain_test.go: -------------------------------------------------------------------------------- 1 | // Apollo provides `net/context`-aware middleware chaining 2 | package apollo 3 | 4 | import ( 5 | "io/ioutil" 6 | "net/http" 7 | "net/http/httptest" 8 | "testing" 9 | 10 | "context" 11 | "github.com/stretchr/testify/assert" 12 | ) 13 | 14 | func TestDefaultNew(t *testing.T) { 15 | assert := assert.New(t) 16 | assert.NotPanics(func() { 17 | chain := New() 18 | assert.Len(chain.constructors, 0) 19 | assert.Equal(chain.context, context.Background()) 20 | }) 21 | } 22 | 23 | func TestThenNoMiddleware(t *testing.T) { 24 | assert := assert.New(t) 25 | assert.NotPanics(func() { 26 | chain := New() 27 | final := chain.Then(HandlerFunc(handlerOne)) 28 | assert.Implements((*http.Handler)(nil), final) 29 | }) 30 | } 31 | 32 | func TestThenFuncNoMiddleware(t *testing.T) { 33 | assert := assert.New(t) 34 | assert.NotPanics(func() { 35 | chain := New() 36 | final := chain.ThenFunc(handlerOne) 37 | assert.Implements((*http.Handler)(nil), final) 38 | }) 39 | } 40 | 41 | func TestThenNil(t *testing.T) { 42 | assert := assert.New(t) 43 | assert.NotPanics(func() { 44 | final := New().Then(nil) 45 | assert.Implements((*http.Handler)(nil), final) 46 | }) 47 | } 48 | 49 | func TestThenFuncNil(t *testing.T) { 50 | assert := assert.New(t) 51 | assert.NotPanics(func() { 52 | final := New().ThenFunc(nil) 53 | assert.Implements((*http.Handler)(nil), final) 54 | }) 55 | } 56 | 57 | func TestAppend(t *testing.T) { 58 | assert := assert.New(t) 59 | chain := New(middleOne) 60 | newChain := chain.Append(middleTwo) 61 | assert.Len(chain.constructors, 1) 62 | assert.Len(newChain.constructors, 2) 63 | } 64 | 65 | func TestAppendContext(t *testing.T) { 66 | assert := assert.New(t) 67 | chain := New(middleOne) 68 | newChain := chain.Append(middleTwo) 69 | assert.Equal(chain.context, context.Background()) 70 | assert.Equal(newChain.context, context.Background()) 71 | } 72 | 73 | func TestAppendAfterWith(t *testing.T) { 74 | assert := assert.New(t) 75 | ctx := NewTestContext(context.Background(), 10) 76 | 77 | chain := New(middleOne) 78 | withChain := chain.With(ctx) 79 | newChain := withChain.Append(middleTwo) 80 | assert.Equal(chain.context, context.Background()) 81 | assert.Equal(withChain.context, ctx) 82 | assert.Equal(newChain.context, ctx) 83 | } 84 | 85 | func TestWithAfterAppend(t *testing.T) { 86 | assert := assert.New(t) 87 | ctx := NewTestContext(context.Background(), 10) 88 | 89 | chain := New(middleOne) 90 | newChain := chain.Append(middleTwo) 91 | withChain := newChain.With(ctx) 92 | assert.Equal(chain.context, context.Background()) 93 | assert.Equal(newChain.context, context.Background()) 94 | assert.Equal(withChain.context, ctx) 95 | } 96 | 97 | func TestWithInPlace(t *testing.T) { 98 | assert := assert.New(t) 99 | ctx := NewTestContext(context.Background(), 10) 100 | 101 | chain := New(middleOne) 102 | chain.With(ctx) 103 | newChain := chain.Append(middleTwo) 104 | assert.Equal(chain.context, context.Background()) 105 | assert.Equal(newChain.context, context.Background()) 106 | } 107 | 108 | func TestWithReassigned(t *testing.T) { 109 | assert := assert.New(t) 110 | ctx := NewTestContext(context.Background(), 10) 111 | 112 | chain := New(middleOne) 113 | chain = chain.With(ctx) 114 | newChain := chain.Append(middleTwo) 115 | assert.Equal(chain.context, ctx) 116 | assert.Equal(newChain.context, ctx) 117 | } 118 | 119 | func TestWithMultiples(t *testing.T) { 120 | assert := assert.New(t) 121 | ctx := NewTestContext(context.Background(), 10) 122 | 123 | chain := New(middleOne) 124 | assert.Equal(chain.context, context.Background()) 125 | chain = chain.With(ctx) 126 | assert.Equal(chain.context, ctx) 127 | chain = chain.With(context.TODO()).With(ctx) 128 | assert.Equal(chain.context, ctx) 129 | } 130 | 131 | func TestChains(t *testing.T) { 132 | assert := assert.New(t) 133 | ctx := NewTestContext(context.Background(), 10) 134 | value, _ := FromContext(ctx) 135 | assert.Equal(value, 10) 136 | 137 | chain := New(middleOne, middleTwo).With(ctx).ThenFunc(handlerContext) 138 | 139 | ts := httptest.NewServer(chain) 140 | defer ts.Close() 141 | 142 | res, err := http.Get(ts.URL) 143 | assert.NoError(err) 144 | 145 | body, err := ioutil.ReadAll(res.Body) 146 | res.Body.Close() 147 | 148 | assert.Equal(res.StatusCode, 200) 149 | assert.Equal(string(body), "m1\nm2\n10\n") 150 | } 151 | -------------------------------------------------------------------------------- /fixtures_test.go: -------------------------------------------------------------------------------- 1 | // Apollo provides `net/context`-aware middleware chaining 2 | package apollo 3 | 4 | import ( 5 | "net/http" 6 | "strconv" 7 | 8 | "context" 9 | ) 10 | 11 | func handlerZero(w http.ResponseWriter, r *http.Request) { 12 | w.Write([]byte("h0\n")) 13 | } 14 | 15 | func handlerOne(ctx context.Context, w http.ResponseWriter, r *http.Request) { 16 | w.Write([]byte("h1\n")) 17 | } 18 | 19 | func handlerContext(ctx context.Context, w http.ResponseWriter, r *http.Request) { 20 | if value, ok := FromContext(ctx); ok { 21 | contents := strconv.Itoa(value) + "\n" 22 | w.Write([]byte(contents)) 23 | } 24 | } 25 | 26 | func middleZero(h http.Handler) http.Handler { 27 | return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 28 | w.Write([]byte("m0\n")) 29 | h.ServeHTTP(w, r) 30 | }) 31 | } 32 | 33 | func middleOne(h Handler) Handler { 34 | return HandlerFunc(func(ctx context.Context, w http.ResponseWriter, r *http.Request) { 35 | w.Write([]byte("m1\n")) 36 | h.ServeHTTP(ctx, w, r) 37 | }) 38 | } 39 | 40 | func middleTwo(h Handler) Handler { 41 | return HandlerFunc(func(ctx context.Context, w http.ResponseWriter, r *http.Request) { 42 | w.Write([]byte("m2\n")) 43 | h.ServeHTTP(ctx, w, r) 44 | }) 45 | } 46 | 47 | // TestContext 48 | type key int 49 | 50 | const testKey key = 0 51 | 52 | func NewTestContext(ctx context.Context, dummy int) context.Context { 53 | return context.WithValue(ctx, testKey, dummy) 54 | } 55 | 56 | func FromContext(ctx context.Context) (int, bool) { 57 | dummy, ok := ctx.Value(testKey).(int) 58 | return dummy, ok 59 | } 60 | --------------------------------------------------------------------------------