こんにちは。KOUKIです。
GitHub REST APIを使って、簡単なWebアプリケーションを開発します。
JavaScript版はこちらです。
<目次>
作成するアプリケーション
こんな感じのものを作ります。
ワークスペースの作成
まずは、ワークスペースを作成します。
1 2 3 4 5 6 7 8 9 10 11 12 13 |
touch Dockerfile touch docker-compose.yml touch main.go mkdir domain touch domain/domain.go mkdir rest touch rest/rest.go mkdir html touch html/index.html touch html/style.css mkdir template touch template/template.go go mod init githubapi-golang |
Dockerの準備
開発環境にDockerを選択していますが、Docker上でなくとも「go run main.go」で動くと思います。
Dockerfile
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
FROM golang:1.15-alpine3.12 RUN apk update && \ apk upgrade && \ apk add git RUN go get github.com/cespare/reflex ENV CGO_ENABLED=0 WORKDIR /go/src/app COPY go.* main.go ./ RUN go mod download |
docker-compose.yml
1 2 3 4 5 6 7 8 9 10 |
version: "3" services: golang: build: . ports: - 80:8080 volumes: - .:/go/src/app/ command: > sh -c "reflex -s -r '\.go$$' go run main.go" |
アプリケーション開発
早速、アプリケーションを開発しましょう。
画面表示
まずは、アプリケーションの画面表示を行いましょう。画面がないと実態がわかりずらいですからね。
Webアプリの作成方法については、以下の記事が参考になると思います。
Go言語
まずは、Go言語からHTMLテンプレートを読み込んで、ブラウザに表示する処理を書きます。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 |
// main.go package main import ( "fmt" "log" "net/http" "text/template" ) func logFatal(err error) { if err != nil { log.Println(err) } } func viewHandler(writer http.ResponseWriter, request *http.Request) { html, err := template.ParseFiles("html/index.html") logFatal(err) err = html.Execute(writer, nil) logFatal(err) } func main() { // cssを適用するために必要な設定 http.Handle("/html/", http.StripPrefix("/html/", http.FileServer(http.Dir("html/")))) http.HandleFunc("/view", viewHandler) fmt.Println("Starting Server...") log.Fatal(http.ListenAndServe(":8080", nil)) } |
HTML
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" /> <link rel="stylesheet" href="./html/style.css" /> <title>Github Profiles</title> </head> <body> <form action="/create" method="post" class="user-form" id="form"> <input type="text" id="search" name="username" placeholder="Search a Github User"/> </form> <main id="main"></main> </body> </html> |
CSS
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 |
@import url("https://fonts.googleapis.com/css2?family=Poppins:wght@200;400&display=swap"); * { box-sizing: border-box; } body { background-color: #2a2a72; color: #fff; font-family: "Poppins", sans-serif; display: flex; flex-direction: column; align-items: center; justify-content: center; height: 100vh; overflow: hidden; margin: 0; } .user-form { width: 100%; max-width: 700px; } .user-form input { width: 100%; display: block; background-color: #4c2885; border: none; border-radius: 10px; color: #fff; padding: 1rem; margin-bottom: 2rem; font-family: inherit; font-size: 1rem; box-shadow: 0 5px 10px rgba(154, 160, 185, 0.05), 0 15px 40px rgba(0, 0, 0, 0.1); } .user-form input::placeholder { color: #bbb; } .user-form input:focus { outline: none; } .card { max-width: 800px; background-color: #4c2885; border-radius: 20px; box-shadow: 0 5px 10px rgba(154, 160, 185, 0.05), 0 15px 40px rgba(0, 0, 0, 0.1); display: flex; padding: 3rem; margin: 0 1.5rem; } .avatar { border-radius: 50%; border: 10px solid #2a2a72; height: 150px; width: 150px; } .user-info { color: #eee; margin-left: 2rem; } .user-info h2 { margin-top: 0; } .user-info ul { list-style-type: none; display: flex; justify-content: space-between; padding: 0; max-width: 400px; } .user-info ul li { display: flex; align-items: center; } .user-info ul li strong { font-size: 0.9rem; margin-left: 0.5rem; } .repo { text-decoration: none; color: #fff; background-color: #212a72; font-size: 0.7rem; padding: 0.25rem 0.5rem; margin-right: 0.5rem; margin-bottom: 0.5rem; display: inline-block; } @media (max-width: 500px) { .card { flex-direction: column; align-items: center; } .user-form { max-width: 400px; } } |
動作確認
画面を表示してみましょう。以下のコマンドで、コンテナを立ち上げます。
1 |
docker-compose up |
ブラウザから「http://localhost/view」にアクセスしてください。

ドメインの作成
次にドメインを作成します。
「https://api.github.com/users/golang」にブラウザからアクセスしてみてください。下記のデータが取得できるはずです。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 |
{ login: "golang", id: 4314092, node_id: "MDEyOk9yZ2FuaXphdGlvbjQzMTQwOTI=", avatar_url: "https://avatars.githubusercontent.com/u/4314092?v=4", gravatar_id: "", url: "https://api.github.com/users/golang", html_url: "https://github.com/golang", followers_url: "https://api.github.com/users/golang/followers", following_url: "https://api.github.com/users/golang/following{/other_user}", gists_url: "https://api.github.com/users/golang/gists{/gist_id}", starred_url: "https://api.github.com/users/golang/starred{/owner}{/repo}", subscriptions_url: "https://api.github.com/users/golang/subscriptions", organizations_url: "https://api.github.com/users/golang/orgs", repos_url: "https://api.github.com/users/golang/repos", events_url: "https://api.github.com/users/golang/events{/privacy}", received_events_url: "https://api.github.com/users/golang/received_events", type: "Organization", site_admin: false, name: "Go", company: null, blog: "https://golang.org", location: null, email: null, hireable: null, bio: "The Go Programming Language", twitter_username: null, public_repos: 51, public_gists: 0, followers: 0, following: 0, created_at: "2013-05-01T18:00:52Z", updated_at: "2020-04-04T15:27:25Z" } |
このデータをGo言語で扱えるようにするために、ドメインを作成します。
1 2 3 4 5 6 7 8 9 10 11 12 |
// domain.go package domain type UserInfo struct { AvatarURL string `json:"avatar_url"` Name string `json:"name"` Bio string `json:"bio"` Followers int `json:"followers"` Following int `json:"following"` PublicRepos int `json:"public_repos"` } |
注意点は、「json:"avatar_url"
」のように実際に取得できるデータのパラメータに名前を一致させる必要があります。パースするんですから当たり前ですよね。
次に、「https://api.github.com/users/golang/repos?sort=created」にアクセスして得られる情報を確認します。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
[ { id: 265280586, node_id: "MDEwOlJlcG9zaXRvcnkyNjUyODA1ODY=", name: "pkgsite", full_name: "golang/pkgsite", private: false, owner: { login: "golang", id: 4314092, .... } ] |
かなりたくさんの情報を取得できるみたいですね。これもドメイン化します。
1 2 3 4 5 6 7 |
// domain.go ... type Repo struct { Name string `json:"name"` HTMLURL string `json:"html_url"` } |
Interfaceの定義
Interfaceを定義しておくと便利な時があるので、定義しましょう。
1 2 3 4 5 6 7 |
// domain.go ... type GetRequest interface { GetUser(username string) UserInfo GetRepos(username string) Repo } |
Getリクエストの作成
Getリクエストをコントロールするインスタンスとメソッドを作成しましょう。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 |
// rest.go package rest import ( "encoding/json" "githubapi-golang/domain" "net/http" ) type getRequest struct { endPoint string } func NewGetRequest(endPoint string) domain.GetRequest { return &getRequest{ endPoint: endPoint, } } func (g *getRequest) GetUser(username string) domain.UserInfo { var u domain.UserInfo resp, err := http.Get(g.endPoint + username) if err != nil { return u } defer resp.Body.Close() err = json.NewDecoder(resp.Body).Decode(&u) if err != nil { return u } return u } func (g *getRequest) GetRepos(username string) []domain.Repo { var rs []domain.Repo resp, err := http.Get(g.endPoint + username + "/repos?sort=created") if err != nil { return rs } defer resp.Body.Close() err = json.NewDecoder(resp.Body).Decode(&rs) if err != nil { return rs } return rs } |
jsonのNewDecoderを使って、APIから得られるデータをドメインに定義したStructへパースしています。
main.goを以下のように修正後に、terminalから「curl localhost/create」を打ってみましょう。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
func createHandler(writer http.ResponseWriter, request *http.Request) { userName := "golang" gr := rest.NewGetRequest("https://api.github.com/users/") user := gr.GetUser(userName) fmt.Println(user) repos := gr.GetRepos(userName) fmt.Println(repos) } func main() { // cssを適用するために必要な設定 http.Handle("/html/", http.StripPrefix("/html/", http.FileServer(http.Dir("html/")))) http.HandleFunc("/view", viewHandler) http.HandleFunc("/create", createHandler) fmt.Println("Starting Server...") log.Fatal(http.ListenAndServe(":8080", nil)) } |
すると、以下のデータが取得できていることがわかります。
1 2 3 4 5 6 7 |
golang_1 | [00] &{https://avatars.githubusercontent.com/u/4314092?v=4 Go The Go Programming Language 0 0 51} golang_1 | [00] &[{ pkgsite https://github.com/golang/pkgsite} { vscode-go https://github.com/golang/vscode-go} { mod https://github.com/golang/mod} { xerrors https://github.com/golang/xerrors} { ...}] |
APIから得られるデータとStructのjsonタグに定義したnameが一致していれば、このようにGo言語がパースをしてくれます。
テンプレートファイルの作成
次は、テンプレートファイルを作成しましょう。
3つのタイプを作りたいと思います。
- 検索前のページ
- 検索後のページ
- エラーページ
リクエストされて、取得したデータによって表示するデータの切り替えを行います。色々方法はあると思いますが、筆者は以下の感じに実装しました。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 |
//template.go package template import ( "fmt" "githubapi-golang/domain" "io/ioutil" "log" ) const ( HTMLDocs = ` <!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" /> <link rel="stylesheet" href="./html/style.css" /> <title>Github Profiles</title> </head> <body> <form action="/create" method="post" class="user-form" id="form"> <input type="text" id="search" name="username" placeholder="Search a Github User"/> </form> <main id="main"> %s </main> </body> <html> ` UserCardHTML = ` <div class="card"> <div> <img src="%s" alt="%s" class="avatar"> </div> <div class="user-info"> <h2>%s</h2> <p>%s</p> <ul> <li>%d<strong>Followers</strong></li> <li>%d<strong>Following</strong></li> <li>%d<strong>Repos</strong></li> </ul> <div id="repos"> %s </div> </div> ` RepoHTML = ` <a class="repo" href="%s" target="_blank"> %s </a> ` CardErrHTML = ` <div class="card"> <h1>No profile with this username</h1> </div> ` ) func OutPutFile(user domain.UserInfo, repos []domain.Repo) { var result string if user != (domain.UserInfo{}) { userCardHTML := fmt.Sprintf( UserCardHTML, user.AvatarURL, user.Name, user.Name, user.Bio, user.Followers, user.Following, user.PublicRepos, "%s", ) var repoHTML string for i := 0; i < len(repos); i++ { repoHTML += fmt.Sprintf( RepoHTML, repos[i].HTMLURL, repos[i].Name, ) } userCardHTML = fmt.Sprintf(userCardHTML, repoHTML) result = fmt.Sprintf(HTMLDocs, userCardHTML) } else { result = fmt.Sprintf(HTMLDocs, CardErrHTML) } // ファイルの書き込み err := ioutil.WriteFile("./html/result.html", []byte(result), 0777) if err != nil { log.Fatal(err) } } |
少し長いですが、出力したいhtmlを定数で定義しておいて、取得したデータによってresult.htmlに書き込んでいるだけです。
result.htmlは、main関数から読み込みます。
createHandler関数の修正
createHandler関数内で、テンプレート処理を呼び出し、result.htmlを画面に返す処理を追加します。
1 2 3 4 5 6 7 8 9 |
func createHandler(writer http.ResponseWriter, request *http.Request) { userName := "golang" gr := rest.NewGetRequest("https://api.github.com/users/") user := gr.GetUser(userName) repos := gr.GetRepos(userName) customTMPL.OutPutFile(user, repos) html, err := template.ParseFiles("html/index.html") logFatal(err) } |
これで完成です!
おわりに
APIのリクエスト送信、データの表示、テンプレートファイルの作成全てをGo言語で作成しました。
Go言語は何でもできますね^^
それでは、また!
Go言語まとめ
ソースコード
main.go
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 |
// main.go package main import ( "fmt" "githubapi-golang/rest" customTMPL "githubapi-golang/template" "log" "net/http" "text/template" ) func logFatal(err error) { if err != nil { log.Println(err) } } func viewHandler(writer http.ResponseWriter, request *http.Request) { html, err := template.ParseFiles("html/index.html") logFatal(err) err = html.Execute(writer, nil) logFatal(err) } func createHandler(writer http.ResponseWriter, request *http.Request) { userName := request.FormValue("username") gr := rest.NewGetRequest("https://api.github.com/users/") user := gr.GetUser(userName) repos := gr.GetRepos(userName) customTMPL.OutPutFile(user, repos) html, err := template.ParseFiles("html/result.html") logFatal(err) err = html.Execute(writer, nil) logFatal(err) } func main() { // cssを適用するために必要な設定 http.Handle("/html/", http.StripPrefix("/html/", http.FileServer(http.Dir("html/")))) http.HandleFunc("/view", viewHandler) http.HandleFunc("/create", createHandler) fmt.Println("Starting Server...") log.Fatal(http.ListenAndServe(":8080", nil)) } |
domain.go
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 |
// domain.go package domain type UserInfo struct { AvatarURL string `json:"avatar_url"` Name string `json:"name"` Bio string `json:"bio"` Followers int `json:"followers"` Following int `json:"following"` PublicRepos int `json:"public_repos"` } type Repo struct { Name string `json:"name"` HTMLURL string `json:"html_url"` } type GetRequest interface { GetUser(username string) UserInfo GetRepos(username string) []Repo } |
rest.go
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 |
// rest.go package rest import ( "encoding/json" "githubapi-golang/domain" "net/http" ) type getRequest struct { endPoint string } func NewGetRequest(endPoint string) domain.GetRequest { return &getRequest{ endPoint: endPoint, } } func (g *getRequest) GetUser(username string) domain.UserInfo { var u domain.UserInfo resp, err := http.Get(g.endPoint + username) if err != nil { return u } defer resp.Body.Close() err = json.NewDecoder(resp.Body).Decode(&u) if err != nil { return u } return u } func (g *getRequest) GetRepos(username string) []domain.Repo { var rs []domain.Repo resp, err := http.Get(g.endPoint + username + "/repos?sort=created") if err != nil { return rs } defer resp.Body.Close() err = json.NewDecoder(resp.Body).Decode(&rs) if err != nil { return rs } return rs } |
template.go
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 |
//template.go package template import ( "fmt" "githubapi-golang/domain" "io/ioutil" "log" ) const ( HTMLDocs = ` <!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" /> <link rel="stylesheet" href="./html/style.css" /> <title>Github Profiles</title> </head> <body> <form action="/create" method="post" class="user-form" id="form"> <input type="text" id="search" name="username" placeholder="Search a Github User"/> </form> <main id="main"> %s </main> </body> <html> ` UserCardHTML = ` <div class="card"> <div> <img src="%s" alt="%s" class="avatar"> </div> <div class="user-info"> <h2>%s</h2> <p>%s</p> <ul> <li>%d<strong>Followers</strong></li> <li>%d<strong>Following</strong></li> <li>%d<strong>Repos</strong></li> </ul> <div id="repos"> %s </div> </div> ` RepoHTML = ` <a class="repo" href="%s" target="_blank"> %s </a> ` CardErrHTML = ` <div class="card"> <h1>No profile with this username</h1> </div> ` ) func OutPutFile(user domain.UserInfo, repos []domain.Repo) { var result string if user != (domain.UserInfo{}) { userCardHTML := fmt.Sprintf( UserCardHTML, user.AvatarURL, user.Name, user.Name, user.Bio, user.Followers, user.Following, user.PublicRepos, "%s", ) var repoHTML string for i := 0; i < len(repos); i++ { repoHTML += fmt.Sprintf( RepoHTML, repos[i].HTMLURL, repos[i].Name, ) } userCardHTML = fmt.Sprintf(userCardHTML, repoHTML) result = fmt.Sprintf(HTMLDocs, userCardHTML) } else { result = fmt.Sprintf(HTMLDocs, CardErrHTML) } // ファイルの書き込み err := ioutil.WriteFile("./html/result.html", []byte(result), 0777) if err != nil { log.Fatal(err) } } |
コメントを残す
コメントを投稿するにはログインしてください。