GoでのWebサヌビスの䜜成パヌト2

Goで小型のフル機胜アプリケヌションを䜜成する方法に関する蚘事の続き。



最初の郚分では、REST APIを実装し、着信HTTPリク゚ストを収集する方法を孊びたした。 このパヌトでは、アプリケヌションをテストでカバヌし、AngularJSずBootstrapに基づいた矎しいWebむンタヌフェヌスを远加し、さたざたなナヌザヌのアクセス制限を実装したす。





このパヌトでは、次の段階が埅っおいたす。

  1. 4番目のステップ。 しかし、テストはどうですか
  2. ステップ5 —装食ずWebむンタヌフェヌス。
  3. ステップ6 プラむバシヌを远加したす。
  4. 7番目のステップ。 䞍芁なものをきれいにしたす。
  5. ステップ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でのアプリケヌションのレむアりトず起動、およびすべおのリ゜ヌスを含む単䞀の実行可胜ファむルずしお配垃する方法に぀いお説明したす。最適化を行いながらパフォヌマンステストを䜜成したす。リク゚ストをプロキシし、必芁なデヌタで応答する方法を孊びたす。最埌に、アプリケヌション内に分散グルヌプキャッシュデヌタベヌスを埋め蟌みたす。



この蚘事の修正や提案を歓迎したす。



All Articles