æåã®éšåã§ã¯ãREST APIãå®è£ ããçä¿¡HTTPãªã¯ãšã¹ããåéããæ¹æ³ãåŠã³ãŸããã ãã®ããŒãã§ã¯ãã¢ããªã±ãŒã·ã§ã³ããã¹ãã§ã«ããŒããAngularJSãšBootstrapã«åºã¥ããçŸããWebã€ã³ã¿ãŒãã§ãŒã¹ãè¿œå ããããŸããŸãªãŠãŒã¶ãŒã®ã¢ã¯ã»ã¹å¶éãå®è£ ããŸãã
ãã®ããŒãã§ã¯ã次ã®æ®µéãåŸ ã£ãŠããŸãã
- 4çªç®ã®ã¹ãããã ãããããã¹ãã¯ã©ãã§ããïŒ
- ã¹ããã5 âè£ é£ŸãšWebã€ã³ã¿ãŒãã§ãŒã¹ã
- ã¹ããã6 ãã©ã€ãã·ãŒãè¿œå ããŸãã
- 7çªç®ã®ã¹ãããã äžèŠãªãã®ããããã«ããŸãã
- ã¹ããã8ã ã¹ãã¬ãŒãžã«ã¯Redisã䜿çšããŠããŸãã
4çªç®ã®ã¹ãããã ãããããã¹ãã¯ã©ãã§ããïŒ
Goã«ã¯ããã¹ããåŠçããããã®å€æ°ã®çµã¿èŸŒã¿ããŒã«ããããŸãã éåžžã®åäœãã¹ãïŒåäœãã¹ãïŒãšãããšãã°ãã³ãããŒã¯ãã¹ãã®äž¡æ¹ãäœæããããšãã§ããŸãã ãã®ããŒã«ãããã§ã¯ããã¹ãã§ã³ãŒãã«ãã¬ããžã確èªããããšãã§ããŸãã
ãã¹ããæäœããããã®åºæ¬ããã±ãŒãžã¯testingã§ãã ããã§ã®2ã€ã®äž»ãªã¿ã€ãã¯ãéåžžã®åäœãã¹ãçšã®
T
ãšè² è·ãã¹ãçšã®
B
ã§ãã Goã®ãã¹ãã¯ãã¡ã€ã³ããã°ã©ã ãšåãããã±ãŒãžã«
_test
ãããæ¥å°ŸèŸ
_test
è¿œå ãããŸãã ãããã£ãŠãããã±ãŒãžå ã§äœ¿çšå¯èœãªãã©ã€ããŒãããŒã¿æ§é ã¯ããã¹ãå ã§ã䜿çšã§ããŸãïŒãã¹ããäºãã«å ±éã®ã°ããŒãã«ã¹ã³ãŒããæã£ãŠããããšãäºå®ã§ãïŒã ã¡ã€ã³ããã°ã©ã ãã³ã³ãã€ã«ãããšãããã¹ããã¡ã€ã«ã¯ç¡èŠãããŸãã
åºæ¬çãªãã¹ãããã±ãŒãžã«å ããŠããã¹ãã®èšè¿°ãç°¡çŽ åããããŸãã¯1ã€ãŸãã¯å¥ã®ã¹ã¿ã€ã«ïŒ BDDã¹ã¿ã€ã«ã§ãïŒã§èšè¿°ã§ããããã«ããå€æ°ã®ãµãŒãããŒãã£ã©ã€ãã©ãªããããŸãã ããšãã°ãããã§ã¯ã TDDã¹ã¿ã€ã«ã§Goãèšè¿°ããæ¹æ³ã«é¢ããåªããå ¥éèšäºããããŸãã
GitHubã«ã¯ããã¹ãã©ã€ãã©ãªãæ¯èŒããããã®ãã¬ãŒãããããŸãããã®äžã«ã¯ãWebã€ã³ã¿ãŒãã§ãŒã¹ãæäŸããgoconveyãªã©ã®ã¢ã³ã¹ã¿ãŒãããã¹ãã®åæ Œã«é¢ããéç¥ãªã©ã®ã·ã¹ãã ãšã®çžäºäœçšããããŸãã ããããç©äºãè€éã«ããªãããã«ããã®ãããžã§ã¯ãã§ã¯ãæ¡ä»¶ããã§ãã¯ããŠã¢ãã¯ãªããžã§ã¯ããäœæããããã®ããã€ãã®ããªããã£ãã®ã¿ãè¿œå ããå°ããªèšŒèšã©ã€ãã©ãªã䜿çšããŸãã
4çªç®ã®ã¹ãããã®ã³ãŒããããŠã³ããŒãããŸãã
git checkout step-4
ã¢ãã«ã®ãã¹ããæžãããšããå§ããŸãããã ãã¡ã€ã«models_test.goãäœæããŸãã go testãŠãŒãã£ãªãã£ã§æ€åºããã«ã¯ããã¹ãä»ãã®é¢æ°ã次ã®ãã¿ãŒã³ãæºãããŠããå¿ èŠããããŸãã
func TestXxx(*testing.T)
Binãªããžã§ã¯ãã®æ£ããäœæããã§ãã¯ããæåã®ãã¹ããäœæããŸãã
func TestNewBin(t *testing.T) { now := time.Now().Unix() bin := NewBin() if assert.NotNil(t, bin) { assert.Equal(t, len(bin.Name), 6) assert.Equal(t, bin.RequestCount, 0) assert.Equal(t, bin.Created, bin.Updated) assert.True(t, bin.Created < (now+1)) assert.True(t, bin.Created > (now-1)) } }
testifyã®ãã¹ãŠã®ãã¹ãã¡ãœããã¯ã* testing.Tãªããžã§ã¯ããæåã®ãã©ã¡ãŒã¿ãŒãšããŠåãå ¥ããŸãã
次ã«ãééã£ããã¹ãšå¢çå€ãå¿ããã«ããã¹ãŠã®ã·ããªãªããã¹ãããŸãã èšäºã«ã¯ãã¹ãŠã®ãã¹ãã®ã³ãŒããåŒçšããã€ããã¯ãããŸãããå€ãã®ãã¹ããããããªããžããªã§ãããã«æ £ããããšãã§ããã®ã§ãæãèå³æ·±ãç¹ã®ã¿ãè¿°ã¹ãŸãã
api_test.goãã¡ã€ã«ã«æ³šæããŠãã ããããã®ãã¡ã€ã«ã§REST APIããã¹ãããŸãã ããŒã¿ã®ã¹ãã¬ãŒãžå®è£ ã«äŸåããªãããã«ãStorageã€ã³ã¿ãŒãã§ãŒã¹ã®åäœãå®è£ ããã¢ãã¯ãªããžã§ã¯ããè¿œå ããŸãã ããã¯ã æš¡æ¬ããã±ãŒãž testifyã䜿çšããŠè¡ããŸã ã ã¢ãã¯ãªããžã§ã¯ããç°¡åã«äœæããããã®ã¡ã«ããºã ãæäŸãããã¹ããäœæãããšãã«å®éã®ãªããžã§ã¯ãã®ä»£ããã«äœ¿çšã§ããŸãã
圌ã®ã³ãŒãã¯æ¬¡ã®ãšããã§ãã
type MockedStorage struct{ mock.Mock } func (s *MockedStorage) CreateBin(_ *Bin) error { args := s.Mock.Called() return args.Error(0) } func (s *MockedStorage) UpdateBin(bin *Bin) error { args := s.Mock.Called(bin) return args.Error(0) } func (s *MockedStorage) LookupBin(name string) (*Bin, error) { args := s.Mock.Called(name) return args.Get(0).(*Bin), args.Error(1) } func (s *MockedStorage) LookupBins(names []string) ([]*Bin, error) { args := s.Mock.Called(names) return args.Get(0).([]*Bin), args.Error(1) } func (s *MockedStorage) LookupRequest(binName, id string) (*Request, error) { args := s.Mock.Called(binName, id) return args.Get(0).(*Request), args.Error(1) } func (s *MockedStorage) CreateRequest(bin *Bin, req *Request) error { args := s.Mock.Called(bin) return args.Error(0) } func (s *MockedStorage) LookupRequests(binName string, from, to int) ([]*Request, error) { args := s.Mock.Called(binName, from, to) return args.Get(0).([]*Request), args.Error(1) }
ããã«ãã¹ãèªäœã§ã¯ãAPIãäœæãããšãã«ãã¢ãã¯ãªããžã§ã¯ããæ³šå ¥ããŸãã
req, _ := http.NewRequest("GET", "/api/v1/bins/", nil) api = GetApi() mockedStorage := &MockedStorage{} api.MapTo(mockedStorage, (*Storage)(nil)) res = httptest.NewRecorder() mockedStorage.On("LookupBins", []string{}).Return([]*Bin(nil), errors.New("Storage error")) api.ServeHTTP(res, req) mockedStorage.AssertExpectations(t) if assert.Equal(t, res.Code, 500) { assert.Contains(t, res.Body.String(), "Storage error") }
ãã®ãã¹ãã§ã¯ãã¢ãã¯ãªããžã§ã¯ããžã®äºæ³ããããªã¯ãšã¹ããšããããã«å¿ èŠãªåçã説æããŸãã ãããã£ãŠããªããžã§ã¯ãã®ã¢ãã¯ã¡ãœããå ã§
s.Mock.Called(names)
ã¡ãœãããåŒã³åºããšãæå®ããããã©ã¡ãŒã¿ãŒãšã¡ãœããåã®å¯Ÿå¿ãèŠã€ããããšããargs.GetïŒ0ïŒãè¿ããšãReturnã«æž¡ãããæåã®åŒæ°ãè¿ãããŸãããã®å Žåã¯realBinã ã€ã³ã¿ãŒãã§ã€ã¹{}åã®ãªããžã§ã¯ããè¿ãGetã¡ãœããã«å ããŠãã€ã³ã¿ãŒãã§ã€ã¹ãå¿ èŠãªåã«å€æããIntãStringãBoolãErrorã®ãã«ããŒã¡ãœããããããŸãã mockedStorage.AssertExpectationsïŒtïŒã¡ãœããã¯ããã¹ãäžã«äºæ³ããããã¹ãŠã®ã¡ãœãããåŒã³åºããããã©ããã確èªããŸãã
httptest.NewRecorderã§äœæãããResponseRecorderãªããžã§ã¯ããèå³æ·±ããã®ã§ããResponseWriterã®åäœãå®è£ ããèŠæ±ããŒã¿ãã©ãã«ã衚瀺ããã«ãæçµçã«è¿ããããã®ïŒå¿çã³ãŒããããããŒãå¿çæ¬æïŒã確èªã§ããŸãã
ãã¹ããå®è¡ããã«ã¯ã次ã®ã³ãã³ããå®è¡ããå¿ èŠããããŸãã
> go test ./src/skimmer ok _/.../src/skimmer 0.032s
ãã¹ãéå§ããŒã ã«ã¯å€æ°ã®ãã©ã°ããããŸãã次ã®ããã«ãã©ã°ãç解ã§ããŸãã
> go help testflag
ãããã§éã¶ããšãã§ããŸããã次ã®ã³ãã³ãã«èå³ããããŸãïŒGoããŒãžã§ã³1.2ã«é¢é£ïŒïŒ
> go test ./src/skimmer/ -coverprofile=c.out && go tool cover -html=c.out
åäœããªãå Žåã¯ãæåã«ã«ãã¬ããžããŒã«ãã€ã³ã¹ããŒã«ããå¿ èŠããããŸã
> go get code.google.com/p/go.tools/cmd/cover
ãã®ã³ãã³ãã¯ãã¹ããå®è¡ãããã¹ãã«ãã¬ããžãããã¡ã€ã«ãc.outãã¡ã€ã«ã«ä¿åããŸãããã®åŸã
go tool
ã¯HTMLããŒãžã§ã³ãäœæãããã©ãŠã¶ãŒã§éããŸãã
Goã§ã®ãã¹ãã«ãã¬ããžãéåžžã«èå³æ·±ãå®è£ ã§ãã ã³ãŒããã³ã³ãã€ã«ããåã«ããœãŒã¹ãã¡ã€ã«ãå€æŽãããã«ãŠã³ã¿ãŒããœãŒã¹ã³ãŒãã«æ¿å ¥ãããŸãã ããšãã°ã次ã®ãããªã³ãŒãïŒ
func Size(a int) string { switch { case a < 0: return "negative" case a == 0: return "zero" } return "enormous" }
ããã«å€ãããŸãïŒ
func Size(a int) string { GoCover.Count[0] = 1 switch { case a < 0: GoCover.Count[2] = 1 return "negative" case a == 0: GoCover.Count[3] = 1 return "zero" } GoCover.Count[1] = 1 return "enormous" }
ã«ãã¬ããžã ãã§ãªããã³ãŒãã®åã»ã¯ã·ã§ã³ããã¹ããããåæ°ã衚瀺ããããšãã§ããŸãã ãã€ãã®ããã«ã ããã¥ã¡ã³ãã§è©³çŽ°ãèªãããšãã§ããŸã ã
æ¬æ ŒçãªREST APIãçšæããããã¹ãã§èŠãããã®ã§ãWebã€ã³ã¿ãŒãã§ãŒã¹ã®è£ 食ãšæ§ç¯ãéå§ã§ããŸãã
ã¹ããã5-è£ é£ŸãšWebã€ã³ã¿ãŒãã§ãŒã¹ã
Goããã±ãŒãžã«ã¯ã htmlãã³ãã¬ãŒããæäœããããã®å®å šãªã©ã€ãã©ãªããããŸãããjavascriptãä»ããŠAPIãšçŽæ¥é£æºãããããããåäžããŒãžã¢ããªã±ãŒã·ã§ã³ãäœæããŸãã ãã®AngularJSãæäŒã£ãŠãã ããã
æ°ããã¹ãããã®ã³ãŒããæŽæ°ããŸãã
> git checkout step-5
æåã®ç« ã§è¿°ã¹ãããã«ãMartiniã«ã¯éçãã¡ã€ã«ãé åžããããã®ãã³ãã©ããããããã©ã«ãã§ã¯ãããªãã¯ãã£ã¬ã¯ããªããéçãã¡ã€ã«ãé åžããŸãã ããã«å¿ èŠãªjsããã³cssã©ã€ãã©ãªãé 眮ããŸãã ããã¯ç§ãã¡ã®èšäºã®ç®æšã§ã¯ãªãã®ã§ãããã³ããšã³ãã®äœæ¥ã«ã€ããŠèª¬æããŸããè§åºŠã«ç²ŸéããŠãã人ã®ããã«ããœãŒã¹ãã¡ã€ã«ãèªåã§èŠãããšãã§ããŸãã
ã¡ã€ã³ããŒãžã衚瀺ããã«ã¯ãå¥ã®ãã³ãã©ãŒãè¿œå ããŸãã
api.Get("**", func(r render.Render){ r.HTML(200, "index", nil) })
Glob
**
æåã¯ãindex.htmlãã¡ã€ã«ããã¹ãŠã®ã¢ãã¬ã¹ã«å¯ŸããŠè¿ãããããšã瀺ããŸãã ãã³ãã¬ãŒããæ£ããæäœããããã«ããã³ãã¬ãŒãã®å ¥æå ã瀺ãã¬ã³ãã©ãŒãäœæãããšãã«ãªãã·ã§ã³ãè¿œå ããŸããã ããã«ãã¢ã³ã®ã¥ã©ãŒãã³ãã¬ãŒããšç«¶åããªãããã«ã{{}}ã{[{}]}ã«åå²ãåœãŠããŸãã
api.Use(render.Renderer(render.Options{ Directory: "public/static/views", Extensions: []string{".html"}, Delims: render.Delims{"{[{", "}]}"}, }))
ããã«ãè²ãã£ãŒã«ãïŒRGBè²å€ãæ ŒçŽãã3ãã€ãïŒãšãã¡ãã³ã³ïŒããŒã¿URIç»åãè²ãå¿ èŠïŒããã³ã¢ãã«ã«è¿œå ãããŸããããããã¯ããªããžã§ã¯ããäœæãããšãã«ã©ã³ãã ã«çæãããç°ãªããã³ãªããžã§ã¯ããè²ã§åºå¥ããŸãã
type Bin struct { ... Color [3]byte `json:"color"` Favicon string `json:"favicon"` } func NewBin() *Bin { color:= RandomColor() bin := Bin{ ... Color: color, Favicon: Solid16x16gifDatauri(color), } ... }
ããã§ãã»ãŒå®å šã«æ©èœããWebã¢ããªã±ãŒã·ã§ã³ãäœæãããŸãããå®è¡ã§ããŸãã
> go run ./src/main.go
ãããŠããã©ãŠã¶ã§éããŸãïŒ
127.0.0.1:3000
127.0.0.1:3000
ïŒéã³ãŸãã
æ®å¿µãªãããã¢ããªã±ãŒã·ã§ã³ã«ã¯ãŸã 2ã€ã®åé¡ããããŸããããã°ã©ã ãçµäºããåŸããã¹ãŠã®ããŒã¿ã倱ããããŠãŒã¶ãŒéã®åé¢ã¯ãããŸããã誰ããåãããšãèŠãŠããŸãã ããããã£ãŠã¿ãŸãããã
ã¹ããã6 ãã©ã€ãã·ãŒãè¿œå ããŸãã
6çªç®ã®ã¹ãããã®ã³ãŒããããŠã³ããŒãããŸãã
> git checkout step-6
ã»ãã·ã§ã³ã䜿çšããŠããŠãŒã¶ãŒãäºãã«åé¢ããŸãã éå§ããã«ã¯ãããããä¿åããå ŽæãéžæããŸãã martini-contribã®ã»ãã·ã§ã³ã¯ã ãŽãªã© Webã©ã€ãã©ãªã»ãã·ã§ã³ã«åºã¥ããŠããŸãã
Gorillaã¯ãWebãã¬ãŒã ã¯ãŒã¯ãå®è£ ããããã®ããŒã«ã®ã»ããã§ãã ãããã®ããŒã«ã¯ãã¹ãŠçžäºã«ççµåãããŠãããããããããéšåã«åå ããŠèªåã«çµã¿èŸŒãããšãã§ããŸãã
ããã«ããããŽãªã©ã«ãã§ã«å®è£ ãããŠãããªããžããªã䜿çšã§ããŸãã ç§ãã¡ã¯ã¯ãããŒããŒã¹ã§ãã
ã»ãã·ã§ã³ãªããžããªãäœæããŸãã
func GetApi(config *Config) *martini.ClassicMartini { ... store := sessions.NewCookieStore([]byte(config.SessionSecret)) ...
NewCookieStoreé¢æ°ã¯ãã©ã¡ãŒã¿ãŒãšããŠããŒãã¢ãåãå ¥ããŸãããã¢ã®æåã®ããŒã¯èªèšŒã«å¿ èŠã§ã2çªç®ã¯æå·åã«å¿ èŠã§ãã 2çªç®ã®ããŒã¯ã¹ãããã§ããŸãã ã»ãã·ã§ã³ã倱ãããšãªãããŒãããŒããŒã·ã§ã³ã§ããããã«ããã«ã¯ãè€æ°ã®ããŒãã¢ã䜿çšã§ããŸãã ã»ãã·ã§ã³ãäœæãããšããæåã®ãã¢ã®ããŒã䜿çšãããŸãããããŒã¿ããã§ãã¯ãããšãã¯ããã¹ãŠã®ããŒãæåã®ãã¢ããé çªã«äœ¿çšãããŸãã
ã¢ããªã±ãŒã·ã§ã³ã«ã¯ç°ãªãããŒãå¿ èŠãªã®ã§ããã®ãã©ã¡ãŒã¿ãŒãConfigãªããžã§ã¯ãã«é 眮ããŸããããã¯å°æ¥ãç°å¢èšå®ãŸãã¯èµ·åãã©ã°ã«åºã¥ããŠã¢ããªã±ãŒã·ã§ã³ãæ§æããã®ã«åœ¹ç«ã¡ãŸãã
ã»ãã·ã§ã³ã®åŠçãè¿œå ããäžéãã³ãã©ãŒãAPIã«è¿œå ããŸãã
// Sessions is a Middleware that maps a session.Session service into the Martini handler chain. // Sessions can use a number of storage solutions with the given store. func Sessions(name string, store Store) martini.Handler { return func(res http.ResponseWriter, r *http.Request, c martini.Context, l *log.Logger) { // Map to the Session interface s := &session{name, r, l, store, nil, false} c.MapTo(s, (*Session)(nil)) // Use before hook to save out the session rw := res.(martini.ResponseWriter) rw.Before(func(martini.ResponseWriter) { if s.Written() { check(s.Session().Save(r, res), l) } }) ... c.Next() } }
ã³ãŒããããããããã«ããªã¯ãšã¹ãããšã«ã»ãã·ã§ã³ãäœæããããªã¯ãšã¹ãã³ã³ããã¹ãã«è¿œå ãããŸãã ãªã¯ãšã¹ãã®æåŸã«ããããã¡ããã®ããŒã¿ãæžã蟌ãŸããçŽåã«ãã»ãã·ã§ã³ããŒã¿ãå€æŽãããŠããå Žåã¯ä¿åãããŸãã
次ã«ãhistory.goãã¡ã€ã«ã®å±¥æŽïŒããã¯åãªãã¹ã©ã€ã¹ã§ããïŒãæžãæããŸãã
type History interface { All() []string Add(string) } type SessionHistory struct { size int name string session sessions.Session data []string } func (history *SessionHistory) All() []string { if history.data == nil { history.load() } return history.data } func (history *SessionHistory) Add(name string) { if history.data == nil { history.load() } history.data = append(history.data, "") copy(history.data[1:], history.data) history.data[0] = name history.save() } func (history *SessionHistory) save() { size := history.size if size > len(history.data){ size = len(history.data) } history.session.Set(history.name, history.data[:size]) } func (history *SessionHistory) load() { sessionValue := history.session.Get(history.name) history.data = []string{} if sessionValue != nil { if values, ok := sessionValue.([]string); ok { history.data = append(history.data, values...) } } } func NewSessionHistoryHandler(size int, name string) martini.Handler { return func(c martini.Context, session sessions.Session) { history := &SessionHistory{size: size, name: name, session: session} c.MapTo(history, (*History)(nil)) } }
NewSessionHistoryHandlerã¡ãœããã§ã¯ãHistoryã€ã³ã¿ãŒãã§ã€ã¹ïŒãã¹ãŠã®å±¥æŽãªããžã§ã¯ãã®è¿œå ãšã¯ãšãªã«ã€ããŠèª¬æããŸãïŒãå®è£ ããSessionHistoryãªããžã§ã¯ããäœæãããããåãªã¯ãšã¹ãã®ã³ã³ããã¹ãã«è¿œå ããŸãã SessionHistoryãªããžã§ã¯ãã«ã¯ãããŒã¿ãã»ãã·ã§ã³ã«ããŒãããã³ä¿åãããã«ããŒã¡ãœããã®ããŒãããã³ä¿åããããŸãã ããã«ãã»ãã·ã§ã³ããã®ããŒã¿ã®ããŠã³ããŒãã¯ããªã³ããã³ãã§ã®ã¿å®è¡ãããŸãã ããã§ã以åã«å±¥æŽã¹ã©ã€ã¹ã䜿çšãããŠãããã¹ãŠã®APIã¡ãœããã§ãHistoryã¿ã€ãã®æ°ãããªããžã§ã¯ãã䜿çšãããŸãã
ãã®æç¹ãããåãŠãŒã¶ãŒã¯Binãªããžã§ã¯ãã®ç¬èªã®å±¥æŽãä¿æããŸãããçŽæ¥ãªã³ã¯ãä»ããŠãã¹ãŠã®Binã衚瀺ã§ããŸãã ãããä¿®æ£ããã«ã¯ããã©ã€ããŒãBinãªããžã§ã¯ããäœæããæ©èœãè¿œå ããŸãã
Binã«2ã€ã®æ°ãããã£ãŒã«ããäœæããŸãããã
type Bin struct { ... Private bool `json:"private"` SecretKey string `json:"-"` }
ããŒã¯SecretKeyãã£ãŒã«ãã«ä¿åããããã©ã€ããŒãBinïŒPrivateãã©ã°ãtrueã«èšå®ãããŠãããã®ïŒãžã®ã¢ã¯ã»ã¹ãèš±å¯ããŸãã ãªããžã§ã¯ãããã©ã€ããŒãã«ããã¡ãœãããè¿œå ããŸãã
func (bin *Bin) SetPrivate() { bin.Private = true bin.SecretKey = rs.Generate(32) }
ãã©ã€ããŒãBinãäœæããããã«ãããã³ããšã³ãã¯ãªããžã§ã¯ããäœæãããšãã«ããã©ã€ããŒããã©ã°ä»ãã®jsonãªããžã§ã¯ããéä¿¡ããŸãã çä¿¡jsonã解æããããã«ããªã¯ãšã¹ãæ¬æãèªã¿åããå¿ èŠãªæ§é ã«ã¢ã³ããã¯ããå°ããªDecodeJsonPayloadã¡ãœãããäœæããŸããã
func DecodeJsonPayload(r *http.Request, v interface{}) error { content, err := ioutil.ReadAll(r.Body) r.Body.Close() if err != nil { return err } err = json.Unmarshal(content, v) if err != nil { return err } return nil }
APIãå€æŽããŠãæ°ããåäœãå®è£ ããŸãã
api.Post("/api/v1/bins/", func(r render.Render, storage Storage, history History, session sessions.Session, req *http.Request){ payload := Bin{} if err := DecodeJsonPayload(req, &payload); err != nil { r.JSON(400, ErrorMsg{fmt.Sprintf("Decoding payload error: %s", err)}) return } bin := NewBin() if payload.Private { bin.SetPrivate() } if err := storage.CreateBin(bin); err == nil { history.Add(bin.Name) if bin.Private { session.Set(fmt.Sprintf("pr_%s", bin.Name), bin.SecretKey) } r.JSON(http.StatusCreated, bin) } else { r.JSON(http.StatusInternalServerError, ErrorMsg{err.Error()}) } })
æåã«ãã¿ã€ãBinã®ãã€ããŒããªããžã§ã¯ããäœæããŸãããã®ãã£ãŒã«ãã«ã¯ããªã¯ãšã¹ãæ¬æããDecodeJsonPayloadé¢æ°ã®å€ãå ¥åãããŸãã ãã®åŸãå ¥åã§ãªãã·ã§ã³ãprivateããèšå®ãããŠããå Žåããã³ããã©ã€ããŒãã«ããŸãã ããã«ããã©ã€ããŒããªããžã§ã¯ãã®å Žåãã»ãã·ã§ã³
session.Set(fmt.Sprintf("pr_%s", bin.Name), bin.SecretKey)
ããŒå€ãä¿åããŸãã ããã§ãä»ã®APIã¡ãœãããå€æŽããŠããã©ã€ããŒãBinãªããžã§ã¯ãã®ã»ãã·ã§ã³å ã®ããŒã®ååšããã§ãã¯ããå¿ èŠããããŸãã
ããã¯æ¬¡ã®ããã«è¡ãããŸãã
api.Get("/api/v1/bins/:bin", func(r render.Render, params martini.Params, session sessions.Session, storage Storage){ if bin, err := storage.LookupBin(params["bin"]); err == nil{ if bin.Private && bin.SecretKey != session.Get(fmt.Sprintf("pr_%s", bin.Name)){ r.JSON(http.StatusForbidden, ErrorMsg{"The bin is private"}) } else { r.JSON(http.StatusOK, bin) } } else { r.JSON(http.StatusNotFound, ErrorMsg{err.Error()}) } })
é¡æšã«ãããä»ã®æ¹æ³ã§è¡ãããŸãã äžéšã®ãã¹ãã¯ãæ°ããåäœãèæ ®ããŠä¿®æ£ãããç¹å®ã®å€æŽãã³ãŒãã§è¡šç€ºã§ããŸãã
ã¢ããªã±ãŒã·ã§ã³ãå¥ã®ãã©ãŠã¶ãŸãã¯ã·ãŒã¯ã¬ããã¢ãŒãã§å®è¡ããå Žåãå±¥æŽãç°ãªãããšãããã³äœæããããã©ãŠã¶ã®ã¿ããã©ã€ããŒãBinãªããžã§ã¯ãã«ã¢ã¯ã»ã¹ã§ããããšã確èªã§ããŸãã
ãã¹ãŠã¯åé¡ãããŸããããä»ã§ã¯ã¹ãã¬ãŒãžå ã®ãã¹ãŠã®ãªããžã§ã¯ãã¯ã»ãŒæ°žä¹ ã«åç¶ããŸããããã¯ããããæ°žé ã§ã¯ãªããããããããæ£ãããšã¯èšããŸããã
7çªç®ã®ã¹ãããã äžèŠãªãã®ããããã«ããŸãã
7çªç®ã®ã¹ãããã³ãŒããããŠã³ããŒãããŸãã
git checkout step-7
ããŒã¹ã¹ãã¬ãŒãžæ§é ã«å¥ã®ãã£ãŒã«ããè¿œå ããŸãã
type BaseStorage struct { ... binLifetime int64 }
Binãªããžã§ã¯ããšé¢é£ã¯ãšãªã®æ倧æå¹æéãä¿åããŸãã ããã§ãã¡ã¢ãªå ã®ã¹ãã¬ãŒãžãæžãæããŸã-memory.goã binLifetimeç§ä»¥äžæŽæ°ãããŠããªããã¹ãŠã®binRecordsãã¯ãªã¢ããã¡ã€ã³ã¡ãœããïŒ
func (storage *MemoryStorage) clean() { storage.Lock() defer storage.Unlock() now := time.Now().Unix() for name, binRecord := range storage.binRecords { if binRecord.bin.Updated < (now - storage.binLifetime) { delete(storage.binRecords, name) } } }
ãŸããã¿ã€ããŒãšãããæäœããã¡ãœãããMemoryStorageã¿ã€ãã«è¿œå ããŸãã
type MemoryStorage struct { ... cleanTimer *time.Timer } func (storage *MemoryStorage) StartCleaning(timeout int) { defer func(){ storage.cleanTimer = time.AfterFunc(time.Duration(timeout) * time.Second, func(){storage.StartCleaning(timeout)}) }() storage.clean() } func (storage *MemoryStorage) StopCleaning() { if storage.cleanTimer != nil { storage.cleanTimer.Stop() } }
ããã±ãŒãžã¡ãœããtime AfterFuncã¯ãtime.Durationãªã©ã®ã¿ã€ã ã¢ãŠããæåã®åŒæ°ã§æž¡ãããåŸãå¥ã®ãŽã«ãŒãã³ã§æå®ãããé¢æ°ãéå§ããŸãïŒãããã£ãŠããã©ã¡ãŒã¿ãŒãªãã§ããå¿ èŠããããããããã§ã¯ããŒãžã£ãŒã䜿çšããŠã¿ã€ã ã¢ãŠããæž¡ããŸãïŒã
ã¢ããªã±ãŒã·ã§ã³ã®æ°Žå¹³ã¹ã±ãŒãªã³ã°ãè¡ãã«ã¯ãç°ãªããµãŒããŒã§å®è¡ããå¿ èŠããããããããŒã¿çšã«å¥ã®ã¹ãã¬ãŒãžãå¿ èŠã§ãã RedisãäŸã«ãšããŸãã
ã¹ããã8ã ã¹ãã¬ãŒãžã«ã¯Redisã䜿çšããŠããŸãã
Redisã®å ¬åŒããã¥ã¡ã³ãã§ã¯ ãGoã®åºç¯ãªã¯ã©ã€ã¢ã³ãã®ãªã¹ãã«ã€ããŠã¢ããã€ã¹ããŠããŸãã å·çæç¹ã§ã¯ãæšå¥šãããã®ã¯radixãšredigoã§ãã redigoã¯ç©æ¥µçã«éçºãããŠããããã倧ããªã³ãã¥ããã£ããããããredigoãéžæããŸãã
ç®çã®ã³ãŒãã«é²ã¿ãŸãããã
git checkout step-8
redis.goãã¡ã€ã«ãèŠãŠãã ããããããStorage for Redisã®å®è£ ã«ãªããŸãã åºæ¬çãªæ§é ã¯éåžžã«åçŽã§ãã
type RedisStorage struct { BaseStorage pool *redis.Pool prefix string cleanTimer *time.Timer }
ããŒã«ã«ã¯ãå€§æ ¹ãžã®æ¥ç¶ã®ããŒã«ãprefix-ãã¹ãŠã®ããŒã®å ±éãã¬ãã£ãã¯ã¹ã«æ ŒçŽãããŸãã ããŒã«ãäœæããã«ã¯ãredigoã®äŸã®ã³ãŒãã䜿çšããŸãã
func getPool(server string, password string) (pool *redis.Pool) { pool = &redis.Pool{ MaxIdle: 3, IdleTimeout: 240 * time.Second, Dial: func() (redis.Conn, error) { c, err := redis.Dial("tcp", server) if err != nil { return nil, err } if password != "" { if _, err := c.Do("AUTH", password); err != nil { c.Close() return nil, err } } return c, err }, TestOnBorrow: func(c redis.Conn, _ time.Time) error { _, err := c.Do("PING") return err }, } return pool }
Dialã§ã¯ãRedisãµãŒããŒã«æ¥ç¶ããåŸããã¹ã¯ãŒããæå®ãããŠããå Žåã«ãã°ã€ã³ãè©Šã¿ãé¢æ°ãæž¡ããŸãã ãã®åŸã確ç«ãããæ¥ç¶ãè¿ãããŸãã TestOnBorrowé¢æ°ã¯ãããŒã«ããæ¥ç¶ãèŠæ±ããããšãã«åŒã³åºãããŸãããã®æ©èœã§ã¯ãæ¥ç¶ã®å®è¡å¯èœæ§ã確èªã§ããŸãã 2çªç®ã®ãã©ã¡ãŒã¿ãŒã¯ãæ¥ç¶ãããŒã«ã«è¿ãããŠããã®æéã§ãã æ¯åpingãéä¿¡ããã ãã§ãã
ãŸããããã±ãŒãžã§ã¯ãããã€ãã®å®æ°ã宣èšããŠããŸãã
const ( KEY_SEPARATOR = "|" // BIN_KEY = "bins" // Bin REQUESTS_KEY = "rq" // REQUEST_HASH_KEY = "rhsh" // CLEANING_SET = "cln" // , Bin CLEANING_FACTOR = 3 // )
ãã®ãã¿ãŒã³ã«åŸã£ãŠããŒãååŸããŸãã
func (storage *RedisStorage) getKey(keys ...string) string { return fmt.Sprintf("%s%s%s", storage.prefix, KEY_SEPARATOR, strings.Join(keys, KEY_SEPARATOR)) }
å€§æ ¹ã«ããŒã¿ãä¿åããã«ã¯ãäœãã§ã·ãªã¢ã«åããå¿ èŠããããŸãã 人æ°ã®ããmsgpack圢åŒãéžæãã人æ°ã®ããã³ãŒããã¯ã©ã€ãã©ãªã䜿çšããŸãã
å¯èœãªãã¹ãŠããã€ããªããŒã¿ã«ã·ãªã¢ã«åãããã®éãè¡ãã¡ãœããã«ã€ããŠèª¬æããŸãã
func (storage *RedisStorage) Dump(v interface{}) (data []byte, err error) { var ( mh codec.MsgpackHandle h = &mh ) err = codec.NewEncoderBytes(&data, h).Encode(v) return } func (storage *RedisStorage) Load(data []byte, v interface{}) error { var ( mh codec.MsgpackHandle h = &mh ) return codec.NewDecoderBytes(data, h).Decode(v) }
次ã«ãä»ã®æ¹æ³ã«ã€ããŠèª¬æããŸãã
Binãªããžã§ã¯ãã®äœæ
func (storage *RedisStorage) UpdateBin(bin *Bin) (err error) { dumpedBin, err := storage.Dump(bin) if err != nil { return } conn := storage.pool.Get() defer conn.Close() key := storage.getKey(BIN_KEY, bin.Name) conn.Send("SET", key, dumpedBin) conn.Send("EXPIRE", key, storage.binLifetime) conn.Flush() return err } func (storage *RedisStorage) CreateBin(bin *Bin) error { if err := storage.UpdateBin(bin); err != nil { return err } return nil }
æåã«ãDumpã¡ãœããã䜿çšããŠãã³ãã·ãªã¢ã«åããŸãã 次ã«ãå€§æ ¹ååç©ãããŒã«ããåãåºããŸãïŒdeferã䜿çšããŠè¿ãå¿ èŠãããããšãå¿ããã«ïŒã
Redigoã¯ãã€ãã©ã€ã³ã¢ãŒãããµããŒãããŠããŸããSendã¡ãœããã䜿çšããŠãããã¡ãŒã«ã³ãã³ããè¿œå ããFlushã¡ãœããã䜿çšããŠãããã¡ãŒãããã¹ãŠã®ããŒã¿ãéä¿¡ããReceiveã§çµæãååŸã§ããŸãã Doã³ãã³ãã¯ã3ã€ãã¹ãŠã®ããŒã ã1ã€ã«çµåããŸãã redigoã®ããã¥ã¡ã³ãã§ãã©ã³ã¶ã¯ã·ã§ã³æ§ãå®è£ ããããšãã§ããŸã ã
Binã®ããŒã¿ãååã§ä¿åãããSETããšããã®ã¬ã³ãŒãã®æå¹æéãèšå®ããExpireãšãã2ã€ã®ã³ãã³ããéä¿¡ããŸãã
Binãªããžã§ã¯ãã®ååŸ
func (storage *RedisStorage) LookupBin(name string) (bin *Bin, err error) { conn := storage.pool.Get() defer conn.Close() reply, err := redis.Bytes(conn.Do("GET", storage.getKey(BIN_KEY, name))) if err != nil { if err == redis.ErrNil { err = errors.New("Bin was not found") } return } err = storage.Load(reply, &bin) return }
ãã«ããŒã¡ãœããredis.Bytesã¯ãconn.Doããã®å¿çããã€ãé åã«èªã¿åãããšããŸãã ãªããžã§ã¯ããèŠã€ãããªãã£ãå Žåãå€§æ ¹ã¯ç¹å¥ãªãšã©ãŒã¿ã€ãredis.ErrNilãè¿ããŸãã ãã¹ãŠãããŸããã£ãå ŽåãããŒã¿ã¯binãªããžã§ã¯ãã«ããŒããããåç §ã«ãã£ãŠLoadã¡ãœããã«æž¡ãããŸãã
Binãªããžã§ã¯ãã®ãªã¹ãã®ååŸ
func (storage *RedisStorage) LookupBins(names []string) ([]*Bin, error) { bins := []*Bin{} if len(names) == 0 { return bins, nil } args := redis.Args{} for _, name := range names { args = args.Add(storage.getKey(BIN_KEY, name)) } conn := storage.pool.Get() defer conn.Close() if values, err := redis.Values(conn.Do("MGET", args...)); err == nil { bytes := [][]byte{} if err = redis.ScanSlice(values, &bytes); err != nil { return nil, err } for _, rawbin := range bytes { if len(rawbin) > 0 { bin := &Bin{} if err := storage.Load(rawbin, bin); err == nil { bins = append(bins, bin) } } } return bins, nil } else { return nil, err } }
ããã§ã¯ãMGETã³ãã³ãã䜿çšããŠããŒã¿ã¹ã©ã€ã¹ãååŸããredis.ScanSliceãã«ããŒã¡ãœããã䜿çšããŠå¿çãç®çã®ã¿ã€ãã®ã¹ã©ã€ã¹ã«ããŒãããããšãé€ããŠãã»ãšãã©ãã¹ãŠãåã®ã¡ãœãããšåãã§ãã
ãªã¯ãšã¹ããäœæãããªã¯ãšã¹ã
func (storage *RedisStorage) CreateRequest(bin *Bin, req *Request) (err error) { data, err := storage.Dump(req) if err != nil { return } conn := storage.pool.Get() defer conn.Close() key := storage.getKey(REQUESTS_KEY, bin.Name) conn.Send("LPUSH", key, req.Id) conn.Send("EXPIRE", key, storage.binLifetime) key = storage.getKey(REQUEST_HASH_KEY, bin.Name) conn.Send("HSET", key, req.Id, data) conn.Send("EXPIRE", key, storage.binLifetime) conn.Flush() requestCount, err := redis.Int(conn.Receive()) if err != nil { return } if requestCount < storage.maxRequests { bin.RequestCount = requestCount } else { bin.RequestCount = storage.maxRequests } bin.Updated = time.Now().Unix() if requestCount > storage.maxRequests * CLEANING_FACTOR { conn.Do("SADD", storage.getKey(CLEANING_SET), bin.Name) } if err = storage.UpdateBin(bin); err != nil { return } return }
ãŸããbin.Nameã®ãªã¯ãšã¹ããªã¹ãã«ãªã¯ãšã¹ãèå¥åãä¿åãã次ã«ããã·ã¥ããŒãã«ã«ã·ãªã¢ã«åããããªã¯ãšã¹ããä¿åããŸããã©ã¡ãã®å Žåããã©ã€ãã¿ã€ã ãè¿œå ããããšãå¿ããªãã§ãã ãããLPUSHã³ãã³ãã¯ãrequestCountãªã¹ãå ã®ãšã³ããªã®æ°ãè¿ããŸãããã®æ°ãæ倧å€ã«ä¿æ°ãæããå€ãè¶ ããå Žåããã®Binã次ã®ã¯ãªãŒã³ã¢ããã®åè£ã«è¿œå ããŸãã
ãªã¯ãšã¹ããšãªã¯ãšã¹ãã®ãªã¹ãã®åä¿¡ã¯ãBinãªããžã§ã¯ããšåæ§ã«è¡ãããŸãã
ã¯ãªãŒãã³ã°
func (storage *RedisStorage) clean() { for { conn := storage.pool.Get() defer conn.Close() binName, err := redis.String(conn.Do("SPOP", storage.getKey(CLEANING_SET))) if err != nil { break } conn.Send("LRANGE", storage.getKey(REQUESTS_KEY, binName), storage.maxRequests, -1) conn.Send("LTRIM", storage.getKey(REQUESTS_KEY, binName), 0, storage.maxRequests-1) conn.Flush() if values, error := redis.Values(conn.Receive()); error == nil { ids := []string{} if err := redis.ScanSlice(values, &ids); err != nil { continue } if len(ids) > 0 { args := redis.Args{}.Add(storage.getKey(REQUEST_HASH_KEY, binName)).AddFlat(ids) conn.Do("HDEL", args...) } } } }
MemoryStorageãšã¯ç°ãªããããã§ã¯åé·ãªèŠæ±ãã¯ãªã¢ããŸããããã¯ãæå¹æéãEXPIRE radishã³ãã³ãã«ãã£ãŠå¶éãããŠããããã§ãããŸããã¯ãªãŒãã³ã°ã®ããã«ãªã¹ãããã¢ã€ãã ãåãåºããå¶éã«å«ãŸããŠããªããªã¯ãšã¹ãã®èå¥åãèŠæ±ããLTRIMã³ãã³ãã䜿çšããŠãªã¹ããå¿ èŠãªãµã€ãºã«å§çž®ããŸããäžåºŠã«è€æ°ã®ããŒãåãå ¥ããHDELã³ãã³ãã䜿çšããŠãããã·ã¥ããŒãã«ãã以åã«ååŸããèå¥åãåé€ããŸãã
redis_test.goãã¡ã€ã«ã§RedisStorageã®èª¬æãçµäºããŸãããåããã¹ããèŠã€ãããŸãã
次ã«ãã¢ããªã±ãŒã·ã§ã³ãèµ·åãããšãã«ãapi.goãã¡ã€ã«ã«ãªããžããªãéžæããæ©èœãè¿œå ããŸãããã
type RedisConfig struct { RedisAddr string RedisPassword string RedisPrefix string } type Config struct { ... Storage string RedisConfig } func GetApi(config *Config) *martini.ClassicMartini { var storage Storage switch config.Storage{ case "redis": redisStorage := NewRedisStorage(config.RedisAddr, config.RedisPassword, config.RedisPassword, MAX_REQUEST_COUNT, BIN_LIFETIME) redisStorage.StartCleaning(60) storage = redisStorage default: memoryStorage := NewMemoryStorage(MAX_REQUEST_COUNT, BIN_LIFETIME) memoryStorage.StartCleaning(60) storage = memoryStorage } ...
æ°ããStorageãã£ãŒã«ããæ§ææ§é ã«è¿œå ããããã«å¿ããŠãRedisStorageãŸãã¯MemoryStorageãåæåããŸããããŸããç¹å®ã®å€§æ ¹ãªãã·ã§ã³çšã«RedisConfigæ§æãè¿œå ãããŸããã
ãŸããèµ·åäžã®main.goãã¡ã€ã«ã«å€æŽãå ããŸãã
import ( "skimmer" "flag" ) var ( config = skimmer.Config{ SessionSecret: "secret123", RedisConfig: skimmer.RedisConfig{ RedisAddr: "127.0.0.1:6379", RedisPassword: "", RedisPrefix: "skimmer", }, } ) func init() { flag.StringVar(&config.Storage, "storage", "memory", "available storages: redis, memory") flag.StringVar(&config.SessionSecret, "sessionSecret", config.SessionSecret, "") flag.StringVar(&config.RedisAddr, "redisAddr", config.RedisAddr, "redis storage only") flag.StringVar(&config.RedisPassword, "redisPassword", config.RedisPassword, "redis storage only") flag.StringVar(&config.RedisPrefix, "redisPrefix", config.RedisPrefix, "redis storage only") } func main() { flag.Parse() api := skimmer.GetApi(&config) api.Run() }
flagããã±ãŒãžã䜿çšããŸããããã«ãããããã°ã©ã ã®èµ·åãªãã·ã§ã³ãç°¡åãã€ç°¡åã«è¿œå ã§ããŸãããã¹ãã¬ãŒãžããã©ã°ãinité¢æ°ã«è¿œå ããŸããããã«ããããã¹ãã¬ãŒãžããã£ãŒã«ãã®æ§æã«å€ãçŽæ¥ä¿åãããŸãããŸããå€§æ ¹ã®èµ·åãªãã·ã§ã³ãè¿œå ããŸãã
inité¢æ°ã¯Goå°çšã§ãããããã±ãŒãžãããŒãããããšåžžã«å®è¡ãããŸããGoã§ã®ããã°ã©ã ã®å®è¡ã®è©³çŽ°ã
ããã§ã-helpãªãã·ã§ã³ã䜿çšããŠããã°ã©ã ãèµ·åãããšã䜿çšå¯èœãªãªãã·ã§ã³ã®ãªã¹ãã衚瀺ãããŸãã
> go run ./src/main.go --help Usage of .../main: -redisAddr="127.0.0.1:6379": redis storage only -redisPassword="": redis storage only -redisPrefix="skimmer": redis storage only -sessionSecret="secret123": -storage="memory": available storages: redis, memory
ããã§ããŸã éåžžã«æªå å·¥ã§æé©åãããŠããªãã¢ããªã±ãŒã·ã§ã³ãã§ããŸãããããµãŒããŒäžã§åäœããŠå®è¡ããæºåãã§ããŸããã
3çªç®ã®éšåã§ã¯ãGAEãã³ã«ã€ã³ãããã³Herokuã§ã®ã¢ããªã±ãŒã·ã§ã³ã®ã¬ã€ã¢ãŠããšèµ·åãããã³ãã¹ãŠã®ãªãœãŒã¹ãå«ãåäžã®å®è¡å¯èœãã¡ã€ã«ãšããŠé åžããæ¹æ³ã«ã€ããŠèª¬æããŸããæé©åãè¡ããªããããã©ãŒãã³ã¹ãã¹ããäœæããŸãããªã¯ãšã¹ãããããã·ããå¿ èŠãªããŒã¿ã§å¿çããæ¹æ³ãåŠã³ãŸããæåŸã«ãã¢ããªã±ãŒã·ã§ã³å ã«åæ£ã°ã«ãŒããã£ãã·ã¥ããŒã¿ããŒã¹ãåã蟌ã¿ãŸãã
ãã®èšäºã®ä¿®æ£ãææ¡ãæè¿ããŸãã