20200922のGoに関する記事は5件です。

【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)なので性能が劣化しない。

引用元:Golangのポインタ渡し初心者を卒業した

との理由からのようです。初学者の自分にはいまいちピンときませんw
実践で使っていく中で腹で理解していけばいいと思っています。
とりあえずこんなことができるということをまずは頭に叩き込もうと思います!

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

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
}
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

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
を作成します

DockerFile
FROM golang:latest

RUN mkdir /app
WORKDIR /app
docker-compose.yml
version: '3'
services:
  go:
    build:
      context: .
      dockerfile: DockerFile
    command: /bin/sh -c "go run main.go"
    stdin_open: true
    tty: true
    volumes:
      - .:/app
main.go
package 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を作成
を行ってください。

DockerFile
FROM golang:latest

RUN mkdir /app
WORKDIR /app

RUN go get github.com/gin-gonic/gin
docker-compose.yml
version: '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
main.go
package 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.yml
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: 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接続の処理を追加
を行ってください。

DockerFile
FROM 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/gorm
docker-compose.yml
version: '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: local
main.go
package 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.go
package 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で何か作ってみたいと考えている方がいらっしゃったらぜひ参考にしてみてください!

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

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.go
package 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.go
package 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)
    }
}

以上です!

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

html/templateメモ

html/templateメモ

目次

html/templateの概要

htmlのテンプレートにこちらのプログラムで処理したデータを埋め込み返す。webサービスには必須の技術だが、それをgolangを実現するにはテンプレートエンジンであるhtml/templateを使う

正確には汎用テンプレートエンジンであるtext/template標準ライブラリと、さらにHTML専用のテンプレートエンジンであるhtml/templateライブラリの二種類があるが、ここでは後者を扱う

templateとは

  • template(テンプレート) とは予め用意されたhtmlのひな型

  • テンプレートエンジン とはテンプレートとデータ 1を組み込んで、クライアントに返す最終的なhtmlを生成するプログラム

  • 通常、ハンドラがテンプレートエンジンを呼び出してデータをテンプレートに組み込み、できあがったHTMLをクライアントに返す

イメージ図12

image.png

基本文法

  • 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.go
package 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.go
package 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にアクセスすると以下のページが表示される

  • image (23).png

動的なテンプレートの追加方法

  • 先ほどの例では部品テンプレート(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>
  • http://127.0.0.1:8080/processにアクセスすると以下のページが表示される

  • image.png

スコープの話

  • アクション「.」は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.go
package 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にアクセスすると以下のページが表示される

image.png

余談

  • 頭文字が大文字であれば外部から参照できる、というのがGo言語の特徴

  • 先ほどの例ではアクション「.」を通じて構造体を渡しているが、その過程でこちらのパッケージを外部に参照させる処理が含まれているのか
    main.go

type Name struct {
    givenname  string
    familyname string
}
  • のようにName構造体のメンバ変数の頭文字を小文字に変更すると、テンプレートに値が渡らない

参考

Go の html/template でヘッダーやフッター等の共通化を実現する方法

Goプログラミング実践入門 ―標準ライブラリでゼロからWebアプリを作る―


  1. 大抵はクライアントから渡される情報を指す。ユーザ名など。テンプレートは既に用意されたものであるため静的で、データはクライアントによって変わるものであるため動的(ただしこれから見ていく例では単純化するためサーバサイドで定義した文字列等を使っている) 

  2. ハンドラがテンプレートエンジンを呼び出して、使用するテンプレートを通常はテンプレートのリストとして、動的なデータと一緒に渡します。テンプレートエンジンはHTMLを生成して、ResponseWriterにそれを書き込み、さらにResponseWriterはクライアントに送り返すHTTPレスポンスにそれを追加します。(イメージ図共に『Goプログラミング実践入門』より引用) 

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む