- 投稿日:2021-06-23T21:06:19+09:00
Go言語でTODOアプリを作ってみた
はじめに メルカリさんの「プログラミング言語Go完全入門」を完走し、その後はGoプログラミング実践入門 標準ライブラリでゼロからWebアプリを作るという書籍を読み終えたので、ここら辺でGoだけを使った簡単なWEBアプリを作りたいと思い、TODOアプリを作成してみました。 こんなやつ作りました 簡易ですが、ご覧の通りです。HOMEボタンと、NEWボタン(新しくタスクを登録する)を上部に置き、その下はタスクを一覧表示しています。Editボタンで編集画面に行き、doneボタンで押下したタスクが削除されるという感じです。 データベースにはPostgreSQLを使用しました。(まだ触ったことのないDBを触ってみたかったので) ディレクトリ構成 todo-app ├ templates │ ├ Edit.tmpl │ ├ Header.tmpl │ ├ Index.tmpl │ ├ Menu.tmpl │ └ New.tmpl └ main.go テーブル作成 create table todo( id serial PRIMARY KEY, task varchar(50) ); メインパッケージ main.go package main import ( "database/sql" "net/http" "text/template" _ "github.com/lib/pq" ) type Todo struct { Id int Task string } func main() { http.HandleFunc("/", Index) http.HandleFunc("/new", New) http.HandleFunc("/edit", Edit) http.HandleFunc("/create", Create) http.HandleFunc("/delete", Delete) http.HandleFunc("/update", Update) http.ListenAndServe("localhost:8080", nil) ・・・① } func dbConn() (db *sql.DB) { ・・・② psqlInfo := "host=localhost user=×× password=×× dbname=×× port=5432 sslmode=disable" db, err := sql.Open("postgres", psqlInfo) if err != nil { panic(err.Error()) } return db } var tmpl = template.Must(template.ParseGlob("templates/*")) ・・・③ func Index(w http.ResponseWriter, r *http.Request) { db := dbConn() selDB, err := db.Query("SELECT * FROM todo;") if err != nil { panic(err.Error()) } td := Todo{} res := []Todo{} for selDB.Next() { ・・・④ var id int var task string err = selDB.Scan(&id, &task) if err != nil { panic(err.Error()) } td.Id = id td.Task = task res = append(res, td) } tmpl.ExecuteTemplate(w, "Index", res) } func New(w http.ResponseWriter, r *http.Request) { tmpl.ExecuteTemplate(w, "New", nil) } func Edit(w http.ResponseWriter, r *http.Request) { db := dbConn() uId := r.URL.Query().Get("id") selDB, err := db.Query("SELECT * FROM todo WHERE id=$1", uId) if err != nil { panic(err.Error()) } td := Todo{} for selDB.Next() { var id int var task string err = selDB.Scan(&id, &task) if err != nil { panic(err.Error()) } td.Id = id td.Task = task } tmpl.ExecuteTemplate(w, "Edit", td) } func Create(w http.ResponseWriter, r *http.Request) { db := dbConn() if r.Method == "POST" { task := r.FormValue("task") crtForm, err := db.Prepare("INSERT INTO todo(task) VALUES($1);") if err != nil { panic(err.Error()) } crtForm.Exec(task) } http.Redirect(w, r, "/", 301) } func Delete(w http.ResponseWriter, r *http.Request) { db := dbConn() td := r.URL.Query().Get("id") delForm, err := db.Prepare("DELETE FROM todo WHERE id=$1;") if err != nil { panic(err.Error()) } delForm.Exec(td) http.Redirect(w, r, "/", 301) } func Update(w http.ResponseWriter, r *http.Request) { db := dbConn() if r.Method == "POST" { task := r.FormValue("task") udtForm, err := db.Prepare("UPDATE todo SET task=$1 WHERE id=$2;") if err != nil { panic(err.Error()) } udtForm.Exec(task) } http.Redirect(w, r, "/", 301) } テンプレート側(ここではTOP画面のみ載せておきます) Index.tmpl {{ define "Index" }} {{ template "Header" }} {{ template "Menu" }} <table border="1"> <thead> <tr> <td>ID</td> <td>TASK</td> <td>Edit</td> <td>Press [done] when you finish.</td> </tr> </thead> <tbody> {{ range . }} <tr> <td>{{ .Id }}</td> <td>{{ .Task }}</td> <td><a href="/edit?id={{ .Id }}">Edit</a></td> <td><a href="/delete?id={{ .Id }}">done</a><td> </tr> {{ end }} </tbody> </table> {{ end }} 解説 ① あまり本編とは関係ないですが、私の環境(macOS BigSur v11.4, Go v1.15.1)では、go run main.goするたびに、ファイアウォールが出てしまっていたので、こちらを参考にさせていただき解消しました。 ② *sql.DB型を戻り値に指定することによって、異なるそれぞれの関数から呼び出せるようにしました。 ③ ParseGlob()は、パターンによってマッチしたファイルのリストを持つParseFilesを呼び出すことと同義のため、Must()の引数にとります。Must()は、変数の初期化に用いられ、*Template型を返すので、ここで定義しておきます。(あとで、ExecuteTemplate()でテンプレートをwriterに書き込むため) ④ SELECTしてきた値が格納されているselDBをNext()を使ってグルグル回してその値を取得して、Todo構造体型が入るスライスにどんどんappendしていきます。 工夫・苦戦した点 DB接続にはGORMなどを使用せずの作成 対応するドライバーでもともとどのようにDB接続し、どのようにステートメントを投げるのか知りたかったので、今回はGORMなどは使いませんでした。(GORMもベースは同じ感じだと思いますが) PostgreSQLのテーブル作成時、idカラムにSERIAL型を設定していなかったため、AUTO_INCREMENTしてくれなかった MySQLしか馴染みがないので、カラム作成したら勝手にINSERT時に値が自動連番で登録されると思っていたけど違いました。笑 PostgreSQLでMySQLのAUTO_INCREMENTを使うを参考に、SERIAL型をidカラムに指定して、テーブルを作り直したところ解決しました。 最後に PostgreSQLの使い方でかなり時間を使ってしまった感はあります。笑 しかし、今回の作成でDBとの接続方法や、ステートメントの投げ方、テンプレート(恐らく実務では使わなさそうですが)側と、処理側の値の表現方法が理解できました。次は、もう少し凝った物を作ろうと思います(テストも書けるようになりたい)。 Standard Go Project Layoutも目を通しておきたいな。また何かアプトプットができましたら、執筆しようと思います。 最後までご拝読いただき、有難うございます。 ※まだ未熟なGopherなので、お気づきの点ございましたらコメント頂けますと幸いです。 参考 pq v1.10.2 ローカルPCでGoでserverを立ち上げたときにファイアウォールを出なくさせる PostgreSQLでMySQLのAUTO_INCREMENTを使う [PostgreSQL] よく使うコマンドまとめ nobonoboさんのZenn
- 投稿日:2021-06-23T20:23:23+09:00
【Golang】[64]byte や [32]byte を []byte にする(配列をスライスに変換する)
固定長の配列を可変長の配列にしたい。 つまり、[size]T の配列を、[]T のスライスに変換したいのです。しかし、キャストしてもエラーが出ます。 キャストできない系 foo := [64]byte{} bar := []byte(foo) // Output: cannot convert foo (type [64]byte) to type []byte TL; DR (今北産業) [:] を使ってslicing する(スライス化して割り当てる) foo[:] は foo[0:len(foo)] と同等 foo := [64]byte{} bar := foo[:] // 型を確認する fmt.Println(reflect.TypeOf(foo)) fmt.Println(reflect.TypeOf(bar)) hoge := [128]string{} fuga := hoge[:] // 型を確認する fmt.Println(reflect.TypeOf(hoge)) fmt.Println(reflect.TypeOf(fuga)) // Output: [64]uint8 // []uint8 // [128]string // []string オンラインで動作を見る @ Go Playground 参考文献 How to compare [32]byte with []byte in golang? @ StackOverflow Golang: 配列からスライスに変換する @ something tech. Goのarrayとsliceを理解するときがきた @ Qiita
- 投稿日:2021-06-23T18:23:45+09:00
gormのmany2manyはこうすると上手く行く
Go言語のgormというORマッパーを使った時のハマったところとその解決についてです。ほぼ自分用メモです。 尚、2021/06/23現在の最新バージョン gorm.io/gorm v1.21.11 を使用しています。 駄目だった実装 https://gorm.io/ja_JP/docs/many_to_many.html gormのドキュメントを見ながら書いた最初のコードはこんな感じでした。 type Person struct { PersonId string `json:"person_id" gorm:"primaryKey;autoIncrement:false"` Languages []Language `gorm:"many2many:person_languages;"` } type Language struct { LanguageCode string `json:"language_code" gorm:"primaryKey;autoIncrement:false"` Name string `json:"name"` } type PersonLanguages struct { PersonId string `json:"person_id" gorm:"primaryKey;autoIncrement:false"` LanguageCode string `json:"language_code" gorm:"primaryKey;autoIncrement:false"` } ここから、personを取得した時に紐づくlanguagesを一気に取得したい、というのがもともとの動機です。 しかし、 result := DB.First(&person) とした時、personに紐付いている中間テーブルを介して取得できるlanguagesがあるはずなのに、 len(person.Languages) はゼロ。 gormのドキュメントを見ただけでは、なぜうまく行かないのかわからず。 上手く行った実装 Preloadする キーとなるカラム名からテーブル名prefixを取る この2点が必要でした。 1. Preloadする DB.Preload("Languages").First(&person) 参照 https://note.com/note_fumi/n/n29a8d64ce46d 2. キーとなるカラム名からテーブル名prefixを取る PersonのPersonIdをIdに、 LanguageのLanguageCodeをCodeにするべきでした。 type Person struct { Id string `json:"id" gorm:"primaryKey;autoIncrement:false"` Languages []Language `gorm:"many2many:person_languages;"` } type Language struct { Code string `json:"code" gorm:"primaryKey;autoIncrement:false"` Name string `json:"name"` } こういうことです。当然ですが、マイグレーションからやり直す必要があります。 こうすると、 DB.Preload("Languages").First(&person) で person.Languages でpersonに紐づくLanguagesがちゃんと取得できます。 ドキュメントにこのような記載が無かったので少し混乱しましたが、自分の仮説が当たっていたようでした。(すごく熟読したわけじゃないので、実は書いてあるかも?) 調査していないですが、gormの挙動から想像するに、has manyでも同じことが起きそうです。 ※元のコードから脚色を加えたものを掲載しているため、このままでは上手く動かない可能性があります。 記事を書き慣れてないので、読みづらかったらすみません。
- 投稿日:2021-06-23T16:29:58+09:00
Goの構造体に埋め込んで、フィールドやメソッドを明示的に指定しなくてもアクセスできる
はじめに Goの構造体で明示的に指定しなくてもメソッドもしくはフィールドにアクセスできる方法を記しています。 ようするに埋め込みです。 Embedding言葉を知らない人でもHitするようなタイトルにしてます。 フィールド名もしくはメソッド名は同じでない場合は明示的に指定しなくてよい 逆に同じ名前があるなら明示的にしてければならない main.go type Hoge struct { Aaa string Bbb string Ccc string } func(h *Hoge) Get() string { return "fuga" } func(h *Hoge) GetHoge() string { return "get hoge" } type Fuga struct { Aaa string Bbb string Ddd string } func(f *Fuga) Get() string { return "fuga" } func(f *Fuga) GetFuga() string { return "get fuga" } type HogeFuga struct { Hoge Fuga } func main() { hoge := Hoge{ Aaa: "hoge", Bbb: "hogehoge", Ccc: "hogehogehoge", } fuga := Fuga{ Aaa: "fuga", Bbb: "fugafuga", Ddd: "fugafugafuga", } hogefuga := HogeFuga{ Hoge: hoge, Fuga: fuga, } //- フィールド名 fmt.Println(hogefuga.Aaa) //- エラーが発生 フィールド名が一緒なので fmt.Println(hogefuga.Bbb) //- エラーが発生 フィールド名が一緒なので fmt.Println(hogefuga.Ccc) //- hogehogehoge fmt.Println(hogefuga.Ddd) //- fugafugafuga fmt.Println(hogefuga.Hoge.Aaa) //- hoge fmt.Println(hogefuga.Hoge.Bbb) //- hogehoge fmt.Println(hogefuga.Hoge.Ccc) //- hogehogehoge fmt.Println(hogefuga.Fuga.Aaa) //- fuga fmt.Println(hogefuga.Fuga.Bbb) //- fugafuga fmt.Println(hogefuga.Fuga.Ddd) //- fugafugafuga //- メソッド名 fmt.Println(hogefuga.Get()) //- エラーが発生 メソッド名が一緒なので fmt.Println(hogefuga.GetHoge()) //- get hoge fmt.Println(hogefuga.GetFuga()) //- get fuga fmt.Println(hogefuga.Hoge.Get()) //- hoge fmt.Println(hogefuga.Fuga.Get()) //- fuga fmt.Println(hogefuga.Hoge.GetHoge()) //- get hoge fmt.Println(hogefuga.Fuga.GetFuga()) //- get fuga } まとめ 埋め込みをする場合はフィールド名とメソッド名が同じでも問題ないが 呼び出すときに同じ命名は明示的に指定して呼び出すこと