- 投稿日:2020-09-22T22:35:34+09:00
【Golang】Go言語の基礎 構造体について①
Goにはオブジェクトと呼ばれるものはありません。したがって、クラスという概念が存在しないです。
その代わりに構造体というものがあり、オブジェクト指向のような書き方をすることが可能です。まずは第一弾として構造体の書き方や初期化の方法などについてみていきます。
②では構造体の埋め込み(オブジェクト指向でいう継承のようなもの。Goには継承はありません。)について書いていこうと思ってます!
構造体を使ってあれこれ書いてみる
構造体は type と structを用いて書くことができます。
package main import "fmt" type Vertex struct { X int Y int //小文字のyにすると外部からアクセスできなくなる S string } func main() { //構造体の初期化 v := Vertex{X: 1, Y: 2} fmt.Println(v) // => {1 2} //構造体のフィールドにアクセス fmt.Println(v.X, v.Y) // => 1 2 //中身を書き換える v.X = 100 fmt.Println(v.X, v.Y) // => 100 2 //このように一部の値を渡すこともできる。その場合、残りはデフォルトの値が入る。(intは0、stringは空文字) v2 := Vertex{X: 1} fmt.Println(v2) // => {1 0 } //構造体に書いてる順番通りに書いてフィールドの値を渡して初期化することもできる v3 := Vertex{1, 2, "huga"} fmt.Println(v3) // => {1 2 huga} //{}で空の構造体を宣言 それぞれの型のデフォルトの値が入る v4 := Vertex{} fmt.Println(v4) // => {0 0 } fmt.Printf("%T %v\n", v4, v4) // => main.Vertex {0 0 } //varで宣言だけした場合はv4と同じ構造体になる。mapやスライスは宣言だけした場合はnilになるので注意。 var v5 Vertex fmt.Println(v5) // =>{0 0 } fmt.Printf("%T %v\n", v5, v5) // =>main.Vertex {0 0 } //ポインタが返ってくる v6 := new(Vertex) fmt.Println(v6) // => &{0 0 } fmt.Printf("%T %v\n", v6, v6) // => *main.Vertex &{0 0 } //ポインタが返ってくる、この書き方だと一目でポインタが返ってくるとわかるのでこちらを使うことが多いようです! v7 := &Vertex{} fmt.Println(v7) // => &{0 0 } fmt.Printf("%T %v\n", v7, v7) // => *main.Vertex &{0 0 } }&を付けてアドレスで引数を渡すと最初に定義した値を書き換えることができる。
package main import "fmt" type Vertex struct { X int Y int //yにするとアクセスできなくなる S string } func changeVertex(v Vertex) { v.X = 1000 } func changeVertex2(v *Vertex) { v.X = 1000 } func main() { v := Vertex{1, 2, "hoge"} //値私なので、宣言した元の構造体は書き換わらない。 changeVertex(v) fmt.Println(v) {1 2 hoge} //ポインタ渡しなので元の構造体を書き換えることができる。 v2 := &Vertex{1, 2, "hoge"} changeVertex2(v2) fmt.Println(v2)&{1000 2 hoge} }なぜポインタ渡しを使うのか?
・値渡しと違って、間接的に参照・操作できる
・上で見たように値渡しだと関数の引数として構造体を渡した場合に、その構造体のコピーが生成されてしまい、元の構造体に影響を与えない。ポインタ渡しだと影響を与えることができる。
・単純にORマッパーなどのライブラリがポインタを使うので使わざるを得ない
・大きな構造体を値渡しするとコピー処理で性能が劣化する。 ポインタ渡しの場合には固定で8バイト>(64bit)/4バイト(32bit)なので性能が劣化しない。
との理由からのようです。初学者の自分にはいまいちピンときませんw
実践で使っていく中で腹で理解していけばいいと思っています。
とりあえずこんなことができるということをまずは頭に叩き込もうと思います!
- 投稿日:2020-09-22T18:04:45+09:00
Goでinterface引数の型がポインタのときに、ポインタの先が示す型を判別する
reflect.ValueOf
,reflect.Value.Elem
を組み合わせるとできる。
(この方法はユーザー定義の型ではなく、言語がデフォルトで用意する型を判別するもの)package main import ( "fmt" "reflect" ) func check(i interface{}) { // interfaceのValueインスタンスを取得 v1 := reflect.ValueOf(i) // ポインタの場合、さらにその先にある型を取得する処理に進む if v1.Kind() == reflect.Ptr { // Elemを利用して、ポインタの先のValueインスタンスを取得 v2 := reflect.ValueOf(v1.Elem()) // Kindを呼ぶと、ポインタの先にある型情報が得られる fmt.Println(v2.Kind()) } } type Foo struct {} func main() { s := Foo{} check(&s) // struct }
- 投稿日:2020-09-22T14:25:02+09:00
Go+MySQL+Dockerで簡単なCRUDを実装する
はじめに
先日、「業務で使う予定ないとはいえGoぐらいある程度知っておいたほうがいいよな...」と思い、Goを使って簡単なCRUDを実装してみたのでそのやり方を備忘録としてまとめておきます。
基本的には以下のサイトの内容を組み合わせて少しアレンジしたものになっています。Goの勉強をする上でこれらのサイトには非常にお世話になったのでこちらもご参考ください。
DockerでGoの開発環境を構築する
Go / Gin で超簡単なWebアプリを作る
Go言語(Golang)入門~MySQL接続編~
docker-compose MySQL8.0 のDBコンテナを作成する
docker-compose upでMySQLが起動するまで待つ方法(2種類紹介)GoをDockerで立ち上げる
まずはGoをDockerで立ち上げていきます。
作業ディレクトリ直下に
- DockerFile
- docker-compose.yml
- main.go
を作成しますDockerFileFROM golang:latest RUN mkdir /app WORKDIR /appdocker-compose.ymlversion: '3' services: go: build: context: . dockerfile: DockerFile command: /bin/sh -c "go run main.go" stdin_open: true tty: true volumes: - .:/appmain.gopackage main import "fmt" func main() { fmt.Println("Hello, World!") }これで
docker-compose up
を行うとコンソール上にHello, World!と出てくると思います。
これが出ればまずGoの起動は成功です。簡単に各ファイルの解説をします。
・DockerFile
Goのコンテナ(仮想環境)を作成します。
ここでWORKDIR /app
を指定していることで以降の動作をすべて/app
以下で行ってくれます。・docker-compose.yml
DockerFileで作ったコンテナを立ち上げるときの設定などを書きます。
これにより、DockerFileにあるコンテナを立ち上げてその中でgo run main.go
のコマンドを叩いてmain.goを起動します・main.go
Goに関する処理はここに書いていきます。今回はHello, World!を出力するだけで終了しています。GoでWebページを作成する
とりあえずGoの起動ができたので次はGoを使ってWebページを作っていきましょう。
今回はGinというフレームワークを使ってみます。
- DockerFileにインストールを追加
- docker-compose.ymlにportsの記述を追加
- main.goの内容を書き換え
- templates/index.htmlを作成
を行ってください。DockerFileFROM golang:latest RUN mkdir /app WORKDIR /app RUN go get github.com/gin-gonic/gindocker-compose.ymlversion: '3' services: go: build: context: . dockerfile: DockerFile command: /bin/sh -c "go run main.go" stdin_open: true tty: true volumes: - .:/app ports: - 8080:8080main.gopackage main import ( "github.com/gin-gonic/gin" ) func main() { router := gin.Default() router.LoadHTMLGlob("templates/*.html") router.GET("/", func(ctx *gin.Context){ ctx.HTML(200, "index.html", gin.H{}) }) router.Run() }templates/index.html<!DOCTYPE html> <html lang="ja"> <head> <meta charset="UTF-8"> <title>Sample App</title> </head> <body> <h1>Hello World!!!</h1> </body> </html>これで
docker-compose build docker-compose up -dを行ってしばらく待ってから
http://localhost:8080にアクセスするとHello World!と表示されると思います。今回やったことを解説します。
今回はGinというフレームワークを追加しました。
GinはDockerFileでコンテナ作成後にgo get github.com/gin-gonic/gin
というコマンドでインストールされ、main.goで呼び出されます。
そしてmain.goの中でtemplatesの中身が読み取られ、router.GET("/", func(ctx *gin.Context){ ctx.HTML(200, "index.html", gin.H{}) })によってroot("/")に対してtemplates/index.htmlが紐づけられることになります。
ちなみにrouter.GETの第一引数("/")を"/test"などに変えると、http://localhost:8080ではなく、http://localhost:8080/testでindex.htmlが表示されるようになります。最後にdocker-compose.ymlにportを追加することでlocalhost:8080へのアクセスを可能にしています。
DockerでMySQLを起動する
ここまででGoでのWebページ作成はできるようになりました。しかし、実際にはWebサービスを作るときにDBとの接続は避けて通れない内容になってきます。
そこで次はDockerを使ってMySQLを立ち上げていきます。まずはdocker-compose.ymlに
・dbコンテナについての記述
・volumeの記述
を追加してください。docker-compose.ymldb: image: mysql:8.0 environment: MYSQL_ROOT_PASSWORD: root MYSQL_DATABASE: go_database MYSQL_USER: go_test MYSQL_PASSWORD: password TZ: 'Asia/Tokyo' command: mysqld --character-set-server=utf8mb4 --collation-server=utf8mb4_unicode_ci volumes: - db-data:/var/lib/mysql - ./db/my.cnf:/etc/mysql/conf.d/my.cnf ports: - 3306:3306 volumes: db-data: driver: localまた、dbディレクトリを作り、その中にmy.cnfファイルを作成してください。
my.cnf[mysqld] character-set-server = utf8mb4 collation-server = utf8mb4_bin default-time-zone = SYSTEM log_timestamps = SYSTEM default-authentication-plugin = mysql_native_password [mysql] default-character-set = utf8mb4 [client] default-character-set = utf8mb4(この辺は参考ページそのままです。ログに関する部分だけなぜか上手くいかなかったので外しています)
ここまでやって
docker-compose up -d
をやるとMySQLのコンテナも立ち上がるはずです。
設定の記述しかないのでここの説明は省略します。GoとMySQLを接続する
MySQLが立ち上がったので早速Goにつないでみます。
今回は接続にsqlドライバーとGORMというフレームワークを使います。
- DockerFileにインストールの追加
- docker-compose.ymlに依存関係の記述を追加
- main.goにDB接続の処理を追加
を行ってください。DockerFileFROM golang:latest RUN mkdir /app WORKDIR /app RUN go get github.com/gin-gonic/gin RUN go get github.com/go-sql-driver/mysql RUN go get github.com/jinzhu/gormdocker-compose.ymlversion: '3' services: go: build: context: . dockerfile: DockerFile command: /bin/sh -c "go run main.go" stdin_open: true tty: true volumes: - .:/app ports: - 8080:8080 depends_on: - "db" db: image: mysql:8.0 environment: MYSQL_ROOT_PASSWORD: root MYSQL_DATABASE: go_database MYSQL_USER: go_test MYSQL_PASSWORD: password TZ: 'Asia/Tokyo' command: mysqld --character-set-server=utf8mb4 --collation-server=utf8mb4_unicode_ci volumes: - db-data:/var/lib/mysql - ./db/my.cnf:/etc/mysql/conf.d/my.cnf ports: - 3306:3306 volumes: db-data: driver: localmain.gopackage main import ( "fmt" "time" "github.com/gin-gonic/gin" "github.com/jinzhu/gorm" _ "github.com/go-sql-driver/mysql" ) func main() { db := sqlConnect() defer db.Close() router := gin.Default() router.LoadHTMLGlob("templates/*.html") router.GET("/", func(ctx *gin.Context){ ctx.HTML(200, "index.html", gin.H{}) }) router.Run() } func sqlConnect() (database *gorm.DB) { DBMS := "mysql" USER := "go_test" PASS := "password" PROTOCOL := "tcp(db:3306)" DBNAME := "go_database" CONNECT := USER + ":" + PASS + "@" + PROTOCOL + "/" + DBNAME + "?charset=utf8&parseTime=true&loc=Asia%2FTokyo" count := 0 db, err := gorm.Open(DBMS, CONNECT) if err != nil { for { if err == nil { fmt.Println("") break } fmt.Print(".") time.Sleep(time.Second) count++ if count > 180 { fmt.Println("") fmt.Println("DB接続失敗") panic(err) } db, err = gorm.Open(DBMS, CONNECT) } } fmt.Println("DB接続成功") return db }これで
docker compose up
を行い、コンソールに「DB接続成功」と出たら成功です。追加された内容はsqlConnectがメインなのでそこを解説します。
func sqlConnect() (database *gorm.DB) { DBMS := "mysql" USER := "go_test" PASS := "password" PROTOCOL := "tcp(db:3306)" DBNAME := "go_database" CONNECT := USER + ":" + PASS + "@" + PROTOCOL + "/" + DBNAME + "?charset=utf8&parseTime=true&loc=Asia%2FTokyo" count := 0 db, err := gorm.Open(DBMS, CONNECT) if err != nil { for { if err == nil { fmt.Println("") break } fmt.Print(".") time.Sleep(time.Second) count++ if count > 180 { fmt.Println("") fmt.Println("DB接続失敗") panic(err) } db, err = gorm.Open(DBMS, CONNECT) } } fmt.Println("DB接続成功") return db }前半部はDBに接続するための情報を定義しています。docker-compose.ymlで設定した内容を入力してください。
その後、db, err := gorm.Open(DBMS, CONNECT)
でDBに接続します。しかし、MySQLの起動時間によってはこのコマンドが実行される時点でMySQLの準備が完了していない場合があります。
そこでこのコードでは2つの対策をしています。1つめはdocker-compose.ymlでの依存関係の設定です。
ここでdepends_onを設定したことにより、dbコンテナが立ち上がってからgoコンテナが立ち上がるようになります。2つめはリトライ処理です。
dbコンテナが起動してからもMySQLが立ち上がるまでに時間がかかるのでもしDBにつながらなかった場合に1秒待ってからリトライするようにしています。
これだと本当にエラーのときにリトライし続けてしまうので適当な回数でエラーを返すようにします。このコードでは3分つながらなかったらエラーになるようになっています。CRUDを実装する
ついにMySQLにもつながるようになったので最後にCRUDの処理を実装して実際の流れをみていきましょう。
あと変更するのはmain.goとindex.htmlのみです。
- Userの定義を作成
- マイグレーション
- post処理の実装
- ユーザー追加フォーム、ユーザー削除ボタンの実装
をやっていきます。main.gopackage main import ( "fmt" "strconv" "time" "github.com/gin-gonic/gin" "github.com/jinzhu/gorm" _ "github.com/go-sql-driver/mysql" ) type User struct { gorm.Model Name string Email string } func main() { db := sqlConnect() db.AutoMigrate(&User{}) defer db.Close() router := gin.Default() router.LoadHTMLGlob("templates/*.html") router.GET("/", func(ctx *gin.Context){ db := sqlConnect() var users []User db.Order("created_at asc").Find(&users) defer db.Close() ctx.HTML(200, "index.html", gin.H{ "users": users, }) }) router.POST("/new", func(ctx *gin.Context) { db := sqlConnect() name := ctx.PostForm("name") email := ctx.PostForm("email") fmt.Println("create user " + name + " with email " + email) db.Create(&User{Name: name, Email: email}) defer db.Close() ctx.Redirect(302, "/") }) router.POST("/delete/:id", func(ctx *gin.Context) { db := sqlConnect() n := ctx.Param("id") id, err := strconv.Atoi(n) if err != nil { panic("id is not a number") } var user User db.First(&user, id) db.Delete(&user) defer db.Close() ctx.Redirect(302, "/") }) router.Run() } func sqlConnect() (database *gorm.DB) { DBMS := "mysql" USER := "go_test" PASS := "password" PROTOCOL := "tcp(db:3306)" DBNAME := "go_database" CONNECT := USER + ":" + PASS + "@" + PROTOCOL + "/" + DBNAME + "?charset=utf8&parseTime=true&loc=Asia%2FTokyo" count := 0 db, err := gorm.Open(DBMS, CONNECT) if err != nil { for { if err == nil { fmt.Println("") break } fmt.Print(".") time.Sleep(time.Second) count++ if count > 180 { fmt.Println("") panic(err) } db, err = gorm.Open(DBMS, CONNECT) } } return db }templates/index.html<!DOCTYPE html> <html lang="ja"> <head> <meta charset="UTF-8"> <title>Sample App</title> </head> <body> <h2>ユーザー追加</h2> <form method="post" action="/new"> <p>名前<input type="text" name="name" size="30" placeholder="入力してください" ></p> <p>メールアドレス<input type="text" name="email" size="30" placeholder="入力してください" ></p> <p><input type="submit" value="Send"></p> </form> <h2>ユーザー一覧</h2> <table> <tr> <td>名前</td> <td>メールアドレス</td> </tr> {{ range .users }} <tr> <td>{{ .Name }}</td> <td>{{ .Email }}</td> <td> <form method="post" action="/delete/{{.ID}}"> <button type="submit">削除</button> </form> </td> </tr> {{ end }} </ul> </body> </html>これで
docker-compose up -d
を行い、http://localhost:8080にアクセスするとユーザー登録フォームが現れ、ユーザーを登録すると下に登録したユーザーの情報が表示されるようになります。
また、コンテナを削除して上げ直しても登録されたユーザーは削除されず、ユーザー一覧に表示が残るようになります。それでは追加部分の解説をしていきます。
まず、main.goでUserという構造体を作成しています。gorm.Model
でidなどモデルに必要な内容をUserに入れ、更にUser特有のname, emailを追加しています。
この構造はdb.AutoMigrate
によってDBに反映されます。続いて各パスでのCRUD処理を実装していきます。
rootパスではユーザー一覧を取得します。
db.Find(&users)でDB内にあるユーザー一覧をUser構造として取得します。
間にOrderを挟むことで取得時に古いユーザーが上に来るようにしています。
最後に取得したユーザーをindex.htmlに渡しています。/newパスではフォームの内容をもとにユーザーを作成しています。
ctx.PostFormでフォームによってsubmitされた内容を取得し、その内容をdb.Createで永続化しています。
処理が終わったらrootにリダイレクトします。/deleteパスではidを指定してユーザーを削除しています。
こちらではURLにユーザーのidを指定しているのですが、同様にctxから取得します。
そしてその内容からdb.Firstでユーザーを取得し、db.Deleteで該当のユーザーを削除します。
ここで、idはstringで渡されているのでstrconv.Atoiでint型に変換していることに注意してください。index.htmlでは一般的なhtmlの書き方でformとtableを作成しています。
ここで、{{ range .users }}
という形でmain.goから渡されたusersを受け取っています。おわりに
今回はGoでのWebサービス開発の導入としてGo+MySQL+Dockerで簡単なCRUDを実装してみました。あくまで練習なのでバリデーションとか細かい制御などは考えていません。
今回行った内容は初歩ではありますが、この内容を広げて複雑化していくことで実際にWebサービスを作ることができると思います。
もしGoで何か作ってみたいと考えている方がいらっしゃったらぜひ参考にしてみてください!
- 投稿日:2020-09-22T11:56:03+09:00
Goで画像をグレースケールにするCLIツールを作った
はじめに
以前、Goで画像の形式を変換するCLIツールを作った を書いたのですが、
今回はGoで画像をグレースケールにするCLIツールを作りました。pngとjpgの画像をグレースケースへと変換してくれます。
機能
オプション 説明 デフォルト -r 変換前の画像を削除 false 使い方
$ # デフォルト、変換前の画像は削除されない $ ./main sample.jpeg$ 変換前の画像を削除する $ ./main -r sample.jpegコード
ディレクトリ構成は以下の通りです。
├── cmd │ └── img2gray │ └── main.go ├── img2gray.go └── go.modまずはimg2gray.goから見ていきます。
img2gray.gopackage img2gray import ( "flag" "image" "image/color" "image/jpeg" "image/png" "os" "path/filepath" ) // 変換前の画像を削除する func removeSrc(src string) error { if err := os.Remove(src); err != nil { return err } return nil } func ToGray(src, dst string, rmsrc bool) error { sf, err := os.Open(src) if err != nil { return err } defer sf.Close() img, _, err := image.Decode(sf) if err != nil { return err } bounds := img.Bounds() // 画像の境界を取得 grayImg := image.NewGray16(bounds) // 与えた境界を持つ新しいGray16を返す。 // 全画素走査 for y := bounds.Min.Y; y < bounds.Max.Y; y++ { for x := bounds.Min.X; x < bounds.Max.X; x++ { // グレースケールへコンバート c := color.Gray16Model.Convert(img.At(x, y)) gray, _ := c.(color.Gray16) // 同じ画素にセット grayImg.Set(x, y, gray) } } df, err := os.Create(dst) if err != nil { return err } defer df.Close() switch filepath.Ext(src) { case ".png": err = png.Encode(df, grayImg) case ".jpeg", ".jpg": err = jpeg.Encode(df, grayImg, &jpeg.Options{Quality: jpeg.DefaultQuality}) } if err != nil { return nil } if rmsrc { if err = removeSrc(src); err != nil { return err } } return nil }続いてcmd/img2gray/main.goです。
cmd/img2gray/main.gopackage main import ( "flag" "fmt" "os" "path/filepath" "github.com/Le0tk0k/img2gray" ) var rm = flag.Bool("r", false, "Remove sorce file") // ファイル名から拡張子以降を取り除く func getFileNameWithoutExt(file string) string { return file[:len(file)-len(filepath.Ext(file))] } func main() { flag.Parse() src := flag.Arg(0) dst := getFileNameWithoutExt(src) + "_gray" + filepath.Ext(src) err := img2gray.ToGray(src, dst, *rm) if err != nil { fmt.Fprintf(os.Stderr, "%s\n", err) } }以上です!
- 投稿日:2020-09-22T11:36:02+09:00
html/templateメモ
html/templateメモ
目次
html/templateの概要
htmlのテンプレートにこちらのプログラムで処理したデータを埋め込み返す。webサービスには必須の技術だが、それをgolangを実現するにはテンプレートエンジンであるhtml/templateを使う
正確には汎用テンプレートエンジンであるtext/template標準ライブラリと、さらにHTML専用のテンプレートエンジンであるhtml/templateライブラリの二種類があるが、ここでは後者を扱う
templateとは
template(テンプレート) とは予め用意されたhtmlのひな型
テンプレートエンジン とはテンプレートとデータ 1を組み込んで、クライアントに返す最終的なhtmlを生成するプログラム
通常、ハンドラがテンプレートエンジンを呼び出してデータをテンプレートに組み込み、できあがったHTMLをクライアントに返す
イメージ図12
基本文法
Go言語のテンプレートはテキストドキュメントであり(Webアプリの場合、通常はHTML
ファイル)、アクション と呼ばれる何らかのコマンドが埋め込まれたものテンプレートはファイル全体であることもあれば、ファイルの一部しかテンプレートとして宣言されていないこともある
アクションが埋め込まれたテキスト、すなわちテンプレートがテンプレートエンジンによって解析(構文解析)され、「実行」されて新たなテキストが生成される
Go言語では任意のアクションは
{{...}}
で囲って記述するプレーンなアクション
- 単純なテンプレートの例を示す
templ.html<!DOCTYPE html> <html> <head> <meta http-equiv="Content-Type" content="text/html; charset=utf-8"> <title>Go Web Programming</title> </head> <body> {{ . }} </body> </html>
{{ . }}
の中のドット「.
」 がアクションであり、テンプレートエンジンがテンプレートを実行したときに、「その部分をデータで置換える」という意味を持つコマンド次は上で定義したテンプレートから、最終的なhtmlを作成するgoスクリプトの方を見てみる
main.gopackage main import ( "net/http" "html/template" ) func process(w http.ResponseWriter, r *http.Request) { t, _ := template.ParseFiles("tmpl.html") t.Execute(w, "hoge") } func main() { server := http.Server{ Addr: "127.0.0.1:8080", } http.HandleFunc("/process", process) server.ListenAndServe() }
t, _ := template.ParseFiles("tmpl.html")
関数ParseFilesでテンプレートファイルtmpl.htmlを解析(コンパイル)する。
この関数は解析済みテンプレート(Template型)とエラーを返す因みに、errorではなくパニックに変換するためにMust()を合わせて利用することもある
var t = template.Must(template.ParseFiles("index.html"))
t.Execute(w, "hoge")
Executeメソッドを呼び出してデータ(この例では「hoge」)をコンパイル済みテンプレートに当てはめている。
生成されたhtmlはt.Execute()の第一引数で指定したio.Writerであるhttp.ResponseWriter
に書き込まれる。
Fprint系の関数とやっていることは似ている├── main.go
└── templ.html
以上二つのファイルを上のようなディレクトリ構成にしておくと、ビルド後に無事htmlファイルを返せるようになる渡されるデータが構造体である場合
上の例で見たのは文字列"hoge"をデータとして渡す方法だった。「
.
」はそのまま"hoge"と置き換えられるので分かりやすいしかし一般的なのは構造体を渡すことで、そのメンバ変数がデータとして使われる。例えば先ほどのmain.goを次のように変えた場合だとどうするか
main.go... type Person struct{ Name:string Sex:string } var person Person = { Name:"HOGE" Sex:"Male" //personのメンバ変数NameとSexをテンプレートに埋め込みたい } func process(w http.ResponseWriter, r *http.Request) { t, _ := template.ParseFiles("tmpl.html") t.Execute(w, person) //構造体personが渡されている ...
- 答えは次のようになる
template.html<!DOCTYPE html> <html> <head> <meta http-equiv="Content-Type" content="text/html; charset=utf-8"> <title>Go Web Programming</title> </head> <body> {{ .Name }} {{ .Sex }} </body> </html>
ドット「
.
」単体では構造体そのものとの置き換えを指すが、.Name
,.Sex
とすることで特定のメンバ変数との置き換えという意味になる
構造体名.メンバ変数名
でメンバ変数の値にアクセスできるのと近いものを感じるその他アクション
テンプレートに渡されたデータと置き換えるドット「
.
」のコマンドは最も基本的なアクションだった。
しかし「.
」以外にもアクションは存在する。それらは単純な置き換えとは違う、よりプログラム寄り(条件文やループ)の処理を行う条件アクション
- 条件アクションとは、多数のデータ評価値の中から引数の値に応じて1つを選択するものである。次の形式で表せる。(ただしargは任意の引数)
{{ if arg }} コンテンツ {{ end }}
- もう1つのタイプは次の形式。
{{ if arg }} コンテンツ {{ else }} 他のコンテンツ {{ end }}イテレータアクション
- イテレータアクションとは、配列やスライス、マップ、チャネルの要素ごとに反復処理を行うアクション。反復ループの内部では、ドットに配列やスライス、マップ、チャネルの要素が次々に設定される。 書式は次の通り
{{ range array }} #arrayは大抵データとして渡されたスライスやマップのイメージ {{ . }} #range内でドットに要素が設定される {{ end }}
- arrayがnilだった場合のフォールバックも存在する
代入アクション
代入アクションを使うと、そのアクションで囲まれたセクション内でargに指定した有効な値をドット「
.
」の値として設定できる。書式は次の通り{{ with arg }} このセクション内でドットにargが設定される {{ end }}
- argが空白だった場合のフォールバックも存在する
インクルードアクション
- テンプレートに別のテンプレートを差し込むことができる。書式は次の通り
{{ template "name" }}
- このアクションは次の章で詳しく説明する
テンプレートを分割したい
テンプレートAとテンプレートBがあったとき、bodyの要素は違ってもヘッダーは共通にしたい
というような、テンプレートを部品化して再利用する需要が出てくる。
この場合、共通部品であるヘッダーをテンプレートCとして作成してそれぞれのテンプレートに埋め込めばよい静的なテンプレートの追加方法
インクルードアクション
{{ template "name" }}
を使えばテンプレートに別のテンプレートを差し込むことができる。単純な例を見る。
2つのテンプレートファイル、t1.htmlとt2.htmlを作成する。この例ではテンプレートt1.htmlがt2.htmlをインクルードする。t1.html<!DOCTYPE html> <html lang="en"> <head> <meta charset="utf-8"> <meta http-equiv="X-UA-Compatible" content="IE=9"> <title>Go Web Programming</title> </head> <body> <div> ここは、t1.html(インクルードの前)</div> <div>t1.html内でのドットの値 - [{{ . }}]</div> <hr/> {{ template "t2.html" }} <hr/> <div> ここは、t1.html(インクルードの後)</div> </body>t2.html<div style="background-color: yellow;"> ここはt2.htmlです<br/> </div>
- ハンドラは次のようにする
main.gopackage main import ( "html/template" "net/http" ) func process(w http.ResponseWriter, r *http.Request) { t, _ := template.ParseFiles("template/t1.html", "template/t2.html") t.Execute(w, "こんにちは") } func main() { server := http.Server{ Addr: "127.0.0.1:8080", } http.HandleFunc("/", process) server.ListenAndServe() }
t, _ := template.ParseFiles("template/t1.html", "template/t2.html")
に注目すると、
今までと違い、ParseFilesメソッドの引数が二つになっている。
ParseFilesメソッドは可変長引数関数であり、コンパイルするファイルを複数指定することができるParseFiles関数の引数が可変長でも、返り値としてのコンパイル済みテンプレートは一つしかなく、それは最終的にレスポンスとして返すhtmlのテンプレートである。
その主要素はもちろん親の(ここではt1.html)テンプレートであり、親のテンプレートは必ず第一引数に指定しなくてはならない。逆にインクルードされる部品としてのテンプレートは第二引数以降に指定するディレクトリ構成はこのようになっている
├── main.go
└── template
└── t2.html
└──t1.html
http://127.0.0.1:8080/process
にアクセスすると以下のページが表示される動的なテンプレートの追加方法
先ほどの例では部品テンプレート(t2.html)は静的だった
部品テンプレートにも動的なデータを含みたい場合は次のような書式になる
{{ template "name" arg}}
{{template "name"}}と違うのは後ろに引数argがくっついている点
こうすることで、インクルードされるテンプレートに渡すデータを指定できるさきほどの例を変更して、t2.htmlにもデータが渡るようにしてみる
t1.html<!DOCTYPE html> <html lang="en"> <head> <meta charset="utf-8"> <meta http-equiv="X-UA-Compatible" content="IE=9"> <title>Go Web Programming</title> </head> <body> <div> ここは、t1.html(インクルードの前)</div> <div>t1.html内でのドットの値 - [{{ . }}]</div> <hr/> {{ template "t2.html" . }} #argにアクション「.」が指定されている <hr/> <div> ここは、t1.html(インクルードの後)</div> </body>t2.html<div style="background-color: yellow;"> ここはt2.htmlです<br/> t2.htmlに渡された値 - [{{ . }}] </div>スコープの話
アクション「
.
」はt.Execute(w, テンプレートに渡すデータ)
で指定したデータと置き換えられること、そのデータが構造体であれば「.メンバ変数
」とするとそのメンバ変数と置き換えられることを既に伝えたその性質を利用して入れ子の構造体を自在に扱えたりもする
次のようなt1.html,t2.html,main.goを作成する
t1.html<!DOCTYPE html> <html lang="en"> <head> <meta charset="utf-8"> <meta http-equiv="X-UA-Compatible" content="IE=9"> <title>Go Web Programming</title> </head> <body> <div> ここは、t1.html(インクルードの前)</div> <div>性別 - [{{ .Sex }}]</div> <hr/> {{ template "t2.html" .Name }} <hr/> <div> ここは、t1.html(インクルードの後)</div> </body>t2.html<div style="background-color: yellow;"> ここはt2.htmlです [{{ .Givenname }}] [{{ .Familyname }}] </div>main.gopackage main import ( "html/template" "net/http" ) type Name struct { Givenname string Familyname string } type Person struct { Name Name Sex string } func process(w http.ResponseWriter, r *http.Request) { person := Person{ Name: Name{ Givenname: "小町", Familyname: "烏丸", }, Sex: "Famale", } t, err := template.ParseFiles("template/t1.html", "template/t2.html") if err != nil { print(err) } t.Execute(w, person) } func main() { server := http.Server{ Addr: "127.0.0.1:8080", } http.HandleFunc("/", process) server.ListenAndServe() }
main.goで入れ子になった構造体を定義し、親の構造体のメンバ変数はt1.htmlへ、入れ子の構造体のメンバ変数はt2.htmlに渡している
t1.htmlの
{{ template "t2.html" .Name }}
に注目すると、t2.htmlに渡されるデータが.Name
になっているこうすることでt2.htmlでは「
.
」のスコープはName構造体に限定されるため、t2.htmlでは入れ子になっている構造体のメンバ変数へのアクセスは[{{ .given_name }}]
とスマートになっている
http://127.0.0.1:8080/process
にアクセスすると以下のページが表示される余談
頭文字が大文字であれば外部から参照できる、というのがGo言語の特徴
先ほどの例ではアクション「
.
」を通じて構造体を渡しているが、その過程でこちらのパッケージを外部に参照させる処理が含まれているのか
main.go
でtype Name struct { givenname string familyname string }
- のようにName構造体のメンバ変数の頭文字を小文字に変更すると、テンプレートに値が渡らない
参考
Go の html/template でヘッダーやフッター等の共通化を実現する方法
Goプログラミング実践入門 ―標準ライブラリでゼロからWebアプリを作る―
大抵はクライアントから渡される情報を指す。ユーザ名など。テンプレートは既に用意されたものであるため静的で、データはクライアントによって変わるものであるため動的(ただしこれから見ていく例では単純化するためサーバサイドで定義した文字列等を使っている) ↩
ハンドラがテンプレートエンジンを呼び出して、使用するテンプレートを通常はテンプレートのリストとして、動的なデータと一緒に渡します。テンプレートエンジンはHTMLを生成して、ResponseWriterにそれを書き込み、さらにResponseWriterはクライアントに送り返すHTTPレスポンスにそれを追加します。(イメージ図共に『Goプログラミング実践入門』より引用) ↩