├── .gitignore ├── 1752_go_prg_blueprints_cvr_w_r.png ├── README.md ├── appendixB ├── backup │ ├── archiver.go │ ├── cmds │ │ ├── backup │ │ │ ├── backup │ │ │ ├── backupdata │ │ │ │ └── .keepme │ │ │ └── main.go │ │ └── backupd │ │ │ └── main.go │ ├── dirhash.go │ └── monitor.go ├── chat │ ├── auth.go │ ├── avatar.go │ ├── avatar_test.go │ ├── avatars │ │ └── .keepme │ ├── client.go │ ├── main.go │ ├── message.go │ ├── room.go │ ├── templates │ │ ├── chat.html │ │ ├── login.html │ │ └── upload.html │ └── upload.go ├── socialpoll │ ├── counter │ │ └── main.go │ └── twittervotes │ │ ├── main.go │ │ └── twitter.go └── trace │ ├── tracer.go │ └── tracer_test.go ├── chapter1 ├── chat │ ├── client.go │ ├── main.go │ ├── room.go │ └── templates │ │ └── chat.html └── trace │ ├── tracer.go │ └── tracer_test.go ├── chapter2 ├── chat │ ├── auth.go │ ├── client.go │ ├── main.go │ ├── message.go │ ├── room.go │ └── templates │ │ ├── chat.html │ │ └── login.html └── trace │ ├── tracer.go │ └── tracer_test.go ├── chapter3 ├── chat │ ├── auth.go │ ├── avatar.go │ ├── avatar_test.go │ ├── avatars │ │ └── .keepme │ ├── client.go │ ├── main.go │ ├── message.go │ ├── room.go │ ├── templates │ │ ├── chat.html │ │ ├── login.html │ │ └── upload.html │ └── upload.go └── trace │ ├── tracer.go │ └── tracer_test.go ├── chapter4 ├── available │ └── main.go ├── coolify │ └── main.go ├── domainfinder │ ├── build.sh │ ├── lib │ │ └── .keepme │ └── main.go ├── domainify │ └── main.go ├── sprinkle │ └── main.go ├── synonyms │ └── main.go └── thesaurus │ ├── bighuge.go │ └── thesaurus.go ├── chapter5 └── socialpoll │ ├── counter │ └── main.go │ └── twittervotes │ ├── main.go │ └── twitter.go ├── chapter6 └── socialpoll │ ├── api │ ├── main.go │ ├── path.go │ ├── polls.go │ ├── respond.go │ └── vars.go │ ├── counter │ └── main.go │ ├── twittervotes │ ├── main.go │ └── twitter.go │ └── web │ ├── main.go │ └── public │ ├── index.html │ ├── new.html │ └── view.html ├── chapter7 └── meander │ ├── cmd │ └── main.go │ ├── cost_level.go │ ├── cost_level_test.go │ ├── journeys.go │ ├── public.go │ └── query.go └── chapter8 └── backup ├── archiver.go ├── cmds ├── backup │ ├── backupdata │ │ └── .keepme │ └── main.go └── backupd │ └── main.go ├── dirhash.go └── monitor.go /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store -------------------------------------------------------------------------------- /1752_go_prg_blueprints_cvr_w_r.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oreilly-japan/go-programming-blueprints/fc204afa474490efb10b7ad8d1928b66848f02e2/1752_go_prg_blueprints_cvr_w_r.png -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Go言語によるWebアプリケーション開発 2 | 3 | --- 4 | 5 | ![表紙](1752_go_prg_blueprints_cvr_w_r.png) 6 | 7 | --- 8 | 9 | 本リポジトリはオライリー・ジャパン発行書籍『[Go言語によるWebアプリケーション開発](http://www.oreilly.co.jp/books/9784873117522/)』(原書名『[Go Programming Blueprints](https://www.packtpub.com/application-development/go-programming-blueprints)』) のサポートサイトです。 10 | 11 | ## サンプルコード 12 | 13 | サンプルコードの解説は本書籍をご覧ください。 14 | 15 | [version1ブランチ](../../tree/version1)の[変更履歴](../../commits/version1)で本書籍の途中のコードを参照することができます。 16 | 17 | 原書のサンプルコードは[github.com/matryer/goblueprints](https://github.com/matryer/goblueprints)でアクセスできます。 18 | 19 | ## 正誤表 20 | 21 | 下記のとおり、本書に誤りがありました。お詫びして訂正いたします。 22 | 23 | 誤植など間違いを見つけた方は、japan@oreilly.co.jpまでお知らせください。 24 | 25 | ### 第3刷まで 26 | 27 | #### ■p.106 12行目 28 | **誤** 29 | 30 | ``` 31 | log.Fatalf("%qに類語はありませんでした\n") 32 | ``` 33 | 34 | **正** 35 | 36 | ``` 37 | log.Fatalf("%qに類語はありませんでした\n", word) 38 | ``` 39 | 40 | #### ■p.186 29行目 41 | **誤** 42 | 43 | ``` 44 | package meander 45 | type Place struct { 46 | *googleGeometry `json:"geometry"` 47 | Name string `json:"name"` 48 | Icon string `json:"icon"` 49 | Photos []*googlePhoto `json:"photos"` 50 | Vicinity string `json:"vicinity"` 51 | } 52 | type googleResponse struct { 53 | Results []*Place `json:"results"` 54 | } 55 | type googleGeometry struct { 56 | *googleLocation `json:"location"` 57 | } 58 | type googleLocation struct { 59 | Lat float64 `json:"lat"` 60 | Lng float64 `json:"lng"` 61 | } 62 | type googlePhoto struct { 63 | PhotoRef string `json:"photo_reference"` 64 | URL string `json:"url"` 65 | } 66 | ``` 67 | 68 | **正** 69 | 70 | ``` 71 | package meander 72 | type Place struct { 73 | Geometry *googleGeometry `json:"geometry"` 74 | Name string `json:"name"` 75 | Icon string `json:"icon"` 76 | Photos []*googlePhoto `json:"photos"` 77 | Vicinity string `json:"vicinity"` 78 | } 79 | type googleResponse struct { 80 | Results []*Place `json:"results"` 81 | } 82 | type googleGeometry struct { 83 | Location *googleLocation `json:"location"` 84 | } 85 | type googleLocation struct { 86 | Lat float64 `json:"lat"` 87 | Lng float64 `json:"lng"` 88 | } 89 | type googlePhoto struct { 90 | PhotoRef string `json:"photo_reference"` 91 | URL string `json:"url"` 92 | } 93 | 94 | ``` 95 | 96 | #### ■p.187 31行目 97 | **誤** 98 | 99 | ``` 100 | "lat": p.Lat, 101 | "lng": p.Lng, 102 | ``` 103 | 104 | **正** 105 | 106 | ``` 107 | "lat": p.Geometry.Location.Lat, 108 | "lng": p.Geometry.Location.Lng, 109 | ``` 110 | 111 | ### 第2刷まで 112 | 113 | #### ■p.36, 41, 46, 49, 50, 53, 61, 63, 77, 88のコマンド 114 | 115 | **誤** 116 | 117 | ``` 118 | ./chat -host=":8080" 119 | ``` 120 | 121 | **正** 122 | 123 | ``` 124 | ./chat -addr=":8080" 125 | ``` 126 | 127 | #### ■p.24 3行目 128 | 129 | **誤** 130 | 131 | ``` 132 | 必要な最小限のコードをtrace.goに追加します。 133 | ``` 134 | 135 | **正** 136 | 137 | ``` 138 | 必要な最小限のコードをtracer.goに追加します。 139 | ``` 140 | 141 | #### ■p.73 2行目 142 | 143 | **誤** 144 | 145 | ``` 146 | io.WriteString(m, strings.ToLower(user.Name())) 147 | ``` 148 | 149 | **正** 150 | 151 | ``` 152 | io.WriteString(m, strings.ToLower(user.Email())) 153 | ``` 154 | 155 | #### ■p.86 13行目 156 | 157 | **誤** 158 | 159 | ``` 160 | io.WriteString(m, strings.ToLower(user.Name())) 161 | ``` 162 | 163 | **正** 164 | 165 | ``` 166 | io.WriteString(m, strings.ToLower(user.Email())) 167 | ``` 168 | 169 | ### 第1刷 170 | 171 | #### ■p.158 ノート記事の1行目 172 | 173 | **誤** 174 | 175 | ``` 176 | GowebやGorillzによるmuxパッケージなどは、 177 | ``` 178 | 179 | **正** 180 | 181 | ``` 182 | GowebやGorillaによるmuxパッケージなどは、 183 | ``` 184 | -------------------------------------------------------------------------------- /appendixB/backup/archiver.go: -------------------------------------------------------------------------------- 1 | package backup 2 | 3 | import ( 4 | "archive/zip" 5 | "io" 6 | "os" 7 | "path/filepath" 8 | ) 9 | 10 | type Archiver interface { 11 | DestFmt() string 12 | Archive(src, dest string) error 13 | } 14 | 15 | type zipper struct{} 16 | 17 | var ZIP Archiver = (*zipper)(nil) 18 | 19 | func (z *zipper) DestFmt() string { 20 | return "%d.zip" 21 | } 22 | 23 | func (z *zipper) Archive(src, dest string) error { 24 | if err := os.MkdirAll(filepath.Dir(dest), 0777); err != nil { 25 | return err 26 | } 27 | out, err := os.Create(dest) 28 | if err != nil { 29 | return err 30 | } 31 | defer out.Close() 32 | w := zip.NewWriter(out) 33 | defer w.Close() 34 | return filepath.Walk(src, func(path string, info os.FileInfo, 35 | err error) error { 36 | if info.IsDir() { 37 | return nil // スキップします 38 | } 39 | if err != nil { 40 | return err 41 | } 42 | in, err := os.Open(path) 43 | if err != nil { 44 | return err 45 | } 46 | defer in.Close() 47 | f, err := w.Create(path) 48 | if err != nil { 49 | return err 50 | } 51 | io.Copy(f, in) 52 | return nil 53 | }) 54 | } 55 | -------------------------------------------------------------------------------- /appendixB/backup/cmds/backup/backup: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oreilly-japan/go-programming-blueprints/fc204afa474490efb10b7ad8d1928b66848f02e2/appendixB/backup/cmds/backup/backup -------------------------------------------------------------------------------- /appendixB/backup/cmds/backup/backupdata/.keepme: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oreilly-japan/go-programming-blueprints/fc204afa474490efb10b7ad8d1928b66848f02e2/appendixB/backup/cmds/backup/backupdata/.keepme -------------------------------------------------------------------------------- /appendixB/backup/cmds/backup/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "encoding/json" 5 | "errors" 6 | "flag" 7 | "fmt" 8 | "log" 9 | "strings" 10 | 11 | "github.com/matryer/filedb" 12 | ) 13 | 14 | type path struct { 15 | Path string 16 | Hash string 17 | } 18 | 19 | func (p path) String() string { 20 | return fmt.Sprintf("%s [%s]", p.Path, p.Hash) 21 | } 22 | 23 | func main() { 24 | var fatalErr error 25 | defer func() { 26 | if fatalErr != nil { 27 | flag.PrintDefaults() 28 | log.Fatalln(fatalErr) 29 | } 30 | }() 31 | var ( 32 | dbpath = flag.String("db", "./backupdata", "データベースのディレクトリへのパス") 33 | ) 34 | flag.Parse() 35 | args := flag.Args() 36 | if len(args) < 1 { 37 | fatalErr = errors.New("エラー; コマンドを指定してください") 38 | return 39 | } 40 | 41 | db, err := filedb.Dial(*dbpath) 42 | if err != nil { 43 | fatalErr = err 44 | return 45 | } 46 | defer db.Close() 47 | col, err := db.C("paths") 48 | if err != nil { 49 | fatalErr = err 50 | return 51 | } 52 | 53 | switch strings.ToLower(args[0]) { 54 | case "list": 55 | var path path 56 | col.ForEach(func(i int, data []byte) bool { 57 | err := json.Unmarshal(data, &path) 58 | if err != nil { 59 | fatalErr = err 60 | return true 61 | } 62 | fmt.Printf("= %s\n", path) 63 | return false 64 | }) 65 | 66 | case "add": 67 | if len(args[1:]) == 0 { 68 | fatalErr = errors.New("追加するパスを指定してください") 69 | return 70 | } 71 | for _, p := range args[1:] { 72 | path := &path{Path: p, Hash: "まだアーカイブされていません"} 73 | if err := col.InsertJSON(path); err != nil { 74 | fatalErr = err 75 | return 76 | } 77 | fmt.Printf("+ %s\n", path) 78 | } 79 | 80 | case "remove": 81 | var path path 82 | col.RemoveEach(func(i int, data []byte) (bool, bool) { 83 | err := json.Unmarshal(data, &path) 84 | if err != nil { 85 | fatalErr = err 86 | return false, true 87 | } 88 | for _, p := range args[1:] { 89 | if path.Path == p { 90 | fmt.Printf("- %s\n", path) 91 | return true, false 92 | } 93 | } 94 | return false, false 95 | }) 96 | 97 | } 98 | 99 | } 100 | -------------------------------------------------------------------------------- /appendixB/backup/cmds/backupd/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "encoding/json" 5 | "errors" 6 | "flag" 7 | "fmt" 8 | "log" 9 | "os" 10 | "os/signal" 11 | "syscall" 12 | "time" 13 | 14 | "github.com/matryer/filedb" 15 | "github.com/oreilly-japan/go-programming-blueprints/appendixB/backup" 16 | ) 17 | 18 | type path struct { 19 | Path string 20 | Hash string 21 | } 22 | 23 | func main() { 24 | var fatalErr error 25 | defer func() { 26 | if fatalErr != nil { 27 | log.Fatalln(fatalErr) 28 | } 29 | }() 30 | var ( 31 | interval = flag.Duration("interval", 10*time.Second, "チェックの間隔") 32 | archive = flag.String("archive", "archive", "アーカイブの保存先") 33 | dbpath = flag.String("db", "./db", "filedbデータベースへのパス") 34 | ) 35 | flag.Parse() 36 | 37 | m := &backup.Monitor{ 38 | Destination: *archive, 39 | Archiver: backup.ZIP, 40 | Paths: make(map[string]string), 41 | } 42 | 43 | db, err := filedb.Dial(*dbpath) 44 | if err != nil { 45 | fatalErr = err 46 | return 47 | } 48 | defer db.Close() 49 | col, err := db.C("paths") 50 | if err != nil { 51 | fatalErr = err 52 | return 53 | } 54 | 55 | var path path 56 | col.ForEach(func(_ int, data []byte) bool { 57 | if err := json.Unmarshal(data, &path); err != nil { 58 | fatalErr = err 59 | return true 60 | } 61 | m.Paths[path.Path] = path.Hash 62 | return false // 処理を続行します 63 | }) 64 | if fatalErr != nil { 65 | return 66 | } 67 | if len(m.Paths) < 1 { 68 | fatalErr = errors.New("パスがありません。backupツールを使って追加してください") 69 | return 70 | } 71 | check(m, col) 72 | signalChan := make(chan os.Signal, 1) 73 | signal.Notify(signalChan, syscall.SIGINT, syscall.SIGTERM) 74 | Loop: 75 | for { 76 | select { 77 | case <-time.After(*interval): 78 | check(m, col) 79 | case <-signalChan: 80 | // 終了 81 | fmt.Println() 82 | log.Printf("終了します...") 83 | break Loop 84 | } 85 | } 86 | 87 | } 88 | 89 | func check(m *backup.Monitor, col *filedb.C) { 90 | log.Println("チェックします...") 91 | counter, err := m.Now() 92 | if err != nil { 93 | log.Panicln("バックアップに失敗しました:", err) 94 | } 95 | if counter > 0 { 96 | log.Printf(" %d個のディレクトリをアーカイブしました\n", counter) 97 | // ハッシュ値を更新します 98 | var path path 99 | col.SelectEach(func(_ int, data []byte) (bool, []byte, bool) { 100 | if err := json.Unmarshal(data, &path); err != nil { 101 | log.Println("JSONデータの読み込みに失敗しました。"+ 102 | "次の項目に進みます:", err) 103 | return true, data, false 104 | } 105 | path.Hash, _ = m.Paths[path.Path] 106 | newdata, err := json.Marshal(&path) 107 | if err != nil { 108 | log.Println("JSONデータの書き出しに失敗しました。"+ 109 | "次の項目に進みます:", err) 110 | return true, data, false 111 | } 112 | return true, newdata, false 113 | }) 114 | } else { 115 | log.Println(" 変更はありません") 116 | } 117 | } 118 | -------------------------------------------------------------------------------- /appendixB/backup/dirhash.go: -------------------------------------------------------------------------------- 1 | package backup 2 | 3 | import ( 4 | "crypto/md5" 5 | "fmt" 6 | "io" 7 | "os" 8 | "path/filepath" 9 | ) 10 | 11 | func DirHash(path string) (string, error) { 12 | hash := md5.New() 13 | err := filepath.Walk(path, func(path string, info os.FileInfo, err error) error { 14 | if err != nil { 15 | return err 16 | } 17 | io.WriteString(hash, path) 18 | fmt.Fprintf(hash, "%v", info.IsDir()) 19 | fmt.Fprintf(hash, "%v", info.ModTime()) 20 | fmt.Fprintf(hash, "%v", info.Mode()) 21 | fmt.Fprintf(hash, "%v", info.Name()) 22 | fmt.Fprintf(hash, "%v", info.Size()) 23 | return nil 24 | }) 25 | if err != nil { 26 | return "", err 27 | } 28 | return fmt.Sprintf("%x", hash.Sum(nil)), nil 29 | } 30 | -------------------------------------------------------------------------------- /appendixB/backup/monitor.go: -------------------------------------------------------------------------------- 1 | package backup 2 | 3 | import ( 4 | "fmt" 5 | "path/filepath" 6 | "time" 7 | ) 8 | 9 | type Monitor struct { 10 | Paths map[string]string 11 | Archiver Archiver 12 | Destination string 13 | } 14 | 15 | func (m *Monitor) Now() (int, error) { 16 | var counter int 17 | for path, lastHash := range m.Paths { 18 | newHash, err := DirHash(path) 19 | if err != nil { 20 | return 0, err 21 | } 22 | if newHash != lastHash { 23 | err := m.act(path) 24 | if err != nil { 25 | return counter, err 26 | } 27 | m.Paths[path] = newHash // ハッシュ値を更新します 28 | counter++ 29 | } 30 | } 31 | return counter, nil 32 | } 33 | 34 | func (m *Monitor) act(path string) error { 35 | dirname := filepath.Base(path) 36 | filename := fmt.Sprintf(m.Archiver.DestFmt(), 37 | time.Now().UnixNano()) 38 | return m.Archiver.Archive(path, filepath.Join(m.Destination, 39 | dirname, filename)) 40 | } 41 | -------------------------------------------------------------------------------- /appendixB/chat/auth.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "crypto/md5" 5 | "fmt" 6 | "io" 7 | "log" 8 | "net/http" 9 | "strings" 10 | 11 | "github.com/stretchr/gomniauth" 12 | gomniauthcommon "github.com/stretchr/gomniauth/common" 13 | "github.com/stretchr/objx" 14 | ) 15 | 16 | type ChatUser interface { 17 | UniqueID() string 18 | AvatarURL() string 19 | } 20 | type chatUser struct { 21 | gomniauthcommon.User 22 | uniqueID string 23 | } 24 | 25 | func (u chatUser) UniqueID() string { 26 | return u.uniqueID 27 | } 28 | 29 | type authHandler struct { 30 | next http.Handler 31 | } 32 | 33 | func (h *authHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { 34 | if cookie, err := r.Cookie("auth"); err == http.ErrNoCookie || cookie.Value == "" { 35 | // 未認証 36 | w.Header().Set("Location", "/login") 37 | w.WriteHeader(http.StatusTemporaryRedirect) 38 | } else if err != nil { 39 | // 何らかの別のエラーが発生 40 | panic(err.Error()) 41 | } else { 42 | // 成功。ラップされたハンドラを呼び出します 43 | h.next.ServeHTTP(w, r) 44 | } 45 | } 46 | func MustAuth(handler http.Handler) http.Handler { 47 | return &authHandler{next: handler} 48 | } 49 | 50 | // loginHandlerはサードパーティーへのログインの処理を受け持ちます。 51 | // パスの形式: /auth/{action}/{provider} 52 | func loginHandler(w http.ResponseWriter, r *http.Request) { 53 | segs := strings.Split(r.URL.Path, "/") 54 | action := segs[2] 55 | provider := segs[3] 56 | switch action { 57 | case "login": 58 | provider, err := gomniauth.Provider(provider) 59 | if err != nil { 60 | log.Fatalln("認証プロバイダーの取得に失敗しました:", provider, "-", err) 61 | } 62 | loginUrl, err := provider.GetBeginAuthURL(nil, nil) 63 | if err != nil { 64 | log.Fatalln("GetBeginAuthURLの呼び出し中にエラーが発生しました:", provider, "-", err) 65 | } 66 | w.Header().Set("Location", loginUrl) 67 | w.WriteHeader(http.StatusTemporaryRedirect) 68 | case "callback": 69 | provider, err := gomniauth.Provider(provider) 70 | if err != nil { 71 | log.Fatalln("認証プロバイダーの取得に失敗しました", provider, "-", err) 72 | } 73 | 74 | creds, err := 75 | provider.CompleteAuth(objx.MustFromURLQuery(r.URL.RawQuery)) 76 | if err != nil { 77 | log.Fatalln("認証を完了できませんでした", provider, "-", err) 78 | } 79 | 80 | user, err := provider.GetUser(creds) 81 | if err != nil { 82 | log.Fatalln("ユーザーの取得に失敗しました", provider, "- ", err) 83 | } 84 | 85 | chatUser := &chatUser{User: user} 86 | m := md5.New() 87 | io.WriteString(m, strings.ToLower(user.Name())) 88 | chatUser.uniqueID = fmt.Sprintf("%x", m.Sum(nil)) 89 | avatarURL, err := avatars.GetAvatarURL(chatUser) 90 | if err != nil { 91 | log.Fatalln("GetAvatarURLに失敗しました", "-", err) 92 | } 93 | // データを保存します 94 | authCookieValue := objx.New(map[string]interface{}{ 95 | "userid": chatUser.uniqueID, 96 | "name": user.Name(), 97 | "avatar_url": avatarURL, 98 | }).MustBase64() 99 | http.SetCookie(w, &http.Cookie{ 100 | Name: "auth", 101 | Value: authCookieValue, 102 | Path: "/"}) 103 | w.Header()["Location"] = []string{"/chat"} 104 | w.WriteHeader(http.StatusTemporaryRedirect) 105 | default: 106 | w.WriteHeader(http.StatusNotFound) 107 | fmt.Fprintf(w, "アクション%sには非対応です", action) 108 | } 109 | } 110 | -------------------------------------------------------------------------------- /appendixB/chat/avatar.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "errors" 5 | "path/filepath" 6 | ) 7 | 8 | // ErrNoAvatarはAvatarインスタンスがアバターのURLを返すことができない 9 | // 場合に発生するエラーです。 10 | var ErrNoAvatarURL = errors.New("chat: アバターのURLを取得できません。") 11 | 12 | // Avatarはユーザーのプロフィール画像を表す型です。 13 | type Avatar interface { 14 | // GetAvatarURLは指定されたクライアントのアバターのURLを返します。 15 | // 問題が発生した場合にはエラーを返します。特に、URLを取得できなかった 16 | // 場合にはErrNoAvatarURLを返します。 17 | GetAvatarURL(ChatUser) (string, error) 18 | } 19 | 20 | type TryAvatars []Avatar 21 | 22 | func (a TryAvatars) GetAvatarURL(u ChatUser) (string, error) { 23 | for _, avatar := range a { 24 | if url, err := avatar.GetAvatarURL(u); err == nil { 25 | return url, nil 26 | } 27 | } 28 | return "", ErrNoAvatarURL 29 | } 30 | 31 | type AuthAvatar struct{} 32 | 33 | var UseAuthAvatar AuthAvatar 34 | 35 | func (AuthAvatar) GetAvatarURL(u ChatUser) (string, error) { 36 | url := u.AvatarURL() 37 | if url != "" { 38 | return url, nil 39 | } 40 | return "", ErrNoAvatarURL 41 | } 42 | 43 | type GravatarAvatar struct{} 44 | 45 | var UseGravatar GravatarAvatar 46 | 47 | func (GravatarAvatar) GetAvatarURL(u ChatUser) (string, error) { 48 | return "//www.gravatar.com/avatar/" + u.UniqueID(), nil 49 | } 50 | 51 | type FileSystemAvatar struct{} 52 | 53 | var UseFileSystemAvatar FileSystemAvatar 54 | 55 | func (FileSystemAvatar) GetAvatarURL(u ChatUser) (string, error) { 56 | matches, err := filepath.Glob(filepath.Join("avatars", u.UniqueID()+"*")) 57 | if err != nil || len(matches) == 0 { 58 | return "", ErrNoAvatarURL 59 | } 60 | return "/" + matches[0], nil 61 | } 62 | -------------------------------------------------------------------------------- /appendixB/chat/avatar_test.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "io/ioutil" 5 | "os" 6 | "path/filepath" 7 | "testing" 8 | 9 | gomniauthtest "github.com/stretchr/gomniauth/test" 10 | ) 11 | 12 | func TestAuthAvatar(t *testing.T) { 13 | var authAvatar AuthAvatar 14 | testUser := &gomniauthtest.TestUser{} 15 | testUser.On("AvatarURL").Return("", ErrNoAvatarURL) 16 | testChatUser := &chatUser{User: testUser} 17 | url, err := authAvatar.GetAvatarURL(testChatUser) 18 | if err != ErrNoAvatarURL { 19 | t.Error("値が存在しない場合、AuthAvatar.GetAvatarURLは" + 20 | "ErrNoAvatarURLを返すべきです") 21 | } 22 | // 値をセットします 23 | testUrl := "http://url-to-avatar/" 24 | testUser = &gomniauthtest.TestUser{} 25 | testChatUser.User = testUser 26 | testUser.On("AvatarURL").Return(testUrl, nil) 27 | url, err = authAvatar.GetAvatarURL(testChatUser) 28 | if err != nil { 29 | t.Error("値が存在する場合、AuthAvatar.GetAvatarURLは" + 30 | "エラーを返すべきではありません") 31 | } else { 32 | if url != testUrl { 33 | t.Error("AuthAvatar.GetAvatarURLは正しいURLを返すべきです") 34 | } 35 | } 36 | } 37 | 38 | func TestGravatarAvatar(t *testing.T) { 39 | var gravatarAvatar GravatarAvatar 40 | user := &chatUser{uniqueID: "abc"} 41 | url, err := gravatarAvatar.GetAvatarURL(user) 42 | if err != nil { 43 | t.Error("GravatarAvitar.GetAvatarURLはエラーを返すべきではありません") 44 | } 45 | if url != "//www.gravatar.com/avatar/abc" { 46 | t.Errorf("GravatarAvitar.GetAvatarURLが%sという誤った値を返しました", url) 47 | } 48 | } 49 | 50 | func TestFileSystemAvatar(t *testing.T) { 51 | 52 | // テスト用のアバターのファイルを生成します 53 | filename := filepath.Join("avatars", "abc.jpg") 54 | ioutil.WriteFile(filename, []byte{}, 0777) 55 | defer func() { os.Remove(filename) }() 56 | 57 | var fileSystemAvatar FileSystemAvatar 58 | user := &chatUser{uniqueID: "abc"} 59 | url, err := fileSystemAvatar.GetAvatarURL(user) 60 | if err != nil { 61 | t.Error("FileSystemAvatar.GetAvatarURLはエラーを返すべきではありません") 62 | } 63 | if url != "/avatars/abc.jpg" { 64 | t.Errorf("FileSystemAvatar.GetAvatarURLが%sという誤った値を返しました", url) 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /appendixB/chat/avatars/.keepme: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oreilly-japan/go-programming-blueprints/fc204afa474490efb10b7ad8d1928b66848f02e2/appendixB/chat/avatars/.keepme -------------------------------------------------------------------------------- /appendixB/chat/client.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "time" 5 | 6 | "github.com/gorilla/websocket" 7 | ) 8 | 9 | // clientはチャットを行っている1人のユーザーを表します。 10 | type client struct { 11 | // socketはこのクライアントのためのWebSocketです。 12 | socket *websocket.Conn 13 | // sendはメッセージが送られるチャネルです。 14 | send chan *message 15 | // roomはこのクライアントが参加しているチャットルームです。 16 | room *room 17 | // userDataはユーザーに関する情報を保持します 18 | userData map[string]interface{} 19 | } 20 | 21 | func (c *client) read() { 22 | for { 23 | var msg *message 24 | if err := c.socket.ReadJSON(&msg); err == nil { 25 | msg.When = time.Now() 26 | msg.Name = c.userData["name"].(string) 27 | if avatarURL, ok := c.userData["avatar_url"]; ok { 28 | msg.AvatarURL = avatarURL.(string) 29 | } 30 | c.room.forward <- msg 31 | } else { 32 | break 33 | } 34 | } 35 | c.socket.Close() 36 | } 37 | func (c *client) write() { 38 | for msg := range c.send { 39 | if err := c.socket.WriteJSON(msg); err != nil { 40 | break 41 | } 42 | } 43 | c.socket.Close() 44 | } 45 | -------------------------------------------------------------------------------- /appendixB/chat/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "flag" 5 | "html/template" 6 | "log" 7 | "net/http" 8 | "os" 9 | "path/filepath" 10 | "sync" 11 | 12 | "github.com/oreilly-japan/go-programming-blueprints/appendixB/trace" 13 | "github.com/stretchr/gomniauth" 14 | "github.com/stretchr/gomniauth/providers/facebook" 15 | "github.com/stretchr/gomniauth/providers/github" 16 | "github.com/stretchr/gomniauth/providers/google" 17 | "github.com/stretchr/objx" 18 | ) 19 | 20 | // 現在アクティブなAvatarの実装 21 | var avatars Avatar = TryAvatars{ 22 | UseFileSystemAvatar, 23 | UseAuthAvatar, 24 | UseGravatar} 25 | 26 | // templは1つのテンプレートを表します 27 | type templateHandler struct { 28 | once sync.Once 29 | filename string 30 | templ *template.Template 31 | } 32 | 33 | // ServeHTTPはHTTPリクエストを処理します 34 | func (t *templateHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { 35 | t.once.Do(func() { 36 | t.templ = 37 | template.Must(template.ParseFiles(filepath.Join("templates", 38 | t.filename))) 39 | }) 40 | data := map[string]interface{}{ 41 | "Host": r.Host, 42 | } 43 | if authCookie, err := r.Cookie("auth"); err == nil { 44 | data["UserData"] = objx.MustFromBase64(authCookie.Value) 45 | } 46 | t.templ.Execute(w, data) 47 | } 48 | 49 | func main() { 50 | var addr = flag.String("addr", ":8080", "アプリケーションのアドレス") 51 | flag.Parse() // フラグを解釈します 52 | // Gomniauthのセットアップ 53 | gomniauth.SetSecurityKey("セキュリティキー") 54 | gomniauth.WithProviders( 55 | facebook.New("クライアントID", "秘密の値", "http://localhost:8080/auth/callback/facebook"), 56 | github.New("クライアントID", "秘密の値", "http://localhost:8080/auth/callback/github"), 57 | google.New("クライアントID", "秘密の値", "http://localhost:8080/auth/callback/google"), 58 | ) 59 | 60 | r := newRoom() 61 | r.tracer = trace.New(os.Stdout) 62 | http.Handle("/chat", MustAuth(&templateHandler{filename: "chat.html"})) 63 | http.Handle("/login", &templateHandler{filename: "login.html"}) 64 | http.HandleFunc("/auth/", loginHandler) 65 | http.HandleFunc("/logout", func(w http.ResponseWriter, r *http.Request) { 66 | http.SetCookie(w, &http.Cookie{ 67 | Name: "auth", 68 | Value: "", 69 | Path: "/", 70 | MaxAge: -1, 71 | }) 72 | w.Header()["Location"] = []string{"/chat"} 73 | w.WriteHeader(http.StatusTemporaryRedirect) 74 | }) 75 | http.Handle("/upload", &templateHandler{filename: "upload.html"}) 76 | http.HandleFunc("/uploader", uploaderHandler) 77 | http.Handle("/avatars/", 78 | http.StripPrefix("/avatars/", 79 | http.FileServer(http.Dir("./avatars")))) 80 | 81 | http.Handle("/room", r) 82 | // チャットルームを開始します 83 | go r.run() 84 | // Webサーバーを起動します 85 | log.Println("Webサーバーを開始します。ポート: ", *addr) 86 | if err := http.ListenAndServe(*addr, nil); err != nil { 87 | log.Fatal("ListenAndServe:", err) 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /appendixB/chat/message.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "time" 5 | ) 6 | 7 | // messageは1つのメッセージを表します。 8 | type message struct { 9 | Name string 10 | Message string 11 | When time.Time 12 | AvatarURL string 13 | } 14 | -------------------------------------------------------------------------------- /appendixB/chat/room.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "log" 5 | "net/http" 6 | 7 | "github.com/gorilla/websocket" 8 | "github.com/oreilly-japan/go-programming-blueprints/appendixB/trace" 9 | "github.com/stretchr/objx" 10 | ) 11 | 12 | type room struct { 13 | // forwardは他のクライアントに転送するためのメッセージを保持するチャネルです。 14 | forward chan *message 15 | // joinはチャットルームに参加しようとしているクライアントのためのチャネルです。 16 | join chan *client 17 | // leaveはチャットルームから退室しようとしているクライアントのためのチャネルです 18 | leave chan *client 19 | // clientsには在室しているすべてのクライアントが保持されます。 20 | clients map[*client]bool 21 | // tracerはチャットルーム上で行われた操作のログを受け取ります。 22 | tracer *trace.Tracer 23 | } 24 | 25 | // newRoomはすぐに利用できるチャットルームを生成して返します。 26 | func newRoom() *room { 27 | return &room{ 28 | forward: make(chan *message), 29 | join: make(chan *client), 30 | leave: make(chan *client), 31 | clients: make(map[*client]bool), 32 | } 33 | } 34 | 35 | func (r *room) run() { 36 | for { 37 | select { 38 | case client := <-r.join: 39 | // 参加 40 | r.clients[client] = true 41 | r.tracer.Trace("新しいクライアントが参加しました") 42 | case client := <-r.leave: 43 | // 退室 44 | delete(r.clients, client) 45 | close(client.send) 46 | r.tracer.Trace("クライアントが退室しました") 47 | case msg := <-r.forward: 48 | r.tracer.Trace("メッセージを受信しました: ", msg.Message) 49 | // すべてのクライアントにメッセージを転送 50 | for client := range r.clients { 51 | select { 52 | case client.send <- msg: 53 | // メッセージを送信 54 | r.tracer.Trace(" -- クライアントに送信されました") 55 | default: 56 | // 送信に失敗 57 | delete(r.clients, client) 58 | close(client.send) 59 | r.tracer.Trace(" -- 送信に失敗しました。クライアントをクリーンアップします") 60 | } 61 | } 62 | } 63 | } 64 | } 65 | 66 | const ( 67 | socketBufferSize = 1024 68 | messageBufferSize = 256 69 | ) 70 | 71 | var upgrader = &websocket.Upgrader{ReadBufferSize: socketBufferSize, WriteBufferSize: socketBufferSize} 72 | 73 | func (r *room) ServeHTTP(w http.ResponseWriter, req *http.Request) { 74 | socket, err := upgrader.Upgrade(w, req, nil) 75 | if err != nil { 76 | log.Fatal("ServeHTTP:", err) 77 | return 78 | } 79 | authCookie, err := req.Cookie("auth") 80 | if err != nil { 81 | log.Fatal("クッキーの取得に失敗しました:", err) 82 | return 83 | } 84 | client := &client{ 85 | socket: socket, 86 | send: make(chan *message, messageBufferSize), 87 | room: r, 88 | userData: objx.MustFromBase64(authCookie.Value), 89 | } 90 | r.join <- client 91 | defer func() { r.leave <- client }() 92 | go client.write() 93 | client.read() 94 | } 95 | -------------------------------------------------------------------------------- /appendixB/chat/templates/chat.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | チャット 4 | 6 | 11 | 12 | 13 |
14 |
15 |
16 |
    17 |
    18 |
    19 |
    20 |
    21 | 22 | またはサインアウト 23 | 24 |
    25 | 26 |
    27 |
    28 | 29 | 31 | 69 | 70 | 71 | 72 | -------------------------------------------------------------------------------- /appendixB/chat/templates/login.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | ログイン 4 | 6 | 7 | 8 |
    9 | 12 |
    13 |
    14 |

    チャットを行うにはサインインが必要です

    15 |
    16 |
    17 |

    サインインに使用するサービスを選んでください:

    18 | 29 |
    30 |
    31 |
    32 | 33 | 34 | -------------------------------------------------------------------------------- /appendixB/chat/templates/upload.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | アップロード 4 | 6 | 7 | 8 |
    9 | 12 |
    14 | 15 |
    16 | 17 | 18 |
    19 | 20 |
    21 |
    22 | 23 | 24 | -------------------------------------------------------------------------------- /appendixB/chat/upload.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "io" 5 | "io/ioutil" 6 | "net/http" 7 | "path/filepath" 8 | ) 9 | 10 | func uploaderHandler(w http.ResponseWriter, req *http.Request) { 11 | userId := req.FormValue("userid") 12 | file, header, err := req.FormFile("avatarFile") 13 | if err != nil { 14 | io.WriteString(w, err.Error()) 15 | return 16 | } 17 | defer file.Close() 18 | data, err := ioutil.ReadAll(file) 19 | if err != nil { 20 | io.WriteString(w, err.Error()) 21 | return 22 | } 23 | filename := filepath.Join("avatars", userId+filepath.Ext(header.Filename)) 24 | err = ioutil.WriteFile(filename, data, 0777) 25 | if err != nil { 26 | io.WriteString(w, err.Error()) 27 | return 28 | } 29 | io.WriteString(w, "成功") 30 | } 31 | -------------------------------------------------------------------------------- /appendixB/socialpoll/counter/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "log" 5 | "os" 6 | "os/signal" 7 | "sync" 8 | "syscall" 9 | "time" 10 | 11 | "github.com/bitly/go-nsq" 12 | 13 | "gopkg.in/mgo.v2" 14 | "gopkg.in/mgo.v2/bson" 15 | ) 16 | 17 | const updateDuration = 1 * time.Second 18 | 19 | func main() { 20 | err := counterMain() 21 | if err != nil { 22 | log.Fatal(err) 23 | } 24 | } 25 | 26 | func counterMain() error { 27 | log.Println("データベースに接続します...") 28 | db, err := mgo.Dial("localhost") 29 | if err != nil { 30 | return err 31 | } 32 | defer func() { 33 | log.Println("データベース接続を閉じます...") 34 | db.Close() 35 | }() 36 | pollData := db.DB("ballots").C("polls") 37 | 38 | var counts map[string]int 39 | var countsLock sync.Mutex 40 | 41 | log.Println("NSQに接続します...") 42 | q, err := nsq.NewConsumer("votes", "counter", nsq.NewConfig()) 43 | if err != nil { 44 | return err 45 | } 46 | q.AddHandler(nsq.HandlerFunc(func(m *nsq.Message) error { 47 | countsLock.Lock() 48 | defer countsLock.Unlock() 49 | if counts == nil { 50 | counts = make(map[string]int) 51 | } 52 | vote := string(m.Body) 53 | counts[vote]++ 54 | return nil 55 | })) 56 | 57 | if err := q.ConnectToNSQLookupd("localhost:4161"); err != nil { 58 | return err 59 | } 60 | 61 | log.Println("NSQ上での投票を待機します...") 62 | 63 | ticker := time.NewTicker(updateDuration) 64 | defer ticker.Stop() 65 | 66 | update := func() { 67 | countsLock.Lock() 68 | defer countsLock.Unlock() 69 | if len(counts) == 0 { 70 | log.Println("新しい投票はありません。データベースの更新をスキップします") 71 | return 72 | } 73 | log.Println("データベースを更新します...") 74 | log.Println(counts) 75 | ok := true 76 | for option, count := range counts { 77 | sel := bson.M{"options": bson.M{"$in": []string{option}}} 78 | up := bson.M{"$inc": bson.M{"results." + option: count}} 79 | if _, err := pollData.UpdateAll(sel, up); err != nil { 80 | log.Println("更新に失敗しました:", err) 81 | ok = false 82 | } else { 83 | counts[option] = 0 84 | } 85 | } 86 | if ok { 87 | log.Println("データベースの更新が完了しました") 88 | counts = nil // 得票数をリセットします 89 | } 90 | } 91 | 92 | termChan := make(chan os.Signal, 1) 93 | signal.Notify(termChan, syscall.SIGINT, syscall.SIGTERM, syscall.SIGHUP) 94 | 95 | for { 96 | select { 97 | case <-ticker.C: 98 | update() 99 | case <-termChan: 100 | q.Stop() 101 | case <-q.StopChan: 102 | // 完了しました 103 | return nil 104 | } 105 | } 106 | } 107 | -------------------------------------------------------------------------------- /appendixB/socialpoll/twittervotes/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "log" 5 | "os" 6 | "os/signal" 7 | "syscall" 8 | 9 | "golang.org/x/net/context" 10 | 11 | "github.com/bitly/go-nsq" 12 | 13 | "gopkg.in/mgo.v2" 14 | ) 15 | 16 | func main() { 17 | ctx, cancel := context.WithCancel(context.Background()) 18 | signalChan := make(chan os.Signal, 1) 19 | go func() { 20 | <-signalChan 21 | cancel() 22 | log.Println("停止します...") 23 | }() 24 | signal.Notify(signalChan, syscall.SIGINT, syscall.SIGTERM) 25 | 26 | if err := dialdb(); err != nil { 27 | log.Fatalln("MongoDBへのダイヤルに失敗しました:", err) 28 | } 29 | defer closedb() 30 | 31 | // 処理を開始します 32 | votes := make(chan string) // 投票結果のためのチャネル 33 | go twitterStream(ctx, votes) 34 | publishVotes(votes) 35 | } 36 | 37 | var db *mgo.Session 38 | 39 | func dialdb() error { 40 | var err error 41 | log.Println("MongoDBにダイヤル中: localhost") 42 | db, err = mgo.Dial("localhost") 43 | return err 44 | } 45 | func closedb() { 46 | db.Close() 47 | log.Println("データベース接続が閉じられました") 48 | } 49 | 50 | type poll struct { 51 | Options []string 52 | } 53 | 54 | func loadOptions() ([]string, error) { 55 | var options []string 56 | iter := db.DB("ballots").C("polls").Find(nil).Iter() 57 | var p poll 58 | for iter.Next(&p) { 59 | options = append(options, p.Options...) 60 | } 61 | iter.Close() 62 | return options, iter.Err() 63 | } 64 | 65 | func publishVotes(votes <-chan string) { 66 | pub, _ := nsq.NewProducer("localhost:4150", nsq.NewConfig()) 67 | for vote := range votes { 68 | pub.Publish("votes", []byte(vote)) // 投票内容をパブリッシュします 69 | } 70 | log.Println("Publisher: 停止中です") 71 | pub.Stop() 72 | log.Println("Publisher: 停止しました") 73 | } 74 | -------------------------------------------------------------------------------- /appendixB/socialpoll/twittervotes/twitter.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "bytes" 5 | "encoding/json" 6 | "io" 7 | "log" 8 | "net/http" 9 | "net/url" 10 | "strconv" 11 | "strings" 12 | "sync" 13 | "time" 14 | 15 | "golang.org/x/net/context" 16 | 17 | "github.com/garyburd/go-oauth/oauth" 18 | "github.com/joeshaw/envdecode" 19 | ) 20 | 21 | var ( 22 | authClient *oauth.Client 23 | creds *oauth.Credentials 24 | ) 25 | 26 | func setupTwitterAuth() { 27 | var ts struct { 28 | ConsumerKey string `env:"SP_TWITTER_KEY,required"` 29 | ConsumerSecret string `env:"SP_TWITTER_SECRET,required"` 30 | AccessToken string `env:"SP_TWITTER_ACCESSTOKEN,required"` 31 | AccessSecret string `env:"SP_TWITTER_ACCESSSECRET,required"` 32 | } 33 | if err := envdecode.Decode(&ts); err != nil { 34 | log.Fatalln(err) 35 | } 36 | log.Println("ts:", ts) 37 | creds = &oauth.Credentials{ 38 | Token: ts.AccessToken, 39 | Secret: ts.AccessSecret, 40 | } 41 | authClient = &oauth.Client{ 42 | Credentials: oauth.Credentials{ 43 | Token: ts.ConsumerKey, 44 | Secret: ts.ConsumerSecret, 45 | }, 46 | } 47 | } 48 | 49 | var ( 50 | authSetupOnce sync.Once 51 | ) 52 | 53 | func makeRequest(query url.Values) (*http.Request, error) { 54 | authSetupOnce.Do(func() { 55 | setupTwitterAuth() 56 | }) 57 | const endpoint = "https://stream.twitter.com/1.1/statuses/filter.json" 58 | 59 | req, err := http.NewRequest("POST", endpoint, strings.NewReader(query.Encode())) 60 | if err != nil { 61 | return nil, err 62 | } 63 | formEnc := query.Encode() 64 | req.Header.Set("Content-Type", "application/x-www-form-urlencoded") 65 | req.Header.Set("Content-Length", strconv.Itoa(len(formEnc))) 66 | ah := authClient.AuthorizationHeader(creds, "POST", req.URL, query) 67 | req.Header.Set("Authorization", ah) 68 | return req, nil 69 | } 70 | 71 | type tweet struct { 72 | Text string 73 | } 74 | 75 | func readFromTwitter(ctx context.Context, votes chan<- string) { 76 | options, err := loadOptions() 77 | if err != nil { 78 | log.Println("選択肢の読み込みに失敗しました:", err) 79 | return 80 | } 81 | 82 | query := make(url.Values) 83 | query.Set("track", strings.Join(options, ",")) 84 | req, err := makeRequest(query) 85 | if err != nil { 86 | log.Println("検索のリクエストの作成に失敗しました:", err) 87 | return 88 | } 89 | client := &http.Client{} 90 | if deadline, ok := ctx.Deadline(); ok { 91 | client.Timeout = deadline.Sub(time.Now()) 92 | } 93 | resp, err := client.Do(req) 94 | if err != nil { 95 | log.Println("検索のリクエストに失敗しました:", err) 96 | return 97 | } 98 | done := make(chan struct{}) 99 | defer func() { <-done }() 100 | 101 | defer resp.Body.Close() 102 | go func() { 103 | defer close(done) 104 | log.Println("resp:", resp.StatusCode) 105 | if resp.StatusCode != 200 { 106 | var buf bytes.Buffer 107 | io.Copy(&buf, resp.Body) 108 | log.Println("resp body: %s", buf.String()) 109 | return 110 | } 111 | decoder := json.NewDecoder(resp.Body) 112 | for { 113 | var tweet tweet 114 | if err := decoder.Decode(&tweet); err != nil { 115 | break 116 | } 117 | log.Println("tweet:", tweet) 118 | for _, option := range options { 119 | if strings.Contains(strings.ToLower(tweet.Text), strings.ToLower(option)) { 120 | log.Println("投票:", option) 121 | votes <- option 122 | } 123 | } 124 | } 125 | }() 126 | select { 127 | case <-ctx.Done(): 128 | case <-done: 129 | } 130 | } 131 | 132 | func readFromTwitterWithTimeout(ctx context.Context, timeout time.Duration, votes chan<- string) { 133 | ctx, cancel := context.WithTimeout(ctx, timeout) 134 | defer cancel() 135 | readFromTwitter(ctx, votes) 136 | } 137 | 138 | func twitterStream(ctx context.Context, votes chan<- string) { 139 | defer close(votes) 140 | for { 141 | log.Println("Twitterに問い合わせます...") 142 | readFromTwitterWithTimeout(ctx, 1*time.Minute, votes) 143 | log.Println(" (待機中)") 144 | select { 145 | case <-ctx.Done(): 146 | log.Println("Twitterへの問い合わせを終了します...") 147 | return 148 | case <-time.After(10 * time.Second): 149 | } 150 | } 151 | } 152 | -------------------------------------------------------------------------------- /appendixB/trace/tracer.go: -------------------------------------------------------------------------------- 1 | package trace 2 | 3 | import ( 4 | "fmt" 5 | "io" 6 | ) 7 | 8 | // Tracerはコード内での出来事を記録します。 9 | type Tracer struct { 10 | out io.Writer 11 | } 12 | 13 | func (t *Tracer) Trace(a ...interface{}) { 14 | if t == nil || t.out == nil { 15 | return 16 | } 17 | fmt.Fprintln(t.out, a...) 18 | } 19 | 20 | func New(w io.Writer) *Tracer { 21 | return &Tracer{out: w} 22 | } 23 | -------------------------------------------------------------------------------- /appendixB/trace/tracer_test.go: -------------------------------------------------------------------------------- 1 | package trace 2 | 3 | import ( 4 | "bytes" 5 | "testing" 6 | ) 7 | 8 | func TestNew(t *testing.T) { 9 | var buf bytes.Buffer 10 | tracer := New(&buf) 11 | tracer.Trace("こんにちは、traceパッケージ") 12 | if buf.String() != "こんにちは、traceパッケージ\n" { 13 | t.Errorf("'%s'という誤った文字列が出力されました", buf.String()) 14 | } 15 | } 16 | 17 | func TestOff(t *testing.T) { 18 | var silentTracer *Tracer 19 | silentTracer.Trace("データ") 20 | } 21 | -------------------------------------------------------------------------------- /chapter1/chat/client.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "github.com/gorilla/websocket" 5 | ) 6 | 7 | // clientはチャットを行っている1人のユーザーを表します。 8 | type client struct { 9 | // socketはこのクライアントのためのWebSocketです。 10 | socket *websocket.Conn 11 | // sendはメッセージが送られるチャネルです。 12 | send chan []byte 13 | // roomはこのクライアントが参加しているチャットルームです。 14 | room *room 15 | } 16 | 17 | func (c *client) read() { 18 | for { 19 | if _, msg, err := c.socket.ReadMessage(); err == nil { 20 | c.room.forward <- msg 21 | } else { 22 | break 23 | } 24 | } 25 | c.socket.Close() 26 | } 27 | func (c *client) write() { 28 | for msg := range c.send { 29 | if err := c.socket.WriteMessage(websocket.TextMessage, msg); err != nil { 30 | break 31 | } 32 | } 33 | c.socket.Close() 34 | } 35 | -------------------------------------------------------------------------------- /chapter1/chat/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "flag" 5 | "html/template" 6 | "log" 7 | "net/http" 8 | "os" 9 | "path/filepath" 10 | "sync" 11 | 12 | "github.com/oreilly-japan/go-programming-blueprints/chapter1/trace" 13 | ) 14 | 15 | // templは1つのテンプレートを表します 16 | type templateHandler struct { 17 | once sync.Once 18 | filename string 19 | templ *template.Template 20 | } 21 | 22 | // ServeHTTPはHTTPリクエストを処理します 23 | func (t *templateHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { 24 | t.once.Do(func() { 25 | t.templ = 26 | template.Must(template.ParseFiles(filepath.Join("templates", 27 | t.filename))) 28 | }) 29 | t.templ.Execute(w, r) 30 | } 31 | 32 | func main() { 33 | var addr = flag.String("addr", ":8080", "アプリケーションのアドレス") 34 | flag.Parse() // フラグを解釈します 35 | r := newRoom() 36 | r.tracer = trace.New(os.Stdout) 37 | http.Handle("/", &templateHandler{filename: "chat.html"}) 38 | http.Handle("/room", r) 39 | // チャットルームを開始します 40 | go r.run() 41 | // Webサーバーを起動します 42 | log.Println("Webサーバーを開始します。ポート: ", *addr) 43 | if err := http.ListenAndServe(*addr, nil); err != nil { 44 | log.Fatal("ListenAndServe:", err) 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /chapter1/chat/room.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "log" 5 | "net/http" 6 | 7 | "github.com/gorilla/websocket" 8 | "github.com/oreilly-japan/go-programming-blueprints/chapter1/trace" 9 | ) 10 | 11 | type room struct { 12 | // forwardは他のクライアントに転送するためのメッセージを保持するチャネルです。 13 | forward chan []byte 14 | // joinはチャットルームに参加しようとしているクライアントのためのチャネルです。 15 | join chan *client 16 | // leaveはチャットルームから退室しようとしているクライアントのためのチャネルです 17 | leave chan *client 18 | // clientsには在室しているすべてのクライアントが保持されます。 19 | clients map[*client]bool 20 | // tracerはチャットルーム上で行われた操作のログを受け取ります。 21 | tracer trace.Tracer 22 | } 23 | 24 | // newRoomはすぐに利用できるチャットルームを生成して返します。 25 | func newRoom() *room { 26 | return &room{ 27 | forward: make(chan []byte), 28 | join: make(chan *client), 29 | leave: make(chan *client), 30 | clients: make(map[*client]bool), 31 | tracer: trace.Off(), 32 | } 33 | } 34 | 35 | func (r *room) run() { 36 | for { 37 | select { 38 | case client := <-r.join: 39 | // 参加 40 | r.clients[client] = true 41 | r.tracer.Trace("新しいクライアントが参加しました") 42 | case client := <-r.leave: 43 | // 退室 44 | delete(r.clients, client) 45 | close(client.send) 46 | r.tracer.Trace("クライアントが退室しました") 47 | case msg := <-r.forward: 48 | r.tracer.Trace("メッセージを受信しました: ", string(msg)) 49 | // すべてのクライアントにメッセージを転送 50 | for client := range r.clients { 51 | select { 52 | case client.send <- msg: 53 | // メッセージを送信 54 | r.tracer.Trace(" -- クライアントに送信されました") 55 | default: 56 | // 送信に失敗 57 | delete(r.clients, client) 58 | close(client.send) 59 | r.tracer.Trace(" -- 送信に失敗しました。クライアントをクリーンアップします") 60 | } 61 | } 62 | } 63 | } 64 | } 65 | 66 | const ( 67 | socketBufferSize = 1024 68 | messageBufferSize = 256 69 | ) 70 | 71 | var upgrader = &websocket.Upgrader{ReadBufferSize: socketBufferSize, WriteBufferSize: socketBufferSize} 72 | 73 | func (r *room) ServeHTTP(w http.ResponseWriter, req *http.Request) { 74 | socket, err := upgrader.Upgrade(w, req, nil) 75 | if err != nil { 76 | log.Fatal("ServeHTTP:", err) 77 | return 78 | } 79 | client := &client{ 80 | socket: socket, 81 | send: make(chan []byte, messageBufferSize), 82 | room: r, 83 | } 84 | r.join <- client 85 | defer func() { r.leave <- client }() 86 | go client.write() 87 | client.read() 88 | } 89 | -------------------------------------------------------------------------------- /chapter1/chat/templates/chat.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | チャット 4 | 8 | 9 | 10 | 11 | WebSocketを使ったチャットアプリケーション 12 |
    13 | 14 | 15 |
    16 | 17 | 19 | 47 | 48 | 49 | 50 | -------------------------------------------------------------------------------- /chapter1/trace/tracer.go: -------------------------------------------------------------------------------- 1 | package trace 2 | 3 | import ( 4 | "fmt" 5 | "io" 6 | ) 7 | 8 | // Tracerはコード内での出来事を記録できるオブジェクトを表すインタフェースです。 9 | type Tracer interface { 10 | Trace(...interface{}) 11 | } 12 | 13 | func New(w io.Writer) Tracer { 14 | return &tracer{out: w} 15 | } 16 | 17 | type tracer struct { 18 | out io.Writer 19 | } 20 | 21 | func (t *tracer) Trace(a ...interface{}) { 22 | t.out.Write([]byte(fmt.Sprint(a...))) 23 | t.out.Write([]byte("\n")) 24 | } 25 | 26 | type nilTracer struct{} 27 | 28 | func (t *nilTracer) Trace(a ...interface{}) {} 29 | 30 | // OffはTraceメソッドの呼び出しを無視するTracerを返します。 31 | func Off() Tracer { 32 | return &nilTracer{} 33 | } 34 | -------------------------------------------------------------------------------- /chapter1/trace/tracer_test.go: -------------------------------------------------------------------------------- 1 | package trace 2 | 3 | import ( 4 | "bytes" 5 | "testing" 6 | ) 7 | 8 | func TestNew(t *testing.T) { 9 | var buf bytes.Buffer 10 | tracer := New(&buf) 11 | if tracer == nil { 12 | t.Error("Newからの戻り値がnilです") 13 | } else { 14 | tracer.Trace("こんにちは、traceパッケージ") 15 | if buf.String() != "こんにちは、traceパッケージ\n" { 16 | t.Errorf("'%s'という誤った文字列が出力されました", buf.String()) 17 | } 18 | } 19 | } 20 | 21 | func TestOff(t *testing.T) { 22 | var silentTracer Tracer = Off() 23 | silentTracer.Trace("データ") 24 | } 25 | -------------------------------------------------------------------------------- /chapter2/chat/auth.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "log" 6 | "net/http" 7 | "strings" 8 | 9 | "github.com/stretchr/gomniauth" 10 | "github.com/stretchr/objx" 11 | ) 12 | 13 | type authHandler struct { 14 | next http.Handler 15 | } 16 | 17 | func (h *authHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { 18 | if _, err := r.Cookie("auth"); err == http.ErrNoCookie { 19 | // 未認証 20 | w.Header().Set("Location", "/login") 21 | w.WriteHeader(http.StatusTemporaryRedirect) 22 | } else if err != nil { 23 | // 何らかの別のエラーが発生 24 | panic(err.Error()) 25 | } else { 26 | // 成功。ラップされたハンドラを呼び出します 27 | h.next.ServeHTTP(w, r) 28 | } 29 | } 30 | func MustAuth(handler http.Handler) http.Handler { 31 | return &authHandler{next: handler} 32 | } 33 | 34 | // loginHandlerはサードパーティーへのログインの処理を受け持ちます。 35 | // パスの形式: /auth/{action}/{provider} 36 | func loginHandler(w http.ResponseWriter, r *http.Request) { 37 | segs := strings.Split(r.URL.Path, "/") 38 | action := segs[2] 39 | provider := segs[3] 40 | switch action { 41 | case "login": 42 | provider, err := gomniauth.Provider(provider) 43 | if err != nil { 44 | log.Fatalln("認証プロバイダーの取得に失敗しました:", provider, "-", err) 45 | } 46 | loginUrl, err := provider.GetBeginAuthURL(nil, nil) 47 | if err != nil { 48 | log.Fatalln("GetBeginAuthURLの呼び出し中にエラーが発生しました:", provider, "-", err) 49 | } 50 | w.Header().Set("Location", loginUrl) 51 | w.WriteHeader(http.StatusTemporaryRedirect) 52 | case "callback": 53 | provider, err := gomniauth.Provider(provider) 54 | if err != nil { 55 | log.Fatalln("認証プロバイダーの取得に失敗しました", provider, "-", err) 56 | } 57 | 58 | creds, err := 59 | provider.CompleteAuth(objx.MustFromURLQuery(r.URL.RawQuery)) 60 | if err != nil { 61 | log.Fatalln("認証を完了できませんでした", provider, "-", err) 62 | } 63 | 64 | user, err := provider.GetUser(creds) 65 | if err != nil { 66 | log.Fatalln("ユーザーの取得に失敗しました", provider, "- ", err) 67 | } 68 | 69 | authCookieValue := objx.New(map[string]interface{}{ 70 | "name": user.Name(), 71 | }).MustBase64() 72 | http.SetCookie(w, &http.Cookie{ 73 | Name: "auth", 74 | Value: authCookieValue, 75 | Path: "/"}) 76 | w.Header()["Location"] = []string{"/chat"} 77 | w.WriteHeader(http.StatusTemporaryRedirect) 78 | default: 79 | w.WriteHeader(http.StatusNotFound) 80 | fmt.Fprintf(w, "アクション%sには非対応です", action) 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /chapter2/chat/client.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "time" 5 | 6 | "github.com/gorilla/websocket" 7 | ) 8 | 9 | // clientはチャットを行っている1人のユーザーを表します。 10 | type client struct { 11 | // socketはこのクライアントのためのWebSocketです。 12 | socket *websocket.Conn 13 | // sendはメッセージが送られるチャネルです。 14 | send chan *message 15 | // roomはこのクライアントが参加しているチャットルームです。 16 | room *room 17 | // userDataはユーザーに関する情報を保持します 18 | userData map[string]interface{} 19 | } 20 | 21 | func (c *client) read() { 22 | for { 23 | var msg *message 24 | if err := c.socket.ReadJSON(&msg); err == nil { 25 | msg.When = time.Now() 26 | msg.Name = c.userData["name"].(string) 27 | c.room.forward <- msg 28 | } else { 29 | break 30 | } 31 | } 32 | c.socket.Close() 33 | } 34 | func (c *client) write() { 35 | for msg := range c.send { 36 | if err := c.socket.WriteJSON(msg); err != nil { 37 | break 38 | } 39 | } 40 | c.socket.Close() 41 | } 42 | -------------------------------------------------------------------------------- /chapter2/chat/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "flag" 5 | "html/template" 6 | "log" 7 | "net/http" 8 | "os" 9 | "path/filepath" 10 | "sync" 11 | 12 | "github.com/oreilly-japan/go-programming-blueprints/chapter2/trace" 13 | "github.com/stretchr/gomniauth" 14 | "github.com/stretchr/gomniauth/providers/facebook" 15 | "github.com/stretchr/gomniauth/providers/github" 16 | "github.com/stretchr/gomniauth/providers/google" 17 | "github.com/stretchr/objx" 18 | ) 19 | 20 | // templは1つのテンプレートを表します 21 | type templateHandler struct { 22 | once sync.Once 23 | filename string 24 | templ *template.Template 25 | } 26 | 27 | // ServeHTTPはHTTPリクエストを処理します 28 | func (t *templateHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { 29 | t.once.Do(func() { 30 | t.templ = 31 | template.Must(template.ParseFiles(filepath.Join("templates", 32 | t.filename))) 33 | }) 34 | data := map[string]interface{}{ 35 | "Host": r.Host, 36 | } 37 | if authCookie, err := r.Cookie("auth"); err == nil { 38 | data["UserData"] = objx.MustFromBase64(authCookie.Value) 39 | } 40 | t.templ.Execute(w, data) 41 | } 42 | 43 | func main() { 44 | var addr = flag.String("addr", ":8080", "アプリケーションのアドレス") 45 | flag.Parse() // フラグを解釈します 46 | // Gomniauthのセットアップ 47 | gomniauth.SetSecurityKey("セキュリティキー") 48 | gomniauth.WithProviders( 49 | facebook.New("クライアントID", "秘密の値", "http://localhost:8080/auth/callback/facebook"), 50 | github.New("クライアントID", "秘密の値", "http://localhost:8080/auth/callback/github"), 51 | google.New("クライアントID", "秘密の値", "http://localhost:8080/auth/callback/google"), 52 | ) 53 | 54 | r := newRoom() 55 | r.tracer = trace.New(os.Stdout) 56 | http.Handle("/chat", MustAuth(&templateHandler{filename: "chat.html"})) 57 | http.Handle("/login", &templateHandler{filename: "login.html"}) 58 | http.HandleFunc("/auth/", loginHandler) 59 | http.Handle("/room", r) 60 | // チャットルームを開始します 61 | go r.run() 62 | // Webサーバーを起動します 63 | log.Println("Webサーバーを開始します。ポート: ", *addr) 64 | if err := http.ListenAndServe(*addr, nil); err != nil { 65 | log.Fatal("ListenAndServe:", err) 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /chapter2/chat/message.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "time" 5 | ) 6 | 7 | // messageは1つのメッセージを表します。 8 | type message struct { 9 | Name string 10 | Message string 11 | When time.Time 12 | } 13 | -------------------------------------------------------------------------------- /chapter2/chat/room.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "log" 5 | "net/http" 6 | 7 | "github.com/gorilla/websocket" 8 | "github.com/oreilly-japan/go-programming-blueprints/chapter2/trace" 9 | "github.com/stretchr/objx" 10 | ) 11 | 12 | type room struct { 13 | // forwardは他のクライアントに転送するためのメッセージを保持するチャネルです。 14 | forward chan *message 15 | // joinはチャットルームに参加しようとしているクライアントのためのチャネルです。 16 | join chan *client 17 | // leaveはチャットルームから退室しようとしているクライアントのためのチャネルです 18 | leave chan *client 19 | // clientsには在室しているすべてのクライアントが保持されます。 20 | clients map[*client]bool 21 | // tracerはチャットルーム上で行われた操作のログを受け取ります。 22 | tracer trace.Tracer 23 | } 24 | 25 | // newRoomはすぐに利用できるチャットルームを生成して返します。 26 | func newRoom() *room { 27 | return &room{ 28 | forward: make(chan *message), 29 | join: make(chan *client), 30 | leave: make(chan *client), 31 | clients: make(map[*client]bool), 32 | tracer: trace.Off(), 33 | } 34 | } 35 | 36 | func (r *room) run() { 37 | for { 38 | select { 39 | case client := <-r.join: 40 | // 参加 41 | r.clients[client] = true 42 | r.tracer.Trace("新しいクライアントが参加しました") 43 | case client := <-r.leave: 44 | // 退室 45 | delete(r.clients, client) 46 | close(client.send) 47 | r.tracer.Trace("クライアントが退室しました") 48 | case msg := <-r.forward: 49 | r.tracer.Trace("メッセージを受信しました: ", msg.Message) 50 | // すべてのクライアントにメッセージを転送 51 | for client := range r.clients { 52 | select { 53 | case client.send <- msg: 54 | // メッセージを送信 55 | r.tracer.Trace(" -- クライアントに送信されました") 56 | default: 57 | // 送信に失敗 58 | delete(r.clients, client) 59 | close(client.send) 60 | r.tracer.Trace(" -- 送信に失敗しました。クライアントをクリーンアップします") 61 | } 62 | } 63 | } 64 | } 65 | } 66 | 67 | const ( 68 | socketBufferSize = 1024 69 | messageBufferSize = 256 70 | ) 71 | 72 | var upgrader = &websocket.Upgrader{ReadBufferSize: socketBufferSize, WriteBufferSize: socketBufferSize} 73 | 74 | func (r *room) ServeHTTP(w http.ResponseWriter, req *http.Request) { 75 | socket, err := upgrader.Upgrade(w, req, nil) 76 | if err != nil { 77 | log.Fatal("ServeHTTP:", err) 78 | return 79 | } 80 | authCookie, err := req.Cookie("auth") 81 | if err != nil { 82 | log.Fatal("クッキーの取得に失敗しました:", err) 83 | return 84 | } 85 | client := &client{ 86 | socket: socket, 87 | send: make(chan *message, messageBufferSize), 88 | room: r, 89 | userData: objx.MustFromBase64(authCookie.Value), 90 | } 91 | r.join <- client 92 | defer func() { r.leave <- client }() 93 | go client.write() 94 | client.read() 95 | } 96 | -------------------------------------------------------------------------------- /chapter2/chat/templates/chat.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | チャット 4 | 8 | 9 | 10 | 11 | WebSocketを使ったチャットアプリケーション 12 |
    13 | {{.UserData.name}}:
    14 | 15 | 16 |
    17 | 18 | 20 | 54 | 55 | 56 | 57 | -------------------------------------------------------------------------------- /chapter2/chat/templates/login.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | ログイン 4 | 6 | 7 | 8 |
    9 | 12 |
    13 |
    14 |

    チャットを行うにはサインインが必要です

    15 |
    16 |
    17 |

    サインインに使用するサービスを選んでください:

    18 | 29 |
    30 |
    31 |
    32 | 33 | 34 | -------------------------------------------------------------------------------- /chapter2/trace/tracer.go: -------------------------------------------------------------------------------- 1 | package trace 2 | 3 | import ( 4 | "fmt" 5 | "io" 6 | ) 7 | 8 | // Tracerはコード内での出来事を記録できるオブジェクトを表すインタフェースです。 9 | type Tracer interface { 10 | Trace(...interface{}) 11 | } 12 | 13 | func New(w io.Writer) Tracer { 14 | return &tracer{out: w} 15 | } 16 | 17 | type tracer struct { 18 | out io.Writer 19 | } 20 | 21 | func (t *tracer) Trace(a ...interface{}) { 22 | t.out.Write([]byte(fmt.Sprint(a...))) 23 | t.out.Write([]byte("\n")) 24 | } 25 | 26 | type nilTracer struct{} 27 | 28 | func (t *nilTracer) Trace(a ...interface{}) {} 29 | 30 | // OffはTraceメソッドの呼び出しを無視するTracerを返します。 31 | func Off() Tracer { 32 | return &nilTracer{} 33 | } 34 | -------------------------------------------------------------------------------- /chapter2/trace/tracer_test.go: -------------------------------------------------------------------------------- 1 | package trace 2 | 3 | import ( 4 | "bytes" 5 | "testing" 6 | ) 7 | 8 | func TestNew(t *testing.T) { 9 | var buf bytes.Buffer 10 | tracer := New(&buf) 11 | if tracer == nil { 12 | t.Error("Newからの戻り値がnilです") 13 | } else { 14 | tracer.Trace("こんにちは、traceパッケージ") 15 | if buf.String() != "こんにちは、traceパッケージ\n" { 16 | t.Errorf("'%s'という誤った文字列が出力されました", buf.String()) 17 | } 18 | } 19 | } 20 | 21 | func TestOff(t *testing.T) { 22 | var silentTracer Tracer = Off() 23 | silentTracer.Trace("データ") 24 | } 25 | -------------------------------------------------------------------------------- /chapter3/chat/auth.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "crypto/md5" 5 | "fmt" 6 | "io" 7 | "log" 8 | "net/http" 9 | "strings" 10 | 11 | "github.com/stretchr/gomniauth" 12 | gomniauthcommon "github.com/stretchr/gomniauth/common" 13 | "github.com/stretchr/objx" 14 | ) 15 | 16 | type ChatUser interface { 17 | UniqueID() string 18 | AvatarURL() string 19 | } 20 | type chatUser struct { 21 | gomniauthcommon.User 22 | uniqueID string 23 | } 24 | 25 | func (u chatUser) UniqueID() string { 26 | return u.uniqueID 27 | } 28 | 29 | type authHandler struct { 30 | next http.Handler 31 | } 32 | 33 | func (h *authHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { 34 | if cookie, err := r.Cookie("auth"); err == http.ErrNoCookie || cookie.Value == "" { 35 | // 未認証 36 | w.Header().Set("Location", "/login") 37 | w.WriteHeader(http.StatusTemporaryRedirect) 38 | } else if err != nil { 39 | // 何らかの別のエラーが発生 40 | panic(err.Error()) 41 | } else { 42 | // 成功。ラップされたハンドラを呼び出します 43 | h.next.ServeHTTP(w, r) 44 | } 45 | } 46 | func MustAuth(handler http.Handler) http.Handler { 47 | return &authHandler{next: handler} 48 | } 49 | 50 | // loginHandlerはサードパーティーへのログインの処理を受け持ちます。 51 | // パスの形式: /auth/{action}/{provider} 52 | func loginHandler(w http.ResponseWriter, r *http.Request) { 53 | segs := strings.Split(r.URL.Path, "/") 54 | action := segs[2] 55 | provider := segs[3] 56 | switch action { 57 | case "login": 58 | provider, err := gomniauth.Provider(provider) 59 | if err != nil { 60 | log.Fatalln("認証プロバイダーの取得に失敗しました:", provider, "-", err) 61 | } 62 | loginUrl, err := provider.GetBeginAuthURL(nil, nil) 63 | if err != nil { 64 | log.Fatalln("GetBeginAuthURLの呼び出し中にエラーが発生しました:", provider, "-", err) 65 | } 66 | w.Header().Set("Location", loginUrl) 67 | w.WriteHeader(http.StatusTemporaryRedirect) 68 | case "callback": 69 | provider, err := gomniauth.Provider(provider) 70 | if err != nil { 71 | log.Fatalln("認証プロバイダーの取得に失敗しました", provider, "-", err) 72 | } 73 | 74 | creds, err := 75 | provider.CompleteAuth(objx.MustFromURLQuery(r.URL.RawQuery)) 76 | if err != nil { 77 | log.Fatalln("認証を完了できませんでした", provider, "-", err) 78 | } 79 | 80 | user, err := provider.GetUser(creds) 81 | if err != nil { 82 | log.Fatalln("ユーザーの取得に失敗しました", provider, "- ", err) 83 | } 84 | 85 | chatUser := &chatUser{User: user} 86 | m := md5.New() 87 | io.WriteString(m, strings.ToLower(user.Name())) 88 | chatUser.uniqueID = fmt.Sprintf("%x", m.Sum(nil)) 89 | avatarURL, err := avatars.GetAvatarURL(chatUser) 90 | if err != nil { 91 | log.Fatalln("GetAvatarURLに失敗しました", "-", err) 92 | } 93 | // データを保存します 94 | authCookieValue := objx.New(map[string]interface{}{ 95 | "userid": chatUser.uniqueID, 96 | "name": user.Name(), 97 | "avatar_url": avatarURL, 98 | }).MustBase64() 99 | http.SetCookie(w, &http.Cookie{ 100 | Name: "auth", 101 | Value: authCookieValue, 102 | Path: "/"}) 103 | w.Header()["Location"] = []string{"/chat"} 104 | w.WriteHeader(http.StatusTemporaryRedirect) 105 | default: 106 | w.WriteHeader(http.StatusNotFound) 107 | fmt.Fprintf(w, "アクション%sには非対応です", action) 108 | } 109 | } 110 | -------------------------------------------------------------------------------- /chapter3/chat/avatar.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "errors" 5 | "io/ioutil" 6 | "path/filepath" 7 | ) 8 | 9 | // ErrNoAvatarはAvatarインスタンスがアバターのURLを返すことができない 10 | // 場合に発生するエラーです。 11 | var ErrNoAvatarURL = errors.New("chat: アバターのURLを取得できません。") 12 | 13 | // Avatarはユーザーのプロフィール画像を表す型です。 14 | type Avatar interface { 15 | // GetAvatarURLは指定されたクライアントのアバターのURLを返します。 16 | // 問題が発生した場合にはエラーを返します。特に、URLを取得できなかった 17 | // 場合にはErrNoAvatarURLを返します。 18 | GetAvatarURL(ChatUser) (string, error) 19 | } 20 | 21 | type TryAvatars []Avatar 22 | 23 | func (a TryAvatars) GetAvatarURL(u ChatUser) (string, error) { 24 | for _, avatar := range a { 25 | if url, err := avatar.GetAvatarURL(u); err == nil { 26 | return url, nil 27 | } 28 | } 29 | return "", ErrNoAvatarURL 30 | } 31 | 32 | type AuthAvatar struct{} 33 | 34 | var UseAuthAvatar AuthAvatar 35 | 36 | func (_ AuthAvatar) GetAvatarURL(u ChatUser) (string, error) { 37 | url := u.AvatarURL() 38 | if url != "" { 39 | return url, nil 40 | } 41 | return "", ErrNoAvatarURL 42 | } 43 | 44 | type GravatarAvatar struct{} 45 | 46 | var UseGravatar GravatarAvatar 47 | 48 | func (_ GravatarAvatar) GetAvatarURL(u ChatUser) (string, error) { 49 | return "//www.gravatar.com/avatar/" + u.UniqueID(), nil 50 | } 51 | 52 | type FileSystemAvatar struct{} 53 | 54 | var UseFileSystemAvatar FileSystemAvatar 55 | 56 | func (_ FileSystemAvatar) GetAvatarURL(u ChatUser) (string, error) { 57 | if files, err := ioutil.ReadDir("avatars"); err == nil { 58 | for _, file := range files { 59 | if file.IsDir() { 60 | continue 61 | } 62 | if match, _ := filepath.Match(u.UniqueID()+"*", file.Name()); match { 63 | return "/avatars/" + file.Name(), nil 64 | } 65 | } 66 | } 67 | return "", ErrNoAvatarURL 68 | } 69 | -------------------------------------------------------------------------------- /chapter3/chat/avatar_test.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "io/ioutil" 5 | "os" 6 | "path/filepath" 7 | "testing" 8 | 9 | gomniauthtest "github.com/stretchr/gomniauth/test" 10 | ) 11 | 12 | func TestAuthAvatar(t *testing.T) { 13 | var authAvatar AuthAvatar 14 | testUser := &gomniauthtest.TestUser{} 15 | testUser.On("AvatarURL").Return("", ErrNoAvatarURL) 16 | testChatUser := &chatUser{User: testUser} 17 | url, err := authAvatar.GetAvatarURL(testChatUser) 18 | if err != ErrNoAvatarURL { 19 | t.Error("値が存在しない場合、AuthAvatar.GetAvatarURLは" + 20 | "ErrNoAvatarURLを返すべきです") 21 | } 22 | // 値をセットします 23 | testUrl := "http://url-to-avatar/" 24 | testUser = &gomniauthtest.TestUser{} 25 | testChatUser.User = testUser 26 | testUser.On("AvatarURL").Return(testUrl, nil) 27 | url, err = authAvatar.GetAvatarURL(testChatUser) 28 | if err != nil { 29 | t.Error("値が存在する場合、AuthAvatar.GetAvatarURLは" + 30 | "エラーを返すべきではありません") 31 | } else { 32 | if url != testUrl { 33 | t.Error("AuthAvatar.GetAvatarURLは正しいURLを返すべきです") 34 | } 35 | } 36 | } 37 | 38 | func TestGravatarAvatar(t *testing.T) { 39 | var gravatarAvatar GravatarAvatar 40 | user := &chatUser{uniqueID: "abc"} 41 | url, err := gravatarAvatar.GetAvatarURL(user) 42 | if err != nil { 43 | t.Error("GravatarAvitar.GetAvatarURLはエラーを返すべきではありません") 44 | } 45 | if url != "//www.gravatar.com/avatar/abc" { 46 | t.Errorf("GravatarAvitar.GetAvatarURLが%sという誤った値を返しました", url) 47 | } 48 | } 49 | 50 | func TestFileSystemAvatar(t *testing.T) { 51 | 52 | // テスト用のアバターのファイルを生成します 53 | filename := filepath.Join("avatars", "abc.jpg") 54 | ioutil.WriteFile(filename, []byte{}, 0777) 55 | defer func() { os.Remove(filename) }() 56 | 57 | var fileSystemAvatar FileSystemAvatar 58 | user := &chatUser{uniqueID: "abc"} 59 | url, err := fileSystemAvatar.GetAvatarURL(user) 60 | if err != nil { 61 | t.Error("FileSystemAvatar.GetAvatarURLはエラーを返すべきではありません") 62 | } 63 | if url != "/avatars/abc.jpg" { 64 | t.Errorf("FileSystemAvatar.GetAvatarURLが%sという誤った値を返しました", url) 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /chapter3/chat/avatars/.keepme: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oreilly-japan/go-programming-blueprints/fc204afa474490efb10b7ad8d1928b66848f02e2/chapter3/chat/avatars/.keepme -------------------------------------------------------------------------------- /chapter3/chat/client.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "time" 5 | 6 | "github.com/gorilla/websocket" 7 | ) 8 | 9 | // clientはチャットを行っている1人のユーザーを表します。 10 | type client struct { 11 | // socketはこのクライアントのためのWebSocketです。 12 | socket *websocket.Conn 13 | // sendはメッセージが送られるチャネルです。 14 | send chan *message 15 | // roomはこのクライアントが参加しているチャットルームです。 16 | room *room 17 | // userDataはユーザーに関する情報を保持します 18 | userData map[string]interface{} 19 | } 20 | 21 | func (c *client) read() { 22 | for { 23 | var msg *message 24 | if err := c.socket.ReadJSON(&msg); err == nil { 25 | msg.When = time.Now() 26 | msg.Name = c.userData["name"].(string) 27 | if avatarURL, ok := c.userData["avatar_url"]; ok { 28 | msg.AvatarURL = avatarURL.(string) 29 | } 30 | c.room.forward <- msg 31 | } else { 32 | break 33 | } 34 | } 35 | c.socket.Close() 36 | } 37 | func (c *client) write() { 38 | for msg := range c.send { 39 | if err := c.socket.WriteJSON(msg); err != nil { 40 | break 41 | } 42 | } 43 | c.socket.Close() 44 | } 45 | -------------------------------------------------------------------------------- /chapter3/chat/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "flag" 5 | "html/template" 6 | "log" 7 | "net/http" 8 | "os" 9 | "path/filepath" 10 | "sync" 11 | 12 | "github.com/oreilly-japan/go-programming-blueprints/chapter3/trace" 13 | "github.com/stretchr/gomniauth" 14 | "github.com/stretchr/gomniauth/providers/facebook" 15 | "github.com/stretchr/gomniauth/providers/github" 16 | "github.com/stretchr/gomniauth/providers/google" 17 | "github.com/stretchr/objx" 18 | ) 19 | 20 | // 現在アクティブなAvatarの実装 21 | var avatars Avatar = TryAvatars{ 22 | UseFileSystemAvatar, 23 | UseAuthAvatar, 24 | UseGravatar} 25 | 26 | // templは1つのテンプレートを表します 27 | type templateHandler struct { 28 | once sync.Once 29 | filename string 30 | templ *template.Template 31 | } 32 | 33 | // ServeHTTPはHTTPリクエストを処理します 34 | func (t *templateHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { 35 | t.once.Do(func() { 36 | t.templ = 37 | template.Must(template.ParseFiles(filepath.Join("templates", 38 | t.filename))) 39 | }) 40 | data := map[string]interface{}{ 41 | "Host": r.Host, 42 | } 43 | if authCookie, err := r.Cookie("auth"); err == nil { 44 | data["UserData"] = objx.MustFromBase64(authCookie.Value) 45 | } 46 | t.templ.Execute(w, data) 47 | } 48 | 49 | func main() { 50 | var addr = flag.String("addr", ":8080", "アプリケーションのアドレス") 51 | flag.Parse() // フラグを解釈します 52 | // Gomniauthのセットアップ 53 | gomniauth.SetSecurityKey("セキュリティキー") 54 | gomniauth.WithProviders( 55 | facebook.New("クライアントID", "秘密の値", "http://localhost:8080/auth/callback/facebook"), 56 | github.New("クライアントID", "秘密の値", "http://localhost:8080/auth/callback/github"), 57 | google.New("クライアントID", "秘密の値", "http://localhost:8080/auth/callback/google"), 58 | ) 59 | 60 | r := newRoom() 61 | r.tracer = trace.New(os.Stdout) 62 | http.Handle("/chat", MustAuth(&templateHandler{filename: "chat.html"})) 63 | http.Handle("/login", &templateHandler{filename: "login.html"}) 64 | http.HandleFunc("/auth/", loginHandler) 65 | http.HandleFunc("/logout", func(w http.ResponseWriter, r *http.Request) { 66 | http.SetCookie(w, &http.Cookie{ 67 | Name: "auth", 68 | Value: "", 69 | Path: "/", 70 | MaxAge: -1, 71 | }) 72 | w.Header()["Location"] = []string{"/chat"} 73 | w.WriteHeader(http.StatusTemporaryRedirect) 74 | }) 75 | http.Handle("/upload", &templateHandler{filename: "upload.html"}) 76 | http.HandleFunc("/uploader", uploaderHandler) 77 | http.Handle("/avatars/", 78 | http.StripPrefix("/avatars/", 79 | http.FileServer(http.Dir("./avatars")))) 80 | 81 | http.Handle("/room", r) 82 | // チャットルームを開始します 83 | go r.run() 84 | // Webサーバーを起動します 85 | log.Println("Webサーバーを開始します。ポート: ", *addr) 86 | if err := http.ListenAndServe(*addr, nil); err != nil { 87 | log.Fatal("ListenAndServe:", err) 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /chapter3/chat/message.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "time" 5 | ) 6 | 7 | // messageは1つのメッセージを表します。 8 | type message struct { 9 | Name string 10 | Message string 11 | When time.Time 12 | AvatarURL string 13 | } 14 | -------------------------------------------------------------------------------- /chapter3/chat/room.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "log" 5 | "net/http" 6 | 7 | "github.com/gorilla/websocket" 8 | "github.com/oreilly-japan/go-programming-blueprints/chapter3/trace" 9 | "github.com/stretchr/objx" 10 | ) 11 | 12 | type room struct { 13 | // forwardは他のクライアントに転送するためのメッセージを保持するチャネルです。 14 | forward chan *message 15 | // joinはチャットルームに参加しようとしているクライアントのためのチャネルです。 16 | join chan *client 17 | // leaveはチャットルームから退室しようとしているクライアントのためのチャネルです 18 | leave chan *client 19 | // clientsには在室しているすべてのクライアントが保持されます。 20 | clients map[*client]bool 21 | // tracerはチャットルーム上で行われた操作のログを受け取ります。 22 | tracer trace.Tracer 23 | } 24 | 25 | // newRoomはすぐに利用できるチャットルームを生成して返します。 26 | func newRoom() *room { 27 | return &room{ 28 | forward: make(chan *message), 29 | join: make(chan *client), 30 | leave: make(chan *client), 31 | clients: make(map[*client]bool), 32 | tracer: trace.Off(), 33 | } 34 | } 35 | 36 | func (r *room) run() { 37 | for { 38 | select { 39 | case client := <-r.join: 40 | // 参加 41 | r.clients[client] = true 42 | r.tracer.Trace("新しいクライアントが参加しました") 43 | case client := <-r.leave: 44 | // 退室 45 | delete(r.clients, client) 46 | close(client.send) 47 | r.tracer.Trace("クライアントが退室しました") 48 | case msg := <-r.forward: 49 | r.tracer.Trace("メッセージを受信しました: ", msg.Message) 50 | // すべてのクライアントにメッセージを転送 51 | for client := range r.clients { 52 | select { 53 | case client.send <- msg: 54 | // メッセージを送信 55 | r.tracer.Trace(" -- クライアントに送信されました") 56 | default: 57 | // 送信に失敗 58 | delete(r.clients, client) 59 | close(client.send) 60 | r.tracer.Trace(" -- 送信に失敗しました。クライアントをクリーンアップします") 61 | } 62 | } 63 | } 64 | } 65 | } 66 | 67 | const ( 68 | socketBufferSize = 1024 69 | messageBufferSize = 256 70 | ) 71 | 72 | var upgrader = &websocket.Upgrader{ReadBufferSize: socketBufferSize, WriteBufferSize: socketBufferSize} 73 | 74 | func (r *room) ServeHTTP(w http.ResponseWriter, req *http.Request) { 75 | socket, err := upgrader.Upgrade(w, req, nil) 76 | if err != nil { 77 | log.Fatal("ServeHTTP:", err) 78 | return 79 | } 80 | authCookie, err := req.Cookie("auth") 81 | if err != nil { 82 | log.Fatal("クッキーの取得に失敗しました:", err) 83 | return 84 | } 85 | client := &client{ 86 | socket: socket, 87 | send: make(chan *message, messageBufferSize), 88 | room: r, 89 | userData: objx.MustFromBase64(authCookie.Value), 90 | } 91 | r.join <- client 92 | defer func() { r.leave <- client }() 93 | go client.write() 94 | client.read() 95 | } 96 | -------------------------------------------------------------------------------- /chapter3/chat/templates/chat.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | チャット 4 | 6 | 11 | 12 | 13 |
    14 |
    15 |
    16 |
      17 |
      18 |
      19 |
      20 |
      21 | 22 | またはサインアウト 23 | 24 |
      25 | 26 |
      27 |
      28 | 29 | 31 | 69 | 70 | 71 | 72 | -------------------------------------------------------------------------------- /chapter3/chat/templates/login.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | ログイン 4 | 6 | 7 | 8 |
      9 | 12 |
      13 |
      14 |

      チャットを行うにはサインインが必要です

      15 |
      16 |
      17 |

      サインインに使用するサービスを選んでください:

      18 | 29 |
      30 |
      31 |
      32 | 33 | 34 | -------------------------------------------------------------------------------- /chapter3/chat/templates/upload.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | アップロード 4 | 6 | 7 | 8 |
      9 | 12 |
      14 | 15 |
      16 | 17 | 18 |
      19 | 20 |
      21 |
      22 | 23 | 24 | -------------------------------------------------------------------------------- /chapter3/chat/upload.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "io" 5 | "io/ioutil" 6 | "net/http" 7 | "path/filepath" 8 | ) 9 | 10 | func uploaderHandler(w http.ResponseWriter, req *http.Request) { 11 | userId := req.FormValue("userid") 12 | file, header, err := req.FormFile("avatarFile") 13 | if err != nil { 14 | io.WriteString(w, err.Error()) 15 | return 16 | } 17 | defer file.Close() 18 | data, err := ioutil.ReadAll(file) 19 | if err != nil { 20 | io.WriteString(w, err.Error()) 21 | return 22 | } 23 | filename := filepath.Join("avatars", userId+filepath.Ext(header.Filename)) 24 | err = ioutil.WriteFile(filename, data, 0777) 25 | if err != nil { 26 | io.WriteString(w, err.Error()) 27 | return 28 | } 29 | io.WriteString(w, "成功") 30 | } 31 | -------------------------------------------------------------------------------- /chapter3/trace/tracer.go: -------------------------------------------------------------------------------- 1 | package trace 2 | 3 | import ( 4 | "fmt" 5 | "io" 6 | ) 7 | 8 | // Tracerはコード内での出来事を記録できるオブジェクトを表すインタフェースです。 9 | type Tracer interface { 10 | Trace(...interface{}) 11 | } 12 | 13 | func New(w io.Writer) Tracer { 14 | return &tracer{out: w} 15 | } 16 | 17 | type tracer struct { 18 | out io.Writer 19 | } 20 | 21 | func (t *tracer) Trace(a ...interface{}) { 22 | t.out.Write([]byte(fmt.Sprint(a...))) 23 | t.out.Write([]byte("\n")) 24 | } 25 | 26 | type nilTracer struct{} 27 | 28 | func (t *nilTracer) Trace(a ...interface{}) {} 29 | 30 | // OffはTraceメソッドの呼び出しを無視するTracerを返します。 31 | func Off() Tracer { 32 | return &nilTracer{} 33 | } 34 | -------------------------------------------------------------------------------- /chapter3/trace/tracer_test.go: -------------------------------------------------------------------------------- 1 | package trace 2 | 3 | import ( 4 | "bytes" 5 | "testing" 6 | ) 7 | 8 | func TestNew(t *testing.T) { 9 | var buf bytes.Buffer 10 | tracer := New(&buf) 11 | if tracer == nil { 12 | t.Error("Newからの戻り値がnilです") 13 | } else { 14 | tracer.Trace("こんにちは、traceパッケージ") 15 | if buf.String() != "こんにちは、traceパッケージ\n" { 16 | t.Errorf("'%s'という誤った文字列が出力されました", buf.String()) 17 | } 18 | } 19 | } 20 | 21 | func TestOff(t *testing.T) { 22 | var silentTracer Tracer = Off() 23 | silentTracer.Trace("データ") 24 | } 25 | -------------------------------------------------------------------------------- /chapter4/available/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "bufio" 5 | "fmt" 6 | "log" 7 | "net" 8 | "os" 9 | "strings" 10 | "time" 11 | ) 12 | 13 | func exists(domain string) (bool, error) { 14 | const whoisServer string = "com.whois-servers.net" 15 | conn, err := net.Dial("tcp", whoisServer+":43") 16 | if err != nil { 17 | return false, err 18 | } 19 | defer conn.Close() 20 | conn.Write([]byte(domain + "\r\n")) 21 | scanner := bufio.NewScanner(conn) 22 | for scanner.Scan() { 23 | if strings.Contains(strings.ToLower(scanner.Text()), "no match") { 24 | return false, nil 25 | } 26 | } 27 | return true, nil 28 | } 29 | 30 | var marks = map[bool]string{true: "○", false: "×"} 31 | 32 | func main() { 33 | s := bufio.NewScanner(os.Stdin) 34 | for s.Scan() { 35 | domain := s.Text() 36 | fmt.Print(domain, " ") 37 | exist, err := exists(domain) 38 | if err != nil { 39 | log.Fatalln(err) 40 | } 41 | fmt.Println(marks[!exist]) 42 | time.Sleep(1 * time.Second) 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /chapter4/coolify/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "bufio" 5 | "fmt" 6 | "math/rand" 7 | "os" 8 | "time" 9 | ) 10 | 11 | const ( 12 | duplicateVowel bool = true 13 | removeVowel bool = false 14 | ) 15 | 16 | func randBool() bool { 17 | return rand.Intn(2) == 0 18 | } 19 | func main() { 20 | rand.Seed(time.Now().UTC().UnixNano()) 21 | s := bufio.NewScanner(os.Stdin) 22 | for s.Scan() { 23 | word := []byte(s.Text()) 24 | if randBool() { 25 | var vI int = -1 26 | for i, char := range word { 27 | switch char { 28 | case 'a', 'e', 'i', 'o', 'u', 'A', 'E', 'I', 'O', 'U': 29 | if randBool() { 30 | vI = i 31 | } 32 | } 33 | } 34 | if vI >= 0 { 35 | switch randBool() { 36 | case duplicateVowel: 37 | word = append(word[:vI+1], word[vI:]...) 38 | case removeVowel: 39 | word = append(word[:vI], word[vI+1:]...) 40 | } 41 | } 42 | } 43 | fmt.Println(string(word)) 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /chapter4/domainfinder/build.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | echo domainfinderをビルドします... 3 | go build -o domainfinder 4 | echo synonymsをビルドします... 5 | cd ../synonyms 6 | go build -o ../domainfinder/lib/synonyms 7 | echo availableをビルドします... 8 | cd ../available 9 | go build -o ../domainfinder/lib/available 10 | echo sprinkleをビルドします... 11 | cd ../sprinkle 12 | go build -o ../domainfinder/lib/sprinkle 13 | echo coolifyをビルドします... 14 | cd ../coolify 15 | go build -o ../domainfinder/lib/coolify 16 | echo domainifyをビルドします... 17 | cd ../domainify 18 | go build -o ../domainfinder/lib/domainify 19 | echo 完了 20 | -------------------------------------------------------------------------------- /chapter4/domainfinder/lib/.keepme: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oreilly-japan/go-programming-blueprints/fc204afa474490efb10b7ad8d1928b66848f02e2/chapter4/domainfinder/lib/.keepme -------------------------------------------------------------------------------- /chapter4/domainfinder/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "log" 5 | "os" 6 | "os/exec" 7 | ) 8 | 9 | var cmdChain = []*exec.Cmd{ 10 | exec.Command("lib/synonyms"), 11 | exec.Command("lib/sprinkle"), 12 | exec.Command("lib/coolify"), 13 | exec.Command("lib/domainify"), 14 | exec.Command("lib/available"), 15 | } 16 | 17 | func main() { 18 | cmdChain[0].Stdin = os.Stdin 19 | cmdChain[len(cmdChain)-1].Stdout = os.Stdout 20 | 21 | for i := 0; i < len(cmdChain)-1; i++ { 22 | thisCmd := cmdChain[i] 23 | nextCmd := cmdChain[i+1] 24 | stdout, err := thisCmd.StdoutPipe() 25 | if err != nil { 26 | log.Panicln(err) 27 | } 28 | nextCmd.Stdin = stdout 29 | } 30 | 31 | for _, cmd := range cmdChain { 32 | if err := cmd.Start(); err != nil { 33 | log.Panicln(err) 34 | } else { 35 | defer cmd.Process.Kill() 36 | } 37 | } 38 | 39 | for _, cmd := range cmdChain { 40 | if err := cmd.Wait(); err != nil { 41 | log.Panicln(err) 42 | } 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /chapter4/domainify/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "bufio" 5 | "fmt" 6 | "math/rand" 7 | "os" 8 | "strings" 9 | "time" 10 | "unicode" 11 | ) 12 | 13 | var tlds = []string{"com", "net"} 14 | 15 | const allowedChars = "abcdefghijklmnopqrstuvwxyz0123456789_-" 16 | 17 | func main() { 18 | rand.Seed(time.Now().UTC().UnixNano()) 19 | s := bufio.NewScanner(os.Stdin) 20 | for s.Scan() { 21 | text := strings.ToLower(s.Text()) 22 | var newText []rune 23 | for _, r := range text { 24 | if unicode.IsSpace(r) { 25 | r = '-' 26 | } 27 | if !strings.ContainsRune(allowedChars, r) { 28 | continue 29 | } 30 | newText = append(newText, r) 31 | } 32 | fmt.Println(string(newText) + "." + 33 | tlds[rand.Intn(len(tlds))]) 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /chapter4/sprinkle/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "bufio" 5 | "fmt" 6 | "math/rand" 7 | "os" 8 | "strings" 9 | "time" 10 | ) 11 | 12 | const otherWord = "*" 13 | 14 | var transforms = []string{ 15 | otherWord, 16 | otherWord, 17 | otherWord, 18 | otherWord, 19 | otherWord + "app", 20 | otherWord + "site", 21 | otherWord + "time", 22 | "get" + otherWord, 23 | "go" + otherWord, 24 | "lets " + otherWord, 25 | } 26 | 27 | func main() { 28 | rand.Seed(time.Now().UTC().UnixNano()) 29 | s := bufio.NewScanner(os.Stdin) 30 | for s.Scan() { 31 | t := transforms[rand.Intn(len(transforms))] 32 | fmt.Println(strings.Replace(t, otherWord, s.Text(), -1)) 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /chapter4/synonyms/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "bufio" 5 | "fmt" 6 | "log" 7 | "os" 8 | 9 | "github.com/oreilly-japan/go-programming-blueprints/chapter4/thesaurus" 10 | ) 11 | 12 | func main() { 13 | apiKey := os.Getenv("BHT_APIKEY") 14 | thesaurus := &thesaurus.BigHuge{APIKey: apiKey} 15 | s := bufio.NewScanner(os.Stdin) 16 | for s.Scan() { 17 | word := s.Text() 18 | syns, err := thesaurus.Synonyms(word) 19 | if err != nil { 20 | log.Fatalf("%qの類語検索に失敗しました: %v\n", word, err) 21 | } 22 | if len(syns) == 0 { 23 | log.Fatalf("%qに類語はありませんでした\n") 24 | } 25 | for _, syn := range syns { 26 | fmt.Println(syn) 27 | } 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /chapter4/thesaurus/bighuge.go: -------------------------------------------------------------------------------- 1 | package thesaurus 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "net/http" 7 | ) 8 | 9 | type BigHuge struct { 10 | APIKey string 11 | } 12 | type synonyms struct { 13 | Noun *words `json:"noun"` 14 | Verb *words `json:"verb"` 15 | } 16 | type words struct { 17 | Syn []string `json:"syn"` 18 | } 19 | 20 | func (b *BigHuge) Synonyms(term string) ([]string, error) { 21 | var syns []string 22 | response, err := http.Get("http://words.bighugelabs.com/api/2/" + 23 | b.APIKey + "/" + term + "/json") 24 | if err != nil { 25 | return syns, fmt.Errorf("bighuge: %qの類語検索に失敗しました: %v", term, err) 26 | } 27 | var data synonyms 28 | defer response.Body.Close() 29 | if err := json.NewDecoder(response.Body).Decode(&data); err != nil { 30 | return syns, err 31 | } 32 | syns = append(syns, data.Noun.Syn...) 33 | syns = append(syns, data.Verb.Syn...) 34 | return syns, nil 35 | } 36 | -------------------------------------------------------------------------------- /chapter4/thesaurus/thesaurus.go: -------------------------------------------------------------------------------- 1 | package thesaurus 2 | 3 | type Thesaurus interface { 4 | Synonyms(term string) ([]string, error) 5 | } 6 | -------------------------------------------------------------------------------- /chapter5/socialpoll/counter/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "flag" 5 | "fmt" 6 | "log" 7 | "os" 8 | "os/signal" 9 | "sync" 10 | "syscall" 11 | "time" 12 | 13 | "github.com/bitly/go-nsq" 14 | 15 | "gopkg.in/mgo.v2" 16 | "gopkg.in/mgo.v2/bson" 17 | ) 18 | 19 | var fatalErr error 20 | 21 | func fatal(e error) { 22 | fmt.Println(e) 23 | flag.PrintDefaults() 24 | fatalErr = e 25 | } 26 | 27 | const updateDuration = 1 * time.Second 28 | 29 | func main() { 30 | defer func() { 31 | if fatalErr != nil { 32 | os.Exit(1) 33 | } 34 | }() 35 | log.Println("データベースに接続します...") 36 | db, err := mgo.Dial("localhost") 37 | if err != nil { 38 | fatal(err) 39 | return 40 | } 41 | defer func() { 42 | log.Println("データベース接続を閉じます...") 43 | db.Close() 44 | }() 45 | pollData := db.DB("ballots").C("polls") 46 | 47 | var countsLock sync.Mutex 48 | var counts map[string]int 49 | 50 | log.Println("NSQに接続します...") 51 | q, err := nsq.NewConsumer("votes", "counter", nsq.NewConfig()) 52 | if err != nil { 53 | fatal(err) 54 | return 55 | } 56 | 57 | q.AddHandler(nsq.HandlerFunc(func(m *nsq.Message) error { 58 | countsLock.Lock() 59 | defer countsLock.Unlock() 60 | if counts == nil { 61 | counts = make(map[string]int) 62 | } 63 | vote := string(m.Body) 64 | counts[vote]++ 65 | return nil 66 | })) 67 | if err := q.ConnectToNSQLookupd("localhost:4161"); err != nil { 68 | fatal(err) 69 | return 70 | } 71 | 72 | log.Println("NSQ上での投票を待機します...") 73 | var updater *time.Timer 74 | updater = time.AfterFunc(updateDuration, func() { 75 | countsLock.Lock() 76 | defer countsLock.Unlock() 77 | if len(counts) == 0 { 78 | log.Println("新しい投票はありません。データベースの更新をスキップします") 79 | } else { 80 | log.Println("データベースを更新します...") 81 | log.Println(counts) 82 | ok := true 83 | for option, count := range counts { 84 | sel := bson.M{"options": bson.M{"$in": []string{option}}} 85 | up := bson.M{"$inc": bson.M{"results." + option: count}} 86 | if _, err := pollData.UpdateAll(sel, up); err != nil { 87 | log.Println("更新に失敗しました:", err) 88 | ok = false 89 | continue 90 | } 91 | counts[option] = 0 92 | } 93 | if ok { 94 | log.Println("データベースの更新が完了しました") 95 | counts = nil // 得票数をリセットします 96 | } 97 | } 98 | updater.Reset(updateDuration) 99 | }) 100 | 101 | termChan := make(chan os.Signal, 1) 102 | signal.Notify(termChan, syscall.SIGINT, syscall.SIGTERM, syscall.SIGHUP) 103 | for { 104 | select { 105 | case <-termChan: 106 | updater.Stop() 107 | q.Stop() 108 | case <-q.StopChan: 109 | // 完了しました 110 | return 111 | } 112 | } 113 | 114 | } 115 | -------------------------------------------------------------------------------- /chapter5/socialpoll/twittervotes/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "log" 5 | "os" 6 | "os/signal" 7 | "sync" 8 | "syscall" 9 | "time" 10 | 11 | "github.com/bitly/go-nsq" 12 | 13 | "gopkg.in/mgo.v2" 14 | ) 15 | 16 | var db *mgo.Session 17 | 18 | func dialdb() error { 19 | var err error 20 | log.Println("MongoDBにダイヤル中: localhost") 21 | db, err = mgo.Dial("localhost") 22 | return err 23 | } 24 | func closedb() { 25 | db.Close() 26 | log.Println("データベース接続が閉じられました") 27 | } 28 | 29 | type poll struct { 30 | Options []string 31 | } 32 | 33 | func loadOptions() ([]string, error) { 34 | var options []string 35 | iter := db.DB("ballots").C("polls").Find(nil).Iter() 36 | var p poll 37 | for iter.Next(&p) { 38 | options = append(options, p.Options...) 39 | } 40 | iter.Close() 41 | return options, iter.Err() 42 | } 43 | 44 | func publishVotes(votes <-chan string) <-chan struct{} { 45 | stopchan := make(chan struct{}, 1) 46 | pub, _ := nsq.NewProducer("localhost:4150", nsq.NewConfig()) 47 | go func() { 48 | for vote := range votes { 49 | pub.Publish("votes", []byte(vote)) // 投票内容をパブリッシュします 50 | } 51 | log.Println("Publisher: 停止中です") 52 | pub.Stop() 53 | log.Println("Publisher: 停止しました") 54 | stopchan <- struct{}{} 55 | }() 56 | return stopchan 57 | } 58 | 59 | func main() { 60 | var stoplock sync.Mutex 61 | stop := false 62 | stopChan := make(chan struct{}, 1) 63 | signalChan := make(chan os.Signal, 1) 64 | go func() { 65 | <-signalChan 66 | stoplock.Lock() 67 | stop = true 68 | stoplock.Unlock() 69 | log.Println("停止します...") 70 | stopChan <- struct{}{} 71 | closeConn() 72 | }() 73 | signal.Notify(signalChan, syscall.SIGINT, syscall.SIGTERM) 74 | if err := dialdb(); err != nil { 75 | log.Fatalln("MongoDBへのダイヤルに失敗しました:", err) 76 | } 77 | defer closedb() 78 | // 処理を開始します 79 | votes := make(chan string) // 投票結果のためのチャネル 80 | publisherStoppedChan := publishVotes(votes) 81 | twitterStoppedChan := startTwitterStream(stopChan, votes) 82 | go func() { 83 | for { 84 | time.Sleep(1 * time.Minute) 85 | closeConn() 86 | stoplock.Lock() 87 | if stop { 88 | stoplock.Unlock() 89 | break 90 | } 91 | stoplock.Unlock() 92 | } 93 | }() 94 | <-twitterStoppedChan 95 | close(votes) 96 | <-publisherStoppedChan 97 | 98 | } 99 | -------------------------------------------------------------------------------- /chapter5/socialpoll/twittervotes/twitter.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "encoding/json" 5 | "io" 6 | "log" 7 | "net" 8 | "net/http" 9 | "net/url" 10 | "strconv" 11 | "strings" 12 | "sync" 13 | "time" 14 | 15 | "github.com/garyburd/go-oauth/oauth" 16 | "github.com/joeshaw/envdecode" 17 | ) 18 | 19 | var conn net.Conn 20 | 21 | func dial(netw, addr string) (net.Conn, error) { 22 | if conn != nil { 23 | conn.Close() 24 | conn = nil 25 | } 26 | netc, err := net.DialTimeout(netw, addr, 5*time.Second) 27 | if err != nil { 28 | return nil, err 29 | } 30 | conn = netc 31 | return netc, nil 32 | } 33 | 34 | var reader io.ReadCloser 35 | 36 | func closeConn() { 37 | if conn != nil { 38 | conn.Close() 39 | } 40 | if reader != nil { 41 | reader.Close() 42 | } 43 | } 44 | 45 | var ( 46 | authClient *oauth.Client 47 | creds *oauth.Credentials 48 | ) 49 | 50 | func setupTwitterAuth() { 51 | var ts struct { 52 | ConsumerKey string `env:"SP_TWITTER_KEY,required"` 53 | ConsumerSecret string `env:"SP_TWITTER_SECRET,required"` 54 | AccessToken string `env:"SP_TWITTER_ACCESSTOKEN,required"` 55 | AccessSecret string `env:"SP_TWITTER_ACCESSSECRET,required"` 56 | } 57 | if err := envdecode.Decode(&ts); err != nil { 58 | log.Fatalln(err) 59 | } 60 | creds = &oauth.Credentials{ 61 | Token: ts.AccessToken, 62 | Secret: ts.AccessSecret, 63 | } 64 | authClient = &oauth.Client{ 65 | Credentials: oauth.Credentials{ 66 | Token: ts.ConsumerKey, 67 | Secret: ts.ConsumerSecret, 68 | }, 69 | } 70 | } 71 | 72 | var ( 73 | authSetupOnce sync.Once 74 | httpClient *http.Client 75 | ) 76 | 77 | func makeRequest(req *http.Request, params url.Values) (*http.Response, error) { 78 | authSetupOnce.Do(func() { 79 | setupTwitterAuth() 80 | httpClient = &http.Client{ 81 | Transport: &http.Transport{ 82 | Dial: dial, 83 | }, 84 | } 85 | }) 86 | formEnc := params.Encode() 87 | req.Header.Set("Content-Type", "application/x-www-form-urlencoded") 88 | req.Header.Set("Content-Length", strconv.Itoa(len(formEnc))) 89 | req.Header.Set("Authorization", 90 | authClient.AuthorizationHeader(creds, "POST", req.URL, params)) 91 | return httpClient.Do(req) 92 | } 93 | 94 | type tweet struct { 95 | Text string 96 | } 97 | 98 | func readFromTwitter(votes chan<- string) { 99 | options, err := loadOptions() 100 | if err != nil { 101 | log.Println("選択肢の読み込みに失敗しました:", err) 102 | return 103 | } 104 | u, err := url.Parse("https://stream.twitter.com/1.1/statuses/filter.json") 105 | if err != nil { 106 | log.Println("URLの解析に失敗しました:", err) 107 | return 108 | } 109 | query := make(url.Values) 110 | query.Set("track", strings.Join(options, ",")) 111 | req, err := http.NewRequest("POST", u.String(), strings.NewReader(query.Encode())) 112 | if err != nil { 113 | log.Println("検索のリクエストの作成に失敗しました:", err) 114 | return 115 | } 116 | resp, err := makeRequest(req, query) 117 | if err != nil { 118 | log.Println("検索のリクエストに失敗しました:", err) 119 | return 120 | } 121 | reader := resp.Body 122 | decoder := json.NewDecoder(reader) 123 | for { 124 | var tweet tweet 125 | if err := decoder.Decode(&tweet); err != nil { 126 | break 127 | } 128 | for _, option := range options { 129 | if strings.Contains( 130 | strings.ToLower(tweet.Text), 131 | strings.ToLower(option), 132 | ) { 133 | log.Println("投票:", option) 134 | votes <- option 135 | } 136 | } 137 | } 138 | } 139 | 140 | func startTwitterStream(stopchan <-chan struct{}, votes chan<- string) <-chan struct{} { 141 | stoppedchan := make(chan struct{}, 1) 142 | go func() { 143 | defer func() { 144 | stoppedchan <- struct{}{} 145 | }() 146 | for { 147 | select { 148 | case <-stopchan: 149 | log.Println("Twitterへの問い合わせを終了します...") 150 | return 151 | default: 152 | log.Println("Twitterに問い合わせます...") 153 | readFromTwitter(votes) 154 | log.Println(" (待機中)") 155 | time.Sleep(10 * time.Second) // 待機してから再接続します 156 | } 157 | } 158 | }() 159 | return stoppedchan 160 | } 161 | -------------------------------------------------------------------------------- /chapter6/socialpoll/api/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "flag" 5 | "log" 6 | "net/http" 7 | "time" 8 | 9 | "github.com/stretchr/graceful" 10 | 11 | "gopkg.in/mgo.v2" 12 | ) 13 | 14 | func main() { 15 | var ( 16 | addr = flag.String("addr", ":8080", "エンドポイントのアドレス") 17 | mongo = flag.String("mongo", "localhost", "MongoDBのアドレス") 18 | ) 19 | flag.Parse() 20 | log.Println("MongoDBに接続します", *mongo) 21 | db, err := mgo.Dial(*mongo) 22 | if err != nil { 23 | log.Fatalln("MongoDBへの接続に失敗しました:", err) 24 | } 25 | defer db.Close() 26 | mux := http.NewServeMux() 27 | mux.HandleFunc("/polls/", withCORS(withVars(withData(db, 28 | withAPIKey(handlePolls))))) 29 | log.Println("Webサーバーを開始します:", *addr) 30 | graceful.Run(*addr, 1*time.Second, mux) 31 | log.Println("停止します...") 32 | } 33 | 34 | func withAPIKey(fn http.HandlerFunc) http.HandlerFunc { 35 | return func(w http.ResponseWriter, r *http.Request) { 36 | if !isValidAPIKey(r.URL.Query().Get("key")) { 37 | respondErr(w, r, http.StatusUnauthorized, "不正なAPIキーです") 38 | return 39 | } 40 | fn(w, r) 41 | } 42 | } 43 | 44 | func isValidAPIKey(key string) bool { 45 | return key == "abc123" 46 | } 47 | 48 | func withData(d *mgo.Session, f http.HandlerFunc) http.HandlerFunc { 49 | return func(w http.ResponseWriter, r *http.Request) { 50 | thisDb := d.Copy() 51 | defer thisDb.Close() 52 | SetVar(r, "db", thisDb.DB("ballots")) 53 | f(w, r) 54 | } 55 | } 56 | 57 | func withVars(fn http.HandlerFunc) http.HandlerFunc { 58 | return func(w http.ResponseWriter, r *http.Request) { 59 | OpenVars(r) 60 | defer CloseVars(r) 61 | fn(w, r) 62 | } 63 | } 64 | 65 | func withCORS(fn http.HandlerFunc) http.HandlerFunc { 66 | return func(w http.ResponseWriter, r *http.Request) { 67 | w.Header().Set("Access-Control-Allow-Origin", "*") 68 | w.Header().Set("Access-Control-Expose-Headers", "Location") 69 | fn(w, r) 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /chapter6/socialpoll/api/path.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "strings" 5 | ) 6 | 7 | const PathSeparator = "/" 8 | 9 | type Path struct { 10 | Path string 11 | ID string 12 | } 13 | 14 | func NewPath(p string) *Path { 15 | var id string 16 | p = strings.Trim(p, PathSeparator) 17 | s := strings.Split(p, PathSeparator) 18 | if len(s) > 1 { 19 | id = s[len(s)-1] 20 | p = strings.Join(s[:len(s)-1], PathSeparator) 21 | } 22 | return &Path{Path: p, ID: id} 23 | } 24 | func (p *Path) HasID() bool { 25 | return len(p.ID) > 0 26 | } 27 | -------------------------------------------------------------------------------- /chapter6/socialpoll/api/polls.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "net/http" 5 | 6 | "gopkg.in/mgo.v2" 7 | "gopkg.in/mgo.v2/bson" 8 | ) 9 | 10 | type poll struct { 11 | ID bson.ObjectId `bson:"_id" json:"id"` 12 | Title string `json:"title"` 13 | Options []string `json:"options"` 14 | Results map[string]int `json:"results,omitempty"` 15 | } 16 | 17 | func handlePolls(w http.ResponseWriter, r *http.Request) { 18 | switch r.Method { 19 | case "GET": 20 | handlePollsGet(w, r) 21 | return 22 | case "POST": 23 | handlePollsPost(w, r) 24 | return 25 | case "DELETE": 26 | handlePollsDelete(w, r) 27 | return 28 | case "OPTIONS": 29 | w.Header().Add("Access-Control-Allow-Methods", "DELETE") 30 | respond(w, r, http.StatusOK, nil) 31 | return 32 | 33 | } 34 | // 未対応のHTTPメソッド 35 | respondHTTPErr(w, r, http.StatusNotFound) 36 | } 37 | 38 | func handlePollsGet(w http.ResponseWriter, r *http.Request) { 39 | db := GetVar(r, "db").(*mgo.Database) 40 | c := db.C("polls") 41 | var q *mgo.Query 42 | p := NewPath(r.URL.Path) 43 | if p.HasID() { 44 | // 特定の調査項目の詳細 45 | q = c.FindId(bson.ObjectIdHex(p.ID)) 46 | } else { 47 | // すべての調査項目のリスト 48 | q = c.Find(nil) 49 | } 50 | var result []*poll 51 | if err := q.All(&result); err != nil { 52 | respondErr(w, r, http.StatusInternalServerError, err) 53 | return 54 | } 55 | respond(w, r, http.StatusOK, &result) 56 | } 57 | 58 | func handlePollsPost(w http.ResponseWriter, r *http.Request) { 59 | db := GetVar(r, "db").(*mgo.Database) 60 | c := db.C("polls") 61 | var p poll 62 | if err := decodeBody(r, &p); err != nil { 63 | respondErr(w, r, http.StatusBadRequest, "リクエストから調査項目を読み込めません", err) 64 | return 65 | } 66 | p.ID = bson.NewObjectId() 67 | if err := c.Insert(p); err != nil { 68 | respondErr(w, r, http.StatusInternalServerError, "調査項目の格納に失敗しました", err) 69 | return 70 | } 71 | w.Header().Set("Location", "polls/"+p.ID.Hex()) 72 | respond(w, r, http.StatusCreated, nil) 73 | } 74 | 75 | func handlePollsDelete(w http.ResponseWriter, r *http.Request) { 76 | db := GetVar(r, "db").(*mgo.Database) 77 | c := db.C("polls") 78 | p := NewPath(r.URL.Path) 79 | if !p.HasID() { 80 | respondErr(w, r, http.StatusMethodNotAllowed, "すべての調査項目を削除することはできません") 81 | return 82 | } 83 | if err := c.RemoveId(bson.ObjectIdHex(p.ID)); err != nil { 84 | respondErr(w, r, http.StatusInternalServerError, "調査項目の削除に失敗しました", err) 85 | return 86 | } 87 | respond(w, r, http.StatusOK, nil) // 成功 88 | } 89 | -------------------------------------------------------------------------------- /chapter6/socialpoll/api/respond.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "net/http" 7 | ) 8 | 9 | func decodeBody(r *http.Request, v interface{}) error { 10 | defer r.Body.Close() 11 | return json.NewDecoder(r.Body).Decode(v) 12 | } 13 | func encodeBody(w http.ResponseWriter, r *http.Request, v interface{}) error { 14 | return json.NewEncoder(w).Encode(v) 15 | } 16 | 17 | func respond(w http.ResponseWriter, r *http.Request, 18 | status int, data interface{}, 19 | ) { 20 | w.WriteHeader(status) 21 | if data != nil { 22 | encodeBody(w, r, data) 23 | } 24 | } 25 | 26 | func respondErr(w http.ResponseWriter, r *http.Request, 27 | status int, args ...interface{}, 28 | ) { 29 | respond(w, r, status, map[string]interface{}{ 30 | "error": map[string]interface{}{ 31 | "message": fmt.Sprint(args...), 32 | }, 33 | }) 34 | } 35 | 36 | func respondHTTPErr(w http.ResponseWriter, r *http.Request, 37 | status int, 38 | ) { 39 | respondErr(w, r, status, http.StatusText(status)) 40 | } 41 | -------------------------------------------------------------------------------- /chapter6/socialpoll/api/vars.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "net/http" 5 | "sync" 6 | ) 7 | 8 | var ( 9 | varsLock sync.RWMutex 10 | vars map[*http.Request]map[string]interface{} 11 | ) 12 | 13 | func OpenVars(r *http.Request) { 14 | varsLock.Lock() 15 | if vars == nil { 16 | vars = map[*http.Request]map[string]interface{}{} 17 | } 18 | vars[r] = map[string]interface{}{} 19 | varsLock.Unlock() 20 | } 21 | 22 | func CloseVars(r *http.Request) { 23 | varsLock.Lock() 24 | delete(vars, r) 25 | varsLock.Unlock() 26 | } 27 | 28 | func GetVar(r *http.Request, key string) interface{} { 29 | varsLock.RLock() 30 | value := vars[r][key] 31 | varsLock.RUnlock() 32 | return value 33 | } 34 | func SetVar(r *http.Request, key string, value interface{}) { 35 | varsLock.Lock() 36 | vars[r][key] = value 37 | varsLock.Unlock() 38 | } 39 | -------------------------------------------------------------------------------- /chapter6/socialpoll/counter/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "flag" 5 | "fmt" 6 | "log" 7 | "os" 8 | "os/signal" 9 | "sync" 10 | "syscall" 11 | "time" 12 | 13 | "github.com/bitly/go-nsq" 14 | 15 | "gopkg.in/mgo.v2" 16 | "gopkg.in/mgo.v2/bson" 17 | ) 18 | 19 | var fatalErr error 20 | 21 | func fatal(e error) { 22 | fmt.Println(e) 23 | flag.PrintDefaults() 24 | fatalErr = e 25 | } 26 | 27 | const updateDuration = 1 * time.Second 28 | 29 | func main() { 30 | defer func() { 31 | if fatalErr != nil { 32 | os.Exit(1) 33 | } 34 | }() 35 | log.Println("データベースに接続します...") 36 | db, err := mgo.Dial("localhost") 37 | if err != nil { 38 | fatal(err) 39 | return 40 | } 41 | defer func() { 42 | log.Println("データベース接続を閉じます...") 43 | db.Close() 44 | }() 45 | pollData := db.DB("ballots").C("polls") 46 | 47 | var countsLock sync.Mutex 48 | var counts map[string]int 49 | 50 | log.Println("NSQに接続します...") 51 | q, err := nsq.NewConsumer("votes", "counter", nsq.NewConfig()) 52 | if err != nil { 53 | fatal(err) 54 | return 55 | } 56 | 57 | q.AddHandler(nsq.HandlerFunc(func(m *nsq.Message) error { 58 | countsLock.Lock() 59 | defer countsLock.Unlock() 60 | if counts == nil { 61 | counts = make(map[string]int) 62 | } 63 | vote := string(m.Body) 64 | counts[vote]++ 65 | return nil 66 | })) 67 | if err := q.ConnectToNSQLookupd("localhost:4161"); err != nil { 68 | fatal(err) 69 | return 70 | } 71 | 72 | log.Println("NSQ上での投票を待機します...") 73 | var updater *time.Timer 74 | updater = time.AfterFunc(updateDuration, func() { 75 | countsLock.Lock() 76 | defer countsLock.Unlock() 77 | if len(counts) == 0 { 78 | log.Println("新しい投票はありません。データベースの更新をスキップします") 79 | } else { 80 | log.Println("データベースを更新します...") 81 | log.Println(counts) 82 | ok := true 83 | for option, count := range counts { 84 | sel := bson.M{"options": bson.M{"$in": []string{option}}} 85 | up := bson.M{"$inc": bson.M{"results." + option: count}} 86 | if _, err := pollData.UpdateAll(sel, up); err != nil { 87 | log.Println("更新に失敗しました:", err) 88 | ok = false 89 | continue 90 | } 91 | counts[option] = 0 92 | } 93 | if ok { 94 | log.Println("データベースの更新が完了しました") 95 | counts = nil // 得票数をリセットします 96 | } 97 | } 98 | updater.Reset(updateDuration) 99 | }) 100 | 101 | termChan := make(chan os.Signal, 1) 102 | signal.Notify(termChan, syscall.SIGINT, syscall.SIGTERM, syscall.SIGHUP) 103 | for { 104 | select { 105 | case <-termChan: 106 | updater.Stop() 107 | q.Stop() 108 | case <-q.StopChan: 109 | // 完了しました 110 | return 111 | } 112 | } 113 | 114 | } 115 | -------------------------------------------------------------------------------- /chapter6/socialpoll/twittervotes/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "log" 5 | "os" 6 | "os/signal" 7 | "sync" 8 | "syscall" 9 | "time" 10 | 11 | "github.com/bitly/go-nsq" 12 | 13 | "gopkg.in/mgo.v2" 14 | ) 15 | 16 | var db *mgo.Session 17 | 18 | func dialdb() error { 19 | var err error 20 | log.Println("MongoDBにダイヤル中: localhost") 21 | db, err = mgo.Dial("localhost") 22 | return err 23 | } 24 | func closedb() { 25 | db.Close() 26 | log.Println("データベース接続が閉じられました") 27 | } 28 | 29 | type poll struct { 30 | Options []string 31 | } 32 | 33 | func loadOptions() ([]string, error) { 34 | var options []string 35 | iter := db.DB("ballots").C("polls").Find(nil).Iter() 36 | var p poll 37 | for iter.Next(&p) { 38 | options = append(options, p.Options...) 39 | } 40 | iter.Close() 41 | return options, iter.Err() 42 | } 43 | 44 | func publishVotes(votes <-chan string) <-chan struct{} { 45 | stopchan := make(chan struct{}, 1) 46 | pub, _ := nsq.NewProducer("localhost:4150", nsq.NewConfig()) 47 | go func() { 48 | for vote := range votes { 49 | pub.Publish("votes", []byte(vote)) // 投票内容をパブリッシュします 50 | } 51 | log.Println("Publisher: 停止中です") 52 | pub.Stop() 53 | log.Println("Publisher: 停止しました") 54 | stopchan <- struct{}{} 55 | }() 56 | return stopchan 57 | } 58 | 59 | func main() { 60 | var stoplock sync.Mutex 61 | stop := false 62 | stopChan := make(chan struct{}, 1) 63 | signalChan := make(chan os.Signal, 1) 64 | go func() { 65 | <-signalChan 66 | stoplock.Lock() 67 | stop = true 68 | stoplock.Unlock() 69 | log.Println("停止します...") 70 | stopChan <- struct{}{} 71 | closeConn() 72 | }() 73 | signal.Notify(signalChan, syscall.SIGINT, syscall.SIGTERM) 74 | if err := dialdb(); err != nil { 75 | log.Fatalln("MongoDBへのダイヤルに失敗しました:", err) 76 | } 77 | defer closedb() 78 | // 処理を開始します 79 | votes := make(chan string) // 投票結果のためのチャネル 80 | publisherStoppedChan := publishVotes(votes) 81 | twitterStoppedChan := startTwitterStream(stopChan, votes) 82 | go func() { 83 | for { 84 | time.Sleep(1 * time.Minute) 85 | closeConn() 86 | stoplock.Lock() 87 | if stop { 88 | stoplock.Unlock() 89 | break 90 | } 91 | stoplock.Unlock() 92 | } 93 | }() 94 | <-twitterStoppedChan 95 | close(votes) 96 | <-publisherStoppedChan 97 | 98 | } 99 | -------------------------------------------------------------------------------- /chapter6/socialpoll/twittervotes/twitter.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "encoding/json" 5 | "io" 6 | "log" 7 | "net" 8 | "net/http" 9 | "net/url" 10 | "strconv" 11 | "strings" 12 | "sync" 13 | "time" 14 | 15 | "github.com/garyburd/go-oauth/oauth" 16 | "github.com/joeshaw/envdecode" 17 | ) 18 | 19 | var conn net.Conn 20 | 21 | func dial(netw, addr string) (net.Conn, error) { 22 | if conn != nil { 23 | conn.Close() 24 | conn = nil 25 | } 26 | netc, err := net.DialTimeout(netw, addr, 5*time.Second) 27 | if err != nil { 28 | return nil, err 29 | } 30 | conn = netc 31 | return netc, nil 32 | } 33 | 34 | var reader io.ReadCloser 35 | 36 | func closeConn() { 37 | if conn != nil { 38 | conn.Close() 39 | } 40 | if reader != nil { 41 | reader.Close() 42 | } 43 | } 44 | 45 | var ( 46 | authClient *oauth.Client 47 | creds *oauth.Credentials 48 | ) 49 | 50 | func setupTwitterAuth() { 51 | var ts struct { 52 | ConsumerKey string `env:"SP_TWITTER_KEY,required"` 53 | ConsumerSecret string `env:"SP_TWITTER_SECRET,required"` 54 | AccessToken string `env:"SP_TWITTER_ACCESSTOKEN,required"` 55 | AccessSecret string `env:"SP_TWITTER_ACCESSSECRET,required"` 56 | } 57 | if err := envdecode.Decode(&ts); err != nil { 58 | log.Fatalln(err) 59 | } 60 | creds = &oauth.Credentials{ 61 | Token: ts.AccessToken, 62 | Secret: ts.AccessSecret, 63 | } 64 | authClient = &oauth.Client{ 65 | Credentials: oauth.Credentials{ 66 | Token: ts.ConsumerKey, 67 | Secret: ts.ConsumerSecret, 68 | }, 69 | } 70 | } 71 | 72 | var ( 73 | authSetupOnce sync.Once 74 | httpClient *http.Client 75 | ) 76 | 77 | func makeRequest(req *http.Request, params url.Values) (*http.Response, error) { 78 | authSetupOnce.Do(func() { 79 | setupTwitterAuth() 80 | httpClient = &http.Client{ 81 | Transport: &http.Transport{ 82 | Dial: dial, 83 | }, 84 | } 85 | }) 86 | formEnc := params.Encode() 87 | req.Header.Set("Content-Type", "application/x-www-form-urlencoded") 88 | req.Header.Set("Content-Length", strconv.Itoa(len(formEnc))) 89 | req.Header.Set("Authorization", 90 | authClient.AuthorizationHeader(creds, "POST", req.URL, params)) 91 | return httpClient.Do(req) 92 | } 93 | 94 | type tweet struct { 95 | Text string 96 | } 97 | 98 | func readFromTwitter(votes chan<- string) { 99 | options, err := loadOptions() 100 | if err != nil { 101 | log.Println("選択肢の読み込みに失敗しました:", err) 102 | return 103 | } 104 | u, err := url.Parse("https://stream.twitter.com/1.1/statuses/filter.json") 105 | if err != nil { 106 | log.Println("URLの解析に失敗しました:", err) 107 | return 108 | } 109 | query := make(url.Values) 110 | query.Set("track", strings.Join(options, ",")) 111 | req, err := http.NewRequest("POST", u.String(), strings.NewReader(query.Encode())) 112 | if err != nil { 113 | log.Println("検索のリクエストの作成に失敗しました:", err) 114 | return 115 | } 116 | resp, err := makeRequest(req, query) 117 | if err != nil { 118 | log.Println("検索のリクエストに失敗しました:", err) 119 | return 120 | } 121 | reader := resp.Body 122 | decoder := json.NewDecoder(reader) 123 | for { 124 | var tweet tweet 125 | if err := decoder.Decode(&tweet); err != nil { 126 | break 127 | } 128 | for _, option := range options { 129 | if strings.Contains( 130 | strings.ToLower(tweet.Text), 131 | strings.ToLower(option), 132 | ) { 133 | log.Println("投票:", option) 134 | votes <- option 135 | } 136 | } 137 | } 138 | } 139 | 140 | func startTwitterStream(stopchan <-chan struct{}, votes chan<- string) <-chan struct{} { 141 | stoppedchan := make(chan struct{}, 1) 142 | go func() { 143 | defer func() { 144 | stoppedchan <- struct{}{} 145 | }() 146 | for { 147 | select { 148 | case <-stopchan: 149 | log.Println("Twitterへの問い合わせを終了します...") 150 | return 151 | default: 152 | log.Println("Twitterに問い合わせます...") 153 | readFromTwitter(votes) 154 | log.Println(" (待機中)") 155 | time.Sleep(10 * time.Second) // 待機してから再接続します 156 | } 157 | } 158 | }() 159 | return stoppedchan 160 | } 161 | -------------------------------------------------------------------------------- /chapter6/socialpoll/web/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "flag" 5 | "log" 6 | "net/http" 7 | ) 8 | 9 | func main() { 10 | var addr = flag.String("addr", ":8081", "Webサイトのアドレス") 11 | flag.Parse() 12 | mux := http.NewServeMux() 13 | mux.Handle("/", http.StripPrefix("/", 14 | http.FileServer(http.Dir("public")))) 15 | log.Println("Webサイトのアドレス:", *addr) 16 | http.ListenAndServe(*addr, mux) 17 | } 18 | -------------------------------------------------------------------------------- /chapter6/socialpoll/web/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 調査項目のリスト 5 | 7 | 8 | 9 |
      10 |
      11 |
      12 |

      調査項目のリスト

      13 | 14 | 新規作成 15 |
      16 |
      17 |
      18 | 20 | 22 | 45 | 46 | 47 | -------------------------------------------------------------------------------- /chapter6/socialpoll/web/public/new.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 調査項目の作成 5 | 7 | 8 | 9 | 12 | 15 |
      16 |
      17 |
      18 |

      調査項目の作成

      19 |
      20 | 21 | 23 |
      24 |
      25 | 26 | 28 |

      (カンマで区切って入力)

      29 |
      30 | または キャンセル 31 |
      32 |
      33 |
      34 | 56 | 57 | 58 | -------------------------------------------------------------------------------- /chapter6/socialpoll/web/public/view.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 調査項目の詳細 5 | 7 | 8 | 9 |
      10 |
      11 |
      12 |

      ...

      13 | 14 |
      15 |
      16 | 17 |
      18 |
      19 |
      20 |
      21 | 22 | 24 | 26 | 79 | 80 | 81 | 82 | -------------------------------------------------------------------------------- /chapter7/meander/cmd/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "encoding/json" 5 | "net/http" 6 | "os" 7 | "runtime" 8 | "strconv" 9 | "strings" 10 | 11 | "github.com/oreilly-japan/go-programming-blueprints/chapter7/meander" 12 | ) 13 | 14 | func main() { 15 | runtime.GOMAXPROCS(runtime.NumCPU()) 16 | meander.APIKey = os.Getenv("GOOGLE_PLACES_API_KEY") 17 | http.HandleFunc("/journeys", cors(func(w http.ResponseWriter, r *http.Request) { 18 | respond(w, r, meander.Journeys) 19 | })) 20 | http.HandleFunc("/recommendations", cors(func( 21 | w http.ResponseWriter, r *http.Request) { 22 | q := &meander.Query{ 23 | Journey: strings.Split(r.URL.Query().Get("journey"), "|"), 24 | } 25 | q.Lat, _ = strconv.ParseFloat(r.URL.Query().Get("lat"), 64) 26 | q.Lng, _ = strconv.ParseFloat(r.URL.Query().Get("lng"), 64) 27 | q.Radius, _ = strconv.Atoi(r.URL.Query().Get("radius")) 28 | q.CostRangeStr = r.URL.Query().Get("cost") 29 | places := q.Run() 30 | respond(w, r, places) 31 | })) 32 | 33 | http.ListenAndServe(":8080", http.DefaultServeMux) 34 | } 35 | func respond(w http.ResponseWriter, r *http.Request, data []interface{}) error { 36 | publicData := make([]interface{}, len(data)) 37 | for i, d := range data { 38 | publicData[i] = meander.Public(d) 39 | } 40 | return json.NewEncoder(w).Encode(publicData) 41 | } 42 | 43 | func cors(f http.HandlerFunc) http.HandlerFunc { 44 | return func(w http.ResponseWriter, r *http.Request) { 45 | w.Header().Set("Access-Control-Allow-Origin", "*") 46 | f(w, r) 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /chapter7/meander/cost_level.go: -------------------------------------------------------------------------------- 1 | package meander 2 | 3 | import "strings" 4 | 5 | type Cost int8 6 | 7 | const ( 8 | _ Cost = iota 9 | Cost1 10 | Cost2 11 | Cost3 12 | Cost4 13 | Cost5 14 | ) 15 | 16 | var costStrings = map[string]Cost{ 17 | "$": Cost1, 18 | "$$": Cost2, 19 | "$$$": Cost3, 20 | "$$$$": Cost4, 21 | "$$$$$": Cost5, 22 | } 23 | 24 | func (l Cost) String() string { 25 | for s, v := range costStrings { 26 | if l == v { 27 | return s 28 | } 29 | } 30 | return "不正な値です" 31 | } 32 | 33 | func ParseCost(s string) Cost { 34 | return costStrings[s] 35 | } 36 | 37 | type CostRange struct { 38 | From Cost 39 | To Cost 40 | } 41 | 42 | func (r CostRange) String() string { 43 | return r.From.String() + "..." + r.To.String() 44 | } 45 | func ParseCostRange(s string) *CostRange { 46 | segs := strings.Split(s, "...") 47 | return &CostRange{ 48 | From: ParseCost(segs[0]), 49 | To: ParseCost(segs[1]), 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /chapter7/meander/cost_level_test.go: -------------------------------------------------------------------------------- 1 | package meander_test 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/cheekybits/is" 7 | "github.com/oreilly-japan/go-programming-blueprints/chapter7/meander" 8 | ) 9 | 10 | func TestCostValues(t *testing.T) { 11 | is := is.New(t) 12 | is.Equal(int(meander.Cost1), 1) 13 | is.Equal(int(meander.Cost2), 2) 14 | is.Equal(int(meander.Cost3), 3) 15 | is.Equal(int(meander.Cost4), 4) 16 | is.Equal(int(meander.Cost5), 5) 17 | } 18 | 19 | func TestCostString(t *testing.T) { 20 | is := is.New(t) 21 | is.Equal(meander.Cost1.String(), "$") 22 | is.Equal(meander.Cost2.String(), "$$") 23 | is.Equal(meander.Cost3.String(), "$$$") 24 | is.Equal(meander.Cost4.String(), "$$$$") 25 | is.Equal(meander.Cost5.String(), "$$$$$") 26 | } 27 | 28 | func TestParseCost(t *testing.T) { 29 | is := is.New(t) 30 | is.Equal(meander.Cost1, meander.ParseCost("$")) 31 | is.Equal(meander.Cost2, meander.ParseCost("$$")) 32 | is.Equal(meander.Cost3, meander.ParseCost("$$$")) 33 | is.Equal(meander.Cost4, meander.ParseCost("$$$$")) 34 | is.Equal(meander.Cost5, meander.ParseCost("$$$$$")) 35 | } 36 | 37 | func TestParseCostRange(t *testing.T) { 38 | is := is.New(t) 39 | var l *meander.CostRange 40 | l = meander.ParseCostRange("$$...$$$") 41 | is.Equal(l.From, meander.Cost2) 42 | is.Equal(l.To, meander.Cost3) 43 | l = meander.ParseCostRange("$...$$$$$") 44 | is.Equal(l.From, meander.Cost1) 45 | is.Equal(l.To, meander.Cost5) 46 | } 47 | func TestCostRangeString(t *testing.T) { 48 | is := is.New(t) 49 | is.Equal("$$...$$$$", (&meander.CostRange{ 50 | From: meander.Cost2, 51 | To: meander.Cost4, 52 | }).String()) 53 | } 54 | -------------------------------------------------------------------------------- /chapter7/meander/journeys.go: -------------------------------------------------------------------------------- 1 | package meander 2 | 3 | import "strings" 4 | 5 | type j struct { 6 | Name string 7 | PlaceTypes []string 8 | } 9 | 10 | var Journeys = []interface{}{ 11 | &j{Name: "ロマンティック", PlaceTypes: []string{"park", "bar", 12 | "movie_theater", "restaurant", "florist", "taxi_stand"}}, 13 | &j{Name: "ショッピング", PlaceTypes: []string{"department_store", 14 | "cafe", "clothing_store", "jewelry_store", "shoe_store"}}, 15 | &j{Name: "ナイトライフ", PlaceTypes: []string{"bar", "casino", 16 | "food", "bar", "night_club", "bar", "bar", "hospital"}}, 17 | &j{Name: "カルチャー", PlaceTypes: []string{"museum", "cafe", 18 | "cemetery", "library", "art_gallery"}}, 19 | &j{Name: "リラックス", PlaceTypes: []string{"hair_care", 20 | "beauty_salon", "cafe", "spa"}}, 21 | } 22 | 23 | func (j *j) Public() interface{} { 24 | return map[string]interface{}{ 25 | "name": j.Name, 26 | "journey": strings.Join(j.PlaceTypes, "|"), 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /chapter7/meander/public.go: -------------------------------------------------------------------------------- 1 | package meander 2 | 3 | type Facade interface { 4 | Public() interface{} 5 | } 6 | 7 | func Public(o interface{}) interface{} { 8 | if p, ok := o.(Facade); ok { 9 | return p.Public() 10 | } 11 | return o 12 | } 13 | -------------------------------------------------------------------------------- /chapter7/meander/query.go: -------------------------------------------------------------------------------- 1 | package meander 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "log" 7 | "math/rand" 8 | "net/http" 9 | "net/url" 10 | "sync" 11 | "time" 12 | ) 13 | 14 | var APIKey string 15 | 16 | type Place struct { 17 | *googleGeometry `json:"geometry"` 18 | Name string `json:"name"` 19 | Icon string `json:"icon"` 20 | Photos []*googlePhoto `json:"photos"` 21 | Vicinity string `json:"vicinity"` 22 | } 23 | type googleResponse struct { 24 | Results []*Place `json:"results"` 25 | } 26 | type googleGeometry struct { 27 | *googleLocation `json:"location"` 28 | } 29 | type googleLocation struct { 30 | Lat float64 `json:"lat"` 31 | Lng float64 `json:"lng"` 32 | } 33 | type googlePhoto struct { 34 | PhotoRef string `json:"photo_reference"` 35 | URL string `json:"url"` 36 | } 37 | 38 | func (p *Place) Public() interface{} { 39 | return map[string]interface{}{ 40 | "name": p.Name, 41 | "icon": p.Icon, 42 | "photos": p.Photos, 43 | "vicinity": p.Vicinity, 44 | "lat": p.Lat, 45 | "lng": p.Lng, 46 | } 47 | } 48 | 49 | type Query struct { 50 | Lat float64 51 | Lng float64 52 | Journey []string 53 | Radius int 54 | CostRangeStr string 55 | } 56 | 57 | func (q *Query) find(types string) (*googleResponse, error) { 58 | u := "https://maps.googleapis.com/maps/api/place/nearbysearch/json" 59 | vals := make(url.Values) 60 | vals.Set("location", fmt.Sprintf("%g,%g", q.Lat, q.Lng)) 61 | vals.Set("radius", fmt.Sprintf("%d", q.Radius)) 62 | vals.Set("types", types) 63 | vals.Set("key", APIKey) 64 | if len(q.CostRangeStr) > 0 { 65 | r := ParseCostRange(q.CostRangeStr) 66 | vals.Set("minprice", fmt.Sprintf("%d", int(r.From)-1)) 67 | vals.Set("maxprice", fmt.Sprintf("%d", int(r.To)-1)) 68 | } 69 | res, err := http.Get(u + "?" + vals.Encode()) 70 | if err != nil { 71 | return nil, err 72 | } 73 | defer res.Body.Close() 74 | var response googleResponse 75 | if err := json.NewDecoder(res.Body).Decode(&response); err != nil { 76 | return nil, err 77 | } 78 | return &response, nil 79 | } 80 | 81 | // 問い合わせを並列に行い、その結果を返します 82 | func (q *Query) Run() []interface{} { 83 | rand.Seed(time.Now().UnixNano()) 84 | var w sync.WaitGroup 85 | var l sync.Mutex 86 | places := make([]interface{}, len(q.Journey)) 87 | for i, r := range q.Journey { 88 | w.Add(1) 89 | go func(types string, i int) { 90 | defer w.Done() 91 | response, err := q.find(types) 92 | if err != nil { 93 | log.Println("施設の検索に失敗しました:", err) 94 | return 95 | } 96 | if len(response.Results) == 0 { 97 | log.Println("施設が見つかりませんでした:", types) 98 | return 99 | } 100 | for _, result := range response.Results { 101 | for _, photo := range result.Photos { 102 | photo.URL = "https://maps.googleapis.com/maps/api/place/photo?" + 103 | "maxwidth=1000&photoreference=" + photo.PhotoRef + 104 | "&key=" + APIKey 105 | } 106 | } 107 | randI := rand.Intn(len(response.Results)) 108 | l.Lock() 109 | places[i] = response.Results[randI] 110 | l.Unlock() 111 | }(r, i) 112 | } 113 | w.Wait() // すべてのリクエストの完了を待ちます 114 | return places 115 | } 116 | -------------------------------------------------------------------------------- /chapter8/backup/archiver.go: -------------------------------------------------------------------------------- 1 | package backup 2 | 3 | import ( 4 | "archive/zip" 5 | "io" 6 | "os" 7 | "path/filepath" 8 | ) 9 | 10 | type Archiver interface { 11 | DestFmt() string 12 | Archive(src, dest string) error 13 | } 14 | 15 | type zipper struct{} 16 | 17 | var ZIP Archiver = (*zipper)(nil) 18 | 19 | func (z *zipper) DestFmt() string { 20 | return "%d.zip" 21 | } 22 | 23 | func (z *zipper) Archive(src, dest string) error { 24 | if err := os.MkdirAll(filepath.Dir(dest), 0777); err != nil { 25 | return err 26 | } 27 | out, err := os.Create(dest) 28 | if err != nil { 29 | return err 30 | } 31 | defer out.Close() 32 | w := zip.NewWriter(out) 33 | defer w.Close() 34 | return filepath.Walk(src, func(path string, info os.FileInfo, 35 | err error) error { 36 | if info.IsDir() { 37 | return nil // スキップします 38 | } 39 | if err != nil { 40 | return err 41 | } 42 | in, err := os.Open(path) 43 | if err != nil { 44 | return err 45 | } 46 | defer in.Close() 47 | f, err := w.Create(path) 48 | if err != nil { 49 | return err 50 | } 51 | io.Copy(f, in) 52 | return nil 53 | }) 54 | } 55 | -------------------------------------------------------------------------------- /chapter8/backup/cmds/backup/backupdata/.keepme: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oreilly-japan/go-programming-blueprints/fc204afa474490efb10b7ad8d1928b66848f02e2/chapter8/backup/cmds/backup/backupdata/.keepme -------------------------------------------------------------------------------- /chapter8/backup/cmds/backup/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "encoding/json" 5 | "errors" 6 | "flag" 7 | "fmt" 8 | "log" 9 | "strings" 10 | 11 | "github.com/matryer/filedb" 12 | ) 13 | 14 | type path struct { 15 | Path string 16 | Hash string 17 | } 18 | 19 | func (p path) String() string { 20 | return fmt.Sprintf("%s [%s]", p.Path, p.Hash) 21 | } 22 | 23 | func main() { 24 | var fatalErr error 25 | defer func() { 26 | if fatalErr != nil { 27 | flag.PrintDefaults() 28 | log.Fatalln(fatalErr) 29 | } 30 | }() 31 | var ( 32 | dbpath = flag.String("db", "./backupdata", "データベースのディレクトリへのパス") 33 | ) 34 | flag.Parse() 35 | args := flag.Args() 36 | if len(args) < 1 { 37 | fatalErr = errors.New("エラー; コマンドを指定してください") 38 | return 39 | } 40 | 41 | db, err := filedb.Dial(*dbpath) 42 | if err != nil { 43 | fatalErr = err 44 | return 45 | } 46 | defer db.Close() 47 | col, err := db.C("paths") 48 | if err != nil { 49 | fatalErr = err 50 | return 51 | } 52 | 53 | switch strings.ToLower(args[0]) { 54 | case "list": 55 | var path path 56 | col.ForEach(func(i int, data []byte) bool { 57 | err := json.Unmarshal(data, &path) 58 | if err != nil { 59 | fatalErr = err 60 | return true 61 | } 62 | fmt.Printf("= %s\n", path) 63 | return false 64 | }) 65 | 66 | case "add": 67 | if len(args[1:]) == 0 { 68 | fatalErr = errors.New("追加するパスを指定してください") 69 | return 70 | } 71 | for _, p := range args[1:] { 72 | path := &path{Path: p, Hash: "まだアーカイブされていません"} 73 | if err := col.InsertJSON(path); err != nil { 74 | fatalErr = err 75 | return 76 | } 77 | fmt.Printf("+ %s\n", path) 78 | } 79 | 80 | case "remove": 81 | var path path 82 | col.RemoveEach(func(i int, data []byte) (bool, bool) { 83 | err := json.Unmarshal(data, &path) 84 | if err != nil { 85 | fatalErr = err 86 | return false, true 87 | } 88 | for _, p := range args[1:] { 89 | if path.Path == p { 90 | fmt.Printf("- %s\n", path) 91 | return true, false 92 | } 93 | } 94 | return false, false 95 | }) 96 | 97 | } 98 | 99 | } 100 | -------------------------------------------------------------------------------- /chapter8/backup/cmds/backupd/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "encoding/json" 5 | "errors" 6 | "flag" 7 | "fmt" 8 | "log" 9 | "os" 10 | "os/signal" 11 | "syscall" 12 | "time" 13 | 14 | "github.com/matryer/filedb" 15 | "github.com/oreilly-japan/go-programming-blueprints/chapter8/backup" 16 | ) 17 | 18 | type path struct { 19 | Path string 20 | Hash string 21 | } 22 | 23 | func main() { 24 | var fatalErr error 25 | defer func() { 26 | if fatalErr != nil { 27 | log.Fatalln(fatalErr) 28 | } 29 | }() 30 | var ( 31 | interval = flag.Int("interval", 10, "チェックの間隔(秒単位)") 32 | archive = flag.String("archive", "archive", "アーカイブの保存先") 33 | dbpath = flag.String("db", "./db", "filedbデータベースへのパス") 34 | ) 35 | flag.Parse() 36 | 37 | m := &backup.Monitor{ 38 | Destination: *archive, 39 | Archiver: backup.ZIP, 40 | Paths: make(map[string]string), 41 | } 42 | 43 | db, err := filedb.Dial(*dbpath) 44 | if err != nil { 45 | fatalErr = err 46 | return 47 | } 48 | defer db.Close() 49 | col, err := db.C("paths") 50 | if err != nil { 51 | fatalErr = err 52 | return 53 | } 54 | 55 | var path path 56 | col.ForEach(func(_ int, data []byte) bool { 57 | if err := json.Unmarshal(data, &path); err != nil { 58 | fatalErr = err 59 | return true 60 | } 61 | m.Paths[path.Path] = path.Hash 62 | return false // 処理を続行します 63 | }) 64 | if fatalErr != nil { 65 | return 66 | } 67 | if len(m.Paths) < 1 { 68 | fatalErr = errors.New("パスがありません。backupツールを使って追加してください") 69 | return 70 | } 71 | check(m, col) 72 | signalChan := make(chan os.Signal, 1) 73 | signal.Notify(signalChan, syscall.SIGINT, syscall.SIGTERM) 74 | Loop: 75 | for { 76 | select { 77 | case <-time.After(time.Duration(*interval) * time.Second): 78 | check(m, col) 79 | case <-signalChan: 80 | // 終了 81 | fmt.Println() 82 | log.Printf("終了します...") 83 | break Loop 84 | } 85 | } 86 | 87 | } 88 | 89 | func check(m *backup.Monitor, col *filedb.C) { 90 | log.Println("チェックします...") 91 | counter, err := m.Now() 92 | if err != nil { 93 | log.Panicln("バックアップに失敗しました:", err) 94 | } 95 | if counter > 0 { 96 | log.Printf(" %d個のディレクトリをアーカイブしました\n", counter) 97 | // ハッシュ値を更新します 98 | var path path 99 | col.SelectEach(func(_ int, data []byte) (bool, []byte, bool) { 100 | if err := json.Unmarshal(data, &path); err != nil { 101 | log.Println("JSONデータの読み込みに失敗しました。"+ 102 | "次の項目に進みます:", err) 103 | return true, data, false 104 | } 105 | path.Hash, _ = m.Paths[path.Path] 106 | newdata, err := json.Marshal(&path) 107 | if err != nil { 108 | log.Println("JSONデータの書き出しに失敗しました。"+ 109 | "次の項目に進みます:", err) 110 | return true, data, false 111 | } 112 | return true, newdata, false 113 | }) 114 | } else { 115 | log.Println(" 変更はありません") 116 | } 117 | } 118 | -------------------------------------------------------------------------------- /chapter8/backup/dirhash.go: -------------------------------------------------------------------------------- 1 | package backup 2 | 3 | import ( 4 | "crypto/md5" 5 | "fmt" 6 | "io" 7 | "os" 8 | "path/filepath" 9 | ) 10 | 11 | func DirHash(path string) (string, error) { 12 | hash := md5.New() 13 | err := filepath.Walk(path, func(path string, info os.FileInfo, err error) error { 14 | if err != nil { 15 | return err 16 | } 17 | io.WriteString(hash, path) 18 | fmt.Fprintf(hash, "%v", info.IsDir()) 19 | fmt.Fprintf(hash, "%v", info.ModTime()) 20 | fmt.Fprintf(hash, "%v", info.Mode()) 21 | fmt.Fprintf(hash, "%v", info.Name()) 22 | fmt.Fprintf(hash, "%v", info.Size()) 23 | return nil 24 | }) 25 | if err != nil { 26 | return "", err 27 | } 28 | return fmt.Sprintf("%x", hash.Sum(nil)), nil 29 | } 30 | -------------------------------------------------------------------------------- /chapter8/backup/monitor.go: -------------------------------------------------------------------------------- 1 | package backup 2 | 3 | import ( 4 | "fmt" 5 | "path/filepath" 6 | "time" 7 | ) 8 | 9 | type Monitor struct { 10 | Paths map[string]string 11 | Archiver Archiver 12 | Destination string 13 | } 14 | 15 | func (m *Monitor) Now() (int, error) { 16 | var counter int 17 | for path, lastHash := range m.Paths { 18 | newHash, err := DirHash(path) 19 | if err != nil { 20 | return 0, err 21 | } 22 | if newHash != lastHash { 23 | err := m.act(path) 24 | if err != nil { 25 | return counter, err 26 | } 27 | m.Paths[path] = newHash // ハッシュ値を更新します 28 | counter++ 29 | } 30 | } 31 | return counter, nil 32 | } 33 | 34 | func (m *Monitor) act(path string) error { 35 | dirname := filepath.Base(path) 36 | filename := fmt.Sprintf(m.Archiver.DestFmt(), 37 | time.Now().UnixNano()) 38 | return m.Archiver.Archive(path, filepath.Join(m.Destination, 39 | dirname, filename)) 40 | } 41 | --------------------------------------------------------------------------------