class: title, smokescreen, shelf, no-footer # <small>インフラチーム研修プログラム</small> <div class=footnote> <small><small> 2024年度秋板 </small></small> </div> --- name: index class: compact # <small>もくじ</small> <div class=footnote> <small><small> </small></small> </div> <small> 1. Go言語 じゃんけん編 (変数、条件文、くりかえし) 1. Go言語 もぐらたたき編 (配列、スライス) 1. Dockerの基礎 (Docker desktop, Dockerfileを書く) 1. Web APIサーバ (HTML 1.1, CGI;FORM文おうむがえし,じゃんけん,ショッピングカート,アップロード) 1. 総合課題: PaaS構築 (テキストは「冴えないPaaSの育て方」) - ここまでの技術 + Unix的な発想と操作(new) - BWS-PaaS original version (v1.0.0)へ原点回帰して、これを作成 - あるていどの雛形は渡すので、それを完成させる課題 </small> --- name: golang class: title, smokescreen, shelf, no-footer # <small>Go言語編</small> <div class=footnote> <small><small> </small></small> </div> --- class: compact # <small>Go言語の研修</small> <div class=footnote> <small><small> 基礎的なところを一通りやります。 おおむねモダンになったC言語ということが分かればよろしい。 慣れれば簡単 </small></small> </div> - [Go言語 イントロダクション](https://exercises-aws.fml.org/ja/appendix/golang/) - [Go言語 じゃんけん編](https://unix-entrance.fml.org/slides/lang/golang/janken/) - 変数、条件文、くりかえし - [Go言語 もぐらたたき編](https://unix-entrance.fml.org/slides/lang/golang/mogura/) - 配列、スライス - Go言語 その他は、各論ですかね - 例: Web APIサーバで具体的な例を見ていきます --- class: compact # <small>オブジェクト指向言語理論の取捨選択</small> <div class=footnote> <small><small> </small></small> </div> --- name: docker class: title, smokescreen, shelf, no-footer # <small>Dockerを操作してみよう</small> <div class=footnote> <small><small> </small></small> </div> --- class: compact,img-right # <small>Docker desktopをインストールする(Windows)</small> <div class=footnote> <small><small> (脚注) LPI Webinarの資料一式は、 <A HREF="https://lpic-2024q2.demo.fml.org/">https://lpic-2024q2.demo.fml.org/</A>に、 まとめてあります </small></small> </div> [![](images/slide-70.png)](https://speakerdeck.com/fmlorg/burauzanohutawokai-ketehttpti-yan-siyou-20240608v1-dot-0-0?slide=70) - Windowsの人はDocker desktopをインストールしてください。 - 資料はLPI Webinarのスライド付録B(スライドのp.70-)です(右図) - [LPI Webinar 2024-06-05](https://speakerdeck.com/fmlorg/burauzanohutawokai-ketehttpti-yan-siyou-20240608v1-dot-0-0?slide=70) - ただしコンテナ名は ex -> debian-pc-plus-golang に読み替えてください --- class: compact # <small>Dockerの使い方: docker hubのコンテナをロードする</small> <div class=footnote> <small><small> </small></small> </div> <small> ``` $ docker run --rm -d --name debian-pc-plus-golang fmlorg/debian-pc-plus-golang [フォーマット] $ docker コマンド [オプション] コンテナ ``` - コンテナ - docker hubのコンテナを指定する場合は「dockerのID/コンテナ名」 <br> 上の例では、dockerのIDが fmlorg で、コンテナ名が debian-pc-plus-golang <br> メジャーな(Docker公式)コンテナイメージの場合"ID/"部分が不要 - オプションの説明 - `--rm` ... コンテナの終了時に後始末をしろという指示 - `-d` ... コンテナを端末から切りはなせという指示 - `--name 名前` ... コンテナに付ける名前。<B>今後は、この名前で指示を出す</B>ので、<B>覚えておく</B>こと。 <br> これを付けないとランダムな名前になるので扱いにくい </small> --- class: compact # <small>課題: debian-pc-plus-golangコンテナにログインする</small> <div class=footnote> <small><small> </small></small> </div> <small> - <B>Powershell</B>を起動し、コンテナにログインしてみましょう - ログインの仕方は、[LPI Webinar 2024-06-05 スライドの p.73](https://speakerdeck.com/fmlorg/burauzanohutawokai-ketehttpti-yan-siyou-20240608v1-dot-0-0?slide=73) を参考にしてください。 <br> <B>注意:</B>ただし<B>コンテナ名は fmlorg/debian-pc-plus-golang に変更</B>です - ログインできたらヘンテコなランダム文字列のプロンプトになるはずです(例: `root@b989a8215c2d:/# `) - コンテナ内で`go version`でも実行してみましょう。バージョン番号が表示されましたか? - コンテナから抜けるには exit か logout コマンドです ``` $ docker exec -it fmlorg/debian-pc-plus-golang /bin/bash [実行例] $ docker exec -it fmlorg/debian-pc-plus-golang /bin/bash root@b989a8215c2d:/# go version go version go1.19.8 linux/amd64 root@b989a8215c2d:/# exit exit ``` </small> --- class: compact # <small>Dockerの使い方: いったん、コンテナを止めてくださいく</small> <div class=footnote> <small><small> </small></small> </div> <small> - オプションの異なるコンテナを再起動するために、いったんコンテナを落とす必要があります ``` $ docker stop fmlorg/debian-pc-plus-golang ``` </small> --- class: compact # <small>Dockerの使い方: サーバのポートを開く</small> <div class=footnote> <small><small> (脚注) dockerを動かしているサーバがホストです。 コンテナ(という戦闘機)を搭載している母艦(carrier)という気分で、 このホストのことを<B>母艦</B>と称したりします。 広く通じる用語なのか?は少し不安ですが、たぶん通じると思います </small></small> </div> <small> - 前々ページのコマンドに `-p 10022:22` オプションを追加して再び実行してください ``` $ docker run --rm -d --name debian-pc-plus-golang -p 10022:22 fmlorg/debian-pc-plus-golang ``` - オプション - `-p ホスト側ポート:コンテナ側ポート` ... `-p 外:内`と覚えてもよい - dockerのオプションの付け方は、どれも、左側が外側(ホスト側)、右側が内側(コンテナ側) - 上の例 `-p 10022:22` は、ホスト側の10022/tcpがコンテナの22/tcpへ転送される - つまり、インターネットに晒した場合、10022/tcpへアクセスすることになる ``` ユーザ(のブラウザ) ---(http://ホスト:10022/ へアクセス)---> ホスト側の10022/tcp --> コンテナ側の22/tcp ``` </small> --- class: compact # <small>課題: コンテナへSSHログインする(1)</small> <div class=footnote> <small><small> (脚注) [宿題] 同じ操作は出きるけれど、微妙に上下の表示が異なるのはなぜか?考えてみよう </small></small> </div> <small> - ターミナルからアクセスしてコンテナにログインできることを確認してください - アクセス先のIPは127.0.0.1、ポートは10022 - ユーザは admin で、パスワードは無し - 初回は、例の yes/no を聞かれるかもしれませんが、そのときは yes - `docker exec -it ...`と同様のプロンプトが出て同じ操作が出来ますよね? ``` [実行例] $ ssh -p 10022 admin@127.0.0.1 Linux b989a8215c2d 6.1.0-23-amd64 #1 SMP PREEMPT_DYNAMIC Debian 6.1.99-1 (2024-07-15) x86_64 ... 省略 ... Debian GNU/Linux comes with ABSOLUTELY NO WARRANTY, to the extent admin@b989a8215c2d:/# go version go version go1.19.8 linux/amd64 admin@b989a8215c2d:/# exit logout Connection to 127.0.0.1 closed. ``` </small> --- class: compact # <small>課題: コンテナへSSHログインする(2)</small> <div class=footnote> <small><small> (脚注1) コンテナを再起動する必要があります <br> (脚注2) sshコマンドの方もオプションを変更する必要がありますぞ </small></small> </div> <small> - いったんコンテナを停止してください - 今度は、次の仕様でコンテナを起動し、ターミナルからSSHでログイン出来ることを確認してください - アクセス先のIPは127.0.0.1、ポートは8022/tcp - ユーザは admin で、パスワードは無し - 実行例は(10022 -> 8022 に変更する以外は)前ページと同じなので省略します </small> --- name: dockerfile class: title, smokescreen, shelf, no-footer # <small>Dockerfileを書こう</small> <div class=footnote> <small><small> (脚注) やっぱり、なんだかんだとトラブルので、1時間でここまであるのは無理だったなぁ </small></small> </div> --- class: compact # <small>Hello from Docker! (無改変コンテナ)</small> <div class=footnote> <small><small> (脚注) 普通はFROM(「どのイメージを元にするか」)指定後にコンテナの改変が続くのですが、 ここでは何もしないので元のまま </small></small> </div> <small> - dockerが配布しているイメージのコピーも作れます、まぁ意味ないですけど:-) - 作業手順 1. `hello-world`というディレクトリを作成し、その中に移動し 1. Dockefileという名のテキストファイルを作成してください。中身は`FROM hello-world`の1行だけでok ``` FROM hello-world ``` 1. コンテナをビルドし、実行してみてください。Hello from Docker!と表示されるはずです ``` [実行例] $ docker build -t hello . .. 省略 ... $ docker run --rm hello Hello from Docker! .. 省略 ... ``` </small> --- class: compact # <small>解説: docker build</small> <div class=footnote> <small><small> </small></small> </div> <small> ``` [フォーマット] $ docker build -t タグ 場所 [実行例] $ docker build -t hello . .. 省略 ... ``` - タグ ... コンテナイメージに付ける名前 <br> 注: コンテナを実行(docker run)する際の引数(コンテナ名)として、このタグを利用できます - 場所 ... Dockefileのあるディレクトリの指定 (「ディレクトリ = docker イメージの単位」という設計思想) ``` [実行例] $ docker build -t hello . .. 省略 ... $ docker run --rm hello ``` </small> --- class: compact # <small>hello-world.goをコンテナで実行する(1)</small> <div class=footnote> <small><small> </small></small> </div> <small> - 今度はhello worldを表示するコンテナを1から作成(フルスクラッチ)することにします <small> - 注: コンテナの中に「Go言語のソースファイル」を組みこみ、「コンパイル済」バイナリを用意するやり方です </small> - 前回つくった「Go言語でhello worldを表示する」プログラムを使いまわします。 以下このファイルを`hello.go`というファイル名とします - コンテナ内でhello.goをコンパイルして実行することにします - あらたに`go-hello-world`というディレクトリを作り、そこで作業することにします。 ディレクトリを作成して、そこに移動してください - 前回作成した`hello.go`ファイルを、このディレクトリにコピーしてください <br> (hello.goを一から書いた方が早そうなら書いてもいい、おまかせします) </small> --- class: compact # <small>hello-world.goをコンテナで実行する(2)</small> <div class=footnote> <small><small> (脚注) わかりやすさ優先のダサいDockerfileですが、今はツッコマナイように:-) モダンに(コンテナを小さく)するのは宿題とします </small></small> </div> <small> - 次のようなDockerfileを作成してください(解説は次ページを参照のこと) ``` FROM debian:12-slim RUN apt update && apt upgrade -y && apt install -y golang COPY hello.go / RUN go build -o /hello /hello.go ENTRYPOINT [ "/hello" ] ``` </small> --- class: compact # <small>hello-world.goをコンテナで実行する(2)の解説</small> <div class=footnote> <small><small> </small></small> </div> <small> ``` # 「FROM イメージ名」 ... Debian GNU/Linuxのコンテナ(-slim = 小)イメージを元に作成する FROM debian:12-slim # 「RUN コマンド」はシェル操作そのもの、コンテナの上でUnixコマンドを実行できます # 以下は「Debian上の環境を最新」にした上で「Go言語(golangパッケージ)をインストール」せよという指示します # シェルにある「コマンドA && コマンドB && コマンドC」という書き方を使うことが普通です # (&&は、コマンドAが成功したらBを実行という意味) RUN apt update && apt upgrade -y && apt install -y golang # 「COPY SRC DST」 ... コンテナの外のファイルを(SRC)をコンテナ内(DST)へコピーできます COPY hello.go / # 上でファイルをコンテナ内の/hello.goにコピーしたので、それをコンパイルして実行ファイル/helloを作成します RUN go build -o /hello /hello.go # ENTRYPOINT ... コンテナ(OS)の起動の最後に何を実行するのか?の指示。ここでは/helloを実行せよと指示しています # 配列なので[ ]で囲んでください。引数がある場合は[ "A", "B", "C" ]のように書いていけます ENTRYPOINT [ "/hello" ] ``` </small> --- class: compact # <small>hello-world.goをコンテナで実行する(3)</small> <div class=footnote> <small><small> </small></small> </div> <small> - コンテナをビルドして、実行してみましょう。hello worldと表示されるはずです ``` $ docker build -t hello . ... 省略 ... $ docker run --rm hello hello world ``` </small> --- name: web-api-01 class: title, smokescreen, shelf, no-footer # <small>Go言語で書くWeb APIサーバ(1)<br>FORM文おうむがえし</small> <div class=footnote> <small><small> (脚注) 授業の裏側の解説とも言います </small></small> </div> --- class: compact # <small>Web APIサーバ(1): FORM文の中身を全て表示する</small> <div class=footnote> <small><small> (脚注) 授業で使っているサーバの /api/lsform/v1 の裏側の解説ですね </small></small> </div> <small> - この「FORM文の中身を全て表示する」サーバの雛形(lsform.go)を渡します <small> - 前パートの「hello.goを組みこんだコンテナを作る」練習を手本にlsform.goを組みこんだコンテナを作ってください </small> - 作業手順 1. 新しいコンテナ(lsform)を作る準備をする(例:go-lsformというディレクトリを作成し、そこへ移動) 1. lsform.goをダウンロードする。実行例: `$ curl -O http://api.fml.org/dist/lsform.go` 1. Dockerfileを書き、コンテナをビルドし、コンテナを起動する 1. 動作確認する ... ブラウザで動作確認するか curl を使ってください ``` [curl の実行例] $ curl -X POST -d KEY=VAL -d jibun=1 http://localhost:8080/api/lsform/v1 ``` - `-X メソッド` ... HTTPのメソッド(GET, POST, 他)の指定、FORM文の場合たいていPOSTを利用 - `-d KEY=VAUE` ... 送信するデータをKEY=VALUE形式で指定する、複数の場合は必要なだけ-dを追加 </small> --- class: compact # <small>Go言語のドキュメントの読み方</small> <div class=footnote> <small><small> </small></small> </div> <small> ``` // たとえばParseForm()とは何か?を調べるには? err := r.ParseForm() ``` 1. ソースコードの引数を見ると`r *http.Request`とあるので<B>http</B>というモジュールらしいとアタリを付ける 1. 適当に golang http とでも検索すれば、下のURL(公式ページ)が出てくるはず 1. ページを開く 1. きちんと変数や関数、型(タイプ)一覧が表示されるので、そこを読む - https://pkg.go.dev/net/http - "net/http" モジュールのドキュメント - https://pkg.go.dev/net/http#Request.ParseForm - ParseForm関数のマニュアル該当部分 </small> --- class: compact # <small>解説 lsform (1)</small> <div class=footnote> <small><small> </small></small> </div> <small> ``` func permitAnyCORS(w http.ResponseWriter) { ... } func setCookie(w http.ResponseWriter, r *http.Request) { ... } func getCookie(w http.ResponseWriter, r *http.Request) string { ... } ``` - とりあえず上の部分の説明は省略します - permitAnyCORS()はCORS(Cross Origin Resource Sharing)つまりセキュリティ関係のおまじないです - setCookie()とgetCookie()は関数名のとおりクッキーを操作するsetter/getterです - import は必要なモジュールを過不足なく追加していくモノです - 最後に`go fmt`を実行すれば、もれなくimportの過不足を怒ってもらえます。必ず実行しましょう - 次ページ以降では、main関数とWWWサーバの実体(ハンドラ本体)を説明します </small> --- class: compact # <small>解説 lsform (2): main</small> <div class=footnote> <small><small> (脚注) これはモダンなGo言語WWWサーバの書き方ではありませんが、とりあえず、わかりやすさ優先です </small></small> </div> <small> ``` func main() { http.HandleFunc("/api/lsform/v1", lsformApi) err := http.ListenAndServe(":8080", nil) if err != nil { log.Fatal(err) } } ``` - コンテンツ配信やAPIサーバ作成なら、とても簡単。標準ライブラリだけで書けます - main関数ではルーティングとサーバの起動だけが書いてあります - 基本的にhttpモジュールを使い、`http.関数()`をヅラヅラと書いていくのです - ルーティング ... `http.HandleFunc("/api/lsform/v1", lsformApi)` (後述) - サーバの起動 ... `http.ListenAndServe(":8080", nil)` (後述) </small> --- class: compact # <small>解説 lsform (3): WWWサーバの起動</small> <div class=footnote> <small><small> </small></small> </div> <small> ``` // フォーマット http.ListenAndServe(アドレス, ハンドラ) // 例: サーバにある全IPアドレスで8080/tcpポートを開いてHTTPを待ち受けます http.ListenAndServe(":8080", nil) ``` - https://pkg.go.dev/net/http#ListenAndServe - 第1引数は「サーバが開く(待ち受ける)アドレス」 - アドレスと書いていますが、正確には「アドレス:ポート番号」です - アドレスを省略した場合は、サーバにある全てのIPアドレスを開きます - 第2引数は「ハンドラ」ですが大抵はnilを指定してデフォルトハンドラ(DefaultServeMux)を使います - そういうわけで、たいていはポート番号の指定を考えるだけで十分です </small> --- class: compact # <small>解説 lsform (4): ルーティング</small> <div class=footnote> <small><small> </small></small> </div> <small> ``` // フォーマット http.HandleFunc(パス, ハンドラ) // 例: http://ホスト/api/lsform/v1 へのリクエストに対し、lsformApi関数を呼び出す http.HandleFunc("/api/lsform/v1", lsformApi) ``` - https://pkg.go.dev/net/http#HandleFunc - コメントのとおりです - URLの`http://ホスト/パス`の「<B>パス</B>に対して呼び出すハンドラ(関数)」を定義します - この例は「URLのパスが"/api/lsform/v1"にマッチする場合はlsformApi関数を呼べ」という指示です </small> --- class: compact # <small>解説 lsform (5a): ハンドラ lsformApi</small> <div class=footnote> <small><small> </small></small> </div> <small> ``` func lsformApi(w http.ResponseWriter, r *http.Request) { permitAnyCORS(w) setCookie(w, r) err := r.ParseForm() if err != nil { fmt.Println("html form parse error") } for k, v := range r.Form { fmt.Fprintf(w, "%v : %v\n", k, v) } } ``` - ハンドラの引数は`(w http.ResponseWriter, r *http.Request)`と決められています - `w`は出力チャンネル(ユーザのブラウザへ送り返す) ... writeのw - `r`は入力チャンネル(ユーザのブラウザからのデータ入力) ... readのr </small> --- class: compact # <small>解説 lsform (5b): ハンドラ lsformApi</small> <div class=footnote> <small><small> (脚注) 「型にむすびついた関数」は、 (雑な説明をすれば)たいていの言語の「クラスのメソッド」に相当します。 Go言語はC++系列のメジャーなオブジェクト指向プログラミング理論とは距離を置いているので、 この説明はよくないですけれども </small></small> </div> <small> ``` err := r.ParseForm() if err != nil { fmt.Println("html form parse error") } } ``` - 変数rに関連付けられた関数群があります - 正確には<B>*http.Request型(の変数r)に関連づけられた関数群</B>です - 利用できる関数は net/http モジュールのtype Requestで関連付けされている関数群になります <br> https://pkg.go.dev/net/http#Request - `r.ParseForm()`は(裏側では毎回よばれているのですが)、 明示的には全部のキーをスキャンする今回のような場面でしか登場しないと思います。 この関数は`r.Form`という変数(ハッシュテーブル,KEY=>VALUE)に、 ブラウザから送られてきたKEY=>VALUEの組を設定します </small> --- class: compact # <small>解説 lsform (5c): ハンドラ lsformApi</small> <div class=footnote> <small><small> (脚注) rangeってPerlのeachが元ネタかな? ちなみにPerlでは、while (($k,$v) = each %form) { ... }のように書きます </small></small> </div> <small> ``` for k, v := range r.Form { fmt.Fprintf(w, "%v : %v\n", k, v) } ``` - `r.ParseForm()`が設定した`r.Form`の値を読み出す部分です - ハッシュテーブルのKEY=>VALUEを一気に取り出すrangeという命令があります - rangeを実行するごとに新たな(k,v)の組が取り出せます(いわゆるイテレーション) - これはC言語にはなかった文法 - このループを回すと、FORM文で送られてきた全てのキーと値の組が表示されます - `Fprintf`の第1引数は`w`です。つまり出力する先は出力チャンネル`w`つまりユーザのブラウザです。 <br> このあたりはC言語のfprintf()と同じです(STDOUTが`w`に変わるだけ) </small> --- class: compact # <small>解説 lsform (6): ハンドラ tips</small> <div class=footnote> <small><small> </small></small> </div> <small> ``` val :=r.FormValue("KEY") ``` - ハンドラで、FORN文の特定のキーの値を取り出すだけなら、`r.FormValue(キー)`を呼ぶだけです。 引数のキーに該当する値が返ります(該当するキーがない場合は空文字列が返ります) ``` [実行例] $ curl -X POST -d KEY=VAL http://localhost:8080/api/lsform/v1 KEY: ["VAL"] ``` </small> --- class: compact # <small>課題: じゃんけんサーバを作ってください</small> <div class=footnote> <small><small> (脚注) 次のパートが「じゃんけんサーバ」の答え合わせになっていますが、できるだけ自力でやってみましょう </small></small> </div> <small> - 仕様 - パスは`/api/janken/v1` - ユーザの手はjibunという変数名で0 1 2を渡す - じゃんけんの結果はjibun aite kekkaすべての情報を出してください。 フォーマットは何でもよいです - 注意点 - r.FormValue()で返る値は文字列(string)なので、ジャンケンするには数字に変換する必要があります ``` [実行例] $ curl -X POST -d jibun=0 http://localhost:8080/api/janken/v1 {"jibun":0,"aite":1,"kekka":2} ``` - この例ではJSONで返していますが、変数3つの情報が返ってくればフォーマットは何でもよいです </small> --- name: web-api-02 class: title, smokescreen, shelf, no-footer # <small>Go言語で書くWeb APIサーバ(2)<br>じゃんけん</small> <div class=footnote> <small><small> (脚注) 授業の裏側の解説とも言います </small></small> </div> --- class: compact # <small>仕様と注意点</small> <div class=footnote> <small><small> </small></small> </div> <small> - 仕様 - パスは`/api/janken/v1` - ユーザの手はjibunという変数名で0 1 2を渡す - じゃんけんの結果はjibun aite kekkaすべての情報を出してください。 フォーマットは何でもよいです - 注意点 - r.FormValue()で返る値は文字列(string)なので、ジャンケンするには数字に変換する必要があります ``` [実行例] $ curl -X POST -d jibun=0 http://localhost:8080/api/janken/v1 {"jibun":0,"aite":1,"kekka":2} ``` - この例ではJSONで返していますが、変数3つの情報が返ってくればフォーマットは何でもよいです </small> --- class: compact # <small>janken.goを作成する</small> <div class=footnote> <small><small> </small></small> </div> <small> - lsform.goを元にして改造していきます - 作業手順 1. ルーティングの設定 1. じゃんけんAPIの本体 - ターミナルで動作確認をする - これをビルドすると、8080/tcp で起動するWWWサーバが起動してきます <br> 停止するときは Ctrl-C - コンテナに組みこむ </small> --- class: compact # <small>ルーティングの設定</small> <div class=footnote> <small><small> </small></small> </div> <small> - lsform.goを手本にmain関数にルーティングを追加します - URLのパスが"/api/janken/v1"にマッチした場合 jankenApi 関数を呼び出すことにします ``` http.HandleFunc("/api/janken/v1", jankenApi) ``` </small> --- class: compact # <small>jankenApi関数(1)</small> <div class=footnote> <small><small> </small></small> </div> <small> - コメント付きのヒナ形を以下に示します、まずは頑張ってみてください ``` func jankenApi(w http.ResponseWriter, r *http.Request) { permitAnyCORS(w) setCookie(w, r) // 1. FORM文のキーjibunに対応する値を取り出す // 2. jibunの値をもとにジャンケンする // 3. jibun, aite, kekkaの3組を出力する(ブラウザに返す) } ``` </small> --- class: compact # <small>jankenApi関数(2)</small> <div class=footnote> <small><small> (脚注1) もちろんdoJanken()は数値を受け取るべきという設計もありえますし、むしろ、その方が正しい気がします。 ただし、 ここではjankenApi()を「できるだけ短く」「余計なコードを入れずに読みやすく」したいので、 この方向でコーディングしています <br> (脚注2) 一つの関数は一つのことだけするべき。 個人的な理想は一つの関数は10行程度ずつに分解されているべきだと思う。 関数呼び出しのオーバヘッドが少しあるけど、 どんどんCPUは速くなるのでオーバヘッドより保守しやすさ(読みやすさ)のほうが大事 </small></small> </div> <small> - 解答例 <small> - ここでは、じゃんけんの本体 doJanken() にはFORM文で取得した文字列をそのまま渡す仕様になっています。 doJanken()側で、いろいろ頑張ってもらう設計です </small> ``` func jankenApi(w http.ResponseWriter, r *http.Request) { permitAnyCORS(w) setCookie(w, r) jibunString := r.FormValue("jibun") jibun, aite, kekka := doJanken(jibunString) fmt.Fprintf(w, "{\"jibun\":%d,\"aite\":%d,\"kekka\":%d}", jibun, aite, kekka) } ``` </small> --- class: compact # <small>doJanken関数(1)</small> <div class=footnote> <small><small> (脚注) ちなみにC言語でも同様のコーディング(文字列で読みこみatoi()関数で数値に変換)になります。 ただ授業ではscanfを使っているため、こういうコードが登場しませんでした。 なお関数名のatoiはascii to integerの略です </small></small> </div> <small> - ひな形です、がんばってみてください - 注: 文字列を数値に変換する関数は標準ライブラリにあります ``` func doJanken(jibunString string) (int, int, int) { // 1. jibunStringは文字列なので数値に変換します。int変数はjibunとします // 2. コンピュータの手aiteは乱数で決めます // 3. ジャンケンをします。C言語のときと同様なので分かるはずです // 4. jibun aite kekka の三つ組を返します。 // C言語と異なりGo言語では複数の値を一気に返せることに注意 } ``` </small> --- class: compact # <small>doJanken関数(2)</small> <div class=footnote> <small><small> </small></small> </div> <small> - 解答例 ``` func doJanken(jibunString string) (int, int, int) { jibun, err := strconv.Atoi(jibunString) if err != nil { jibun = 0 } // aite (computer) rand.Seed(time.Now().UnixNano()) aite := rand.Intn(3) // kekka kekka := (3 + jibun - aite) % 3 return jibun, aite, kekka } ``` </small> --- class: compact # <small>import文を修正するのを忘れずに</small> <div class=footnote> <small><small> (脚注) ふつうはtimeモジュールも追加することになると思いますが、この演習では、たまたま不要です。 <br> cookieのところで使う都合があるため、すでにlsform.goがtimeを使っていました </small></small> </div> <small> - あらたに乱数を使っているので、import に"math/rand"を追加するのを忘れないように! ``` import ( "fmt" "log" "math/rand" "net/http" "strconv" "time" ) ``` </small> --- name: web-api-03 class: title, smokescreen, shelf, no-footer # <small>Go言語で書くWeb APIサーバ(3)<br>ショッピングカート</small> <div class=footnote> <small><small> (脚注) 授業の裏側の解説とも言います </small></small> </div> --- class: compact # <small>ショッピングカート(HTML)の例</small> <div class=footnote> <small><small> </small></small> </div> <small> - APIサーバ側は、lsformとjankenが分かれば書けるはずなので省略します - ショッピングカートと言っても、単に商品(item-01, ...)と購入数(数値)が入力できるHTMLを用意すれば充分です。 商品の入れ替えなどはサポートしません(これは発展課題じゃないか?:-) ``` <P>SHOPPING CART <form method="POST" action="http://localhost:8080/api/cart/v1"> <P>item-01 <input name="item-01" type="text"> <P>item-02 <input name="item-02" type="text"> <P>item-03 <input name="item-03" type="text"> <P> <input type="submit" value="buy"> </form> ``` </small> --- name: web-api-04 class: title, smokescreen, shelf, no-footer # <small>Go言語で書くWeb APIサーバ(4)<br>ファイルのアップロード</small> <div class=footnote> <small><small> (脚注) 授業の裏側の解説とも言います </small></small> </div> --- class: compact # <small>ファイルのアップロードをするHTML</small> <div class=footnote> <small><small> </small></small> </div> <small> - これも春学期の復習ですね。注意する点はENCODINGの属性を忘れずに追加することです - `enctype="multipart/form-data"` - あとはタイプの指定です。`type=file`とすればブラウザがファイルの選択画面を出してくれます ``` <form method="post" enctype="multipart/form-data" action="http://localhost:8080/api/upload/v1"> <input name="file" type="file"/> <input type="submit" /> </form> ``` - `name="file"`なので、FORMで取り出すときのキーがfileということです - `type="file"`の場合、FORM文のキーfileで取り出せる値は「ファイルの中身」となることに注意 </small> --- class: compact # <small>作業手順</small> <div class=footnote> <small><small> </small></small> </div> <small> - 作業手順 1. ルーティングの設定 1. アップロードAPIの本体を書く - ターミナルで動作確認をする - これをビルドすると、8080/tcp で起動するWWWサーバが起動してきます <br> 停止するときは Ctrl-C - コンテナに組みこむ </small> --- class: compact # <small>ルーティング</small> <div class=footnote> <small><small> </small></small> </div> <small> - URLのパスが"/api/upload/v1"にマッチした場合 uploadApi 関数を呼び出すことにします ``` http.HandleFunc("/api/upload/v1", uploadApi) ``` </small> --- class: compact # <small>uploadApi</small> <div class=footnote> <small><small> </small></small> </div> <small> - コメント付きのヒナ形を以下に示します、まずは頑張ってみてください ``` func jankenApi(w http.ResponseWriter, r *http.Request) { permitAnyCORS(w) setCookie(w, r) // 1. FORM文をparseし、キーfileに対応する値を取り出す // ヒント: type=fileの場合FormFile()という関数を使う // 2. 書きこむファイルを準備する // 3. 書きこむ } ``` </small> --- class: compact # <small>FORM文をparseする</small> <div class=footnote> <small><small> </small></small> </div> <small> - r.FormFileを使うとtype=fileの場合のキーに対応する情報が取り出せます - 返る値は3つ組で「ファイルハンドル、メタデータ、エラー」です - ファイルハンドルが返るので、最後に閉じる必要があります - deferでハンドルを閉じるようにします ``` rh, fileHeader, rerr := r.FormFile("file") if rerr != nil { http.Error(w, rerr.Error(), http.StatusBadRequest) return } defer rh.Close() ``` </small> --- class: compact # <small>書きこむ先を準備する</small> <div class=footnote> <small><small> </small></small> </div> <small> - ここでは書きこむ先は固定("/tmp/file.uploaded")とします - osモジュールを読みこみ、os.Create関数を使います - ここは昔のUnix風なんですね(Createでファイルを作成し、Closeで閉じる) ``` path := "/tmp/file.uploaded" wh, werr := os.Create(path) if werr != nil { http.Error(w, werr.Error(), http.StatusBadRequest) return } defer wh.Close() ``` - Windowsの人は(a)書きこむ先を変えるか(b)"C:\tmp"を作成するかしてくださいね!? <br> 単に"file.uploaded"と書けばホームフォルダの下に生成されるのかなぁ??? </small> --- class: compact # <small>コピーする</small> <div class=footnote> <small><small> </small></small> </div> <small> - `io.Copy()`を使い、ファイルハンドルからファイルハンドルへコピーします - `io.Copy(DST, SRC)`です。引数の順番に注意してください - でもUnixの、このての関数は、だいたい DST SRC の順番なので不自然では無いです - 返る値は「書きこんだサイズ」と「エラー」です ``` n, _ := io.Copy(wh, rh) // verify the uploaded file size if n != fileHeader.Size { http.Error(w, "size unmatched!", http.StatusBadRequest) } ``` - io.Copyの返すエラーは捨てています(`_` で受けているところ) - そのかわりに、ここでは、 変数 n (書きこんだサイズ)とメタデータにあるサイズ(fileHeader.Size)が同じかどうかを検査して、 きちんと書けたのかどうか?を確認しています </small> --- class: compact # <small>モジュールの修正も忘れずに</small> <div class=footnote> <small><small> </small></small> </div> <small> - go fmt しましょう - "io"と"os"が足らないと怒られるはずです。きちんとimport文を直してください - ビルドして動かしてみましょう(動作確認その1) - コンテナに組み込んでみましょう(動作確認その2) </small> --- name: saenai-paas class: title, smokescreen, shelf, no-footer # <small>総合: PaaSを作ってみよう</small> <div class=footnote> <small><small> (脚注) 作ってみよう!とは言うものの、フルスクラッチではなく、あるていどの雛形からのスタートです </small></small> </div> --- class: compact # <small>XXX</small> <div class=footnote> <small><small> </small></small> </div> --- class: compact # <small>XXX</small> <div class=footnote> <small><small> </small></small> </div> --- class: compact # <small>XXX</small> <div class=footnote> <small><small> </small></small> </div> --- class: compact # <small>XXX</small> <div class=footnote> <small><small> </small></small> </div> --- class: compact # <small>XXX</small> <div class=footnote> <small><small> </small></small> </div> --- class: compact # <small>XXX</small> <div class=footnote> <small><small> </small></small> </div> --- class: compact # <small>XXX</small> <div class=footnote> <small><small> </small></small> </div>