├── .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 | 
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 |
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 |
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 |
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 |
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 |
19 |
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 |
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 |
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 |
--------------------------------------------------------------------------------