Go言語でCRUDを作る

今年はGo言語も勉強したいということでやっていく。学習コスト自体もそんなに高くないと聞いていたのでチュートリアルすっとばして開発に入っていく。
わからんかったら適当に調べるスタンスで。

環境設定

まずは環境設定

$ sudo pacman -S go

Go言語を動かすワークスペースを作る、無難にHomeディレクトリに作る
公式には以下のように書かれているので、以下に習っての3つのディレクトリを作る

Go code must be kept inside a workspace. A workspace is a directory hierarchy with three directories at its root:

- src contains Go source files organized into packages (one package per directory),
- pkg contains package objects, and
- bin contains executable commands.
$ mkdir go
$ cd go
$ mkdir bin src pkg
$ vim ~/.zshrc

GOPATHを通す

# go
export GOPATH=~/go
export GOBIN=$GOPATH/bin

再度、zshの設定を読み込む

$ source ~/.zshrc

Hello World

まずGoでのHello Worldを読んでどんな感じになるのかを掴む。

$ mkdir src/helloworld
$ touch src/helloworld/main.go
package main

import "fmt"

func main() {
    fmt.Println("hello world")
}

コンパイル

$ go run main.go

importで必要なパッケージを読み込み関数ベースで書いていくのがわかる。
他にもさらっとGithubに上がっているコードなんかを読んで、どんなふうになるのかイメージを掴む。
書き方的に見て、Express.jsと同じ要領だとわかる。こちらも同様にディレクトリ構成を決める必要があるのと
次に、main関数があってそこで何かをしていく。Cライクな書き方になっているのが理解できる。

ということで早速サンプルコードを探して書きながら勉強していく。

CRUD

こちらの記事が非常にわかりやすいコードを書いているのでこちらを参考にすすめる。
要件としてはMySQLに接続してデータを保存できるCRUDをスクラッチで書いていくというもの。
大抵の開発ではサーバーサイドはAPIサーバーとしての役割を担うけれど、今回はじめてなのでViewとなる部分もGo言語のテンプレートを使用して開発を進める。

srcにcrudディレクトリを作成、ここがプロジェクトの場所となる

$ mkdir src/crud
$ cd src/crud
$ touch main.go

まずはデータベース側の準備
golangというデータベースを用意、次に以下のSQLを実行してテーブルの作成を行う。

DROP TABLE IF EXISTS `employee`;
CREATE TABLE `employee` (
  `id` int(6) unsigned NOT NULL AUTO_INCREMENT,
  `name` varchar(30) NOT NULL,
  `city` varchar(30) NOT NULL,
  PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=latin1;
  • importで必要なパッケージ読み込む
  • DB間とのやり取りをしやすいようemployeeのタイプ定義
  • データベースへの接続関数
  • サーバーの立ち上げ
package main

import (
  "database/sql"
  "log"
  "net/http"
  "text/template"

  _ "github.com/go-sql-driver/mysql"
)

type Employee struct {
  Id   int
  Name string
  City string
}

func dbConn() (db *sql.DB) {
  dbDriver := "mysql"
  dbUser := "root"
  dbPass := "root"
  dbName := "golang"
  db, err := sql.Open(dbDriver, dbUser+":"+dbPass+"@/"+dbName)
  if err != nil {
    panic(err.Error())
  }
  return db
}

func main() {
  log.Println("Server started on: http://localhost:8080")
  http.ListenAndServe(":8080", nil)
}

ここまでサーバー立ち上げまでの処理
次にデータベースからデータハンドリングする処理を作る

var tmpl = template.Must(template.ParseGlob("form/*"))

func Index(w http.ResponseWriter, r *http.Request){
  db := dbConn()
  selDB, err := db.Query("SELECT * FROM employee ORDER BY id DESC")
  if err != nil {
    panic(err.Error())
  }
  emp := Employee{}
  res := []Employee{}
  for selDB.Next(){
    var id int
    var name, city string
    err = selDB.Scan(&id, &name, &city)
    if err != nil {
      panic(err.Error())
    }
    emp.Id = id
    emp.Name = name
    emp.City = city
    res = append(res, emp)
  }
  tmpl.ExecuteTemplate(w, "Index", res)
  defer db.Close()
}

func Show(w http.ResponseWriter, r *http.Request){
  db := dbConn()
  nId := r.URL.Query().Get("id")
  selDB, err := db.Query("SELECT * FROM employee WHERE id=?",nId)
  if err != nil {
    panic(err.Error())
  }
  emp := Employee{}
  for selDB.Next() {
    var id int
    var name, city string
    err = selDB.Scan(&id, &name, &city)
    if err != nil {
        panic(err.Error())
    }
    emp.Id = id
    emp.Name = name
    emp.City = city
  }
  tmpl.ExecuteTemplate(w, "Show", emp)
  defer db.Close()
}

func New(w http.ResponseWriter, r *http.Request){
  tmpl.ExecuteTemplate(w, "New", nil)
}

func Edit(w http.ResponseWriter, r *http.Request){
  db := dbConn()
  nId := r.URL.Query().Get("id")
  selDB, err := db.Query("SELECT * employee WHERE id=?",nId)
  if err != nil {
      panic(err.Error())
  }
  emp := Employee{}
  for selDB.Next(){
    var id int
    var name, city string
    err = selDB.Scan(&id, &name, &city)
    if err != nil {
        panic(err.Error())
    }
    emp.Id = id
    emp.Name = name
    emp.City = city
  }
  tmpl.ExecuteTemplate(w, "Edit", emp)
  defer db.Close()
}

func Insert(w http.ResponseWriter, r *http.Request) {
  db := dbConn()
  if r.Method == "POST" {
    name := r.FormValue("name")
    city := r.FormValue("city")
    insForm, err := db.Prepare("INSERT INTO employee(name, city) VALUES(?,?)")
    if err != nil {
        panic(err.Error())
    }
    insForm.Exec(name, city)
    log.Println("INSERT: Name: " + name + " | City: " + city)
  }
  defer db.Close()
  http.Redirect(w, r, "/", 301)
}

func Update(w http.ResponseWriter, r *http.Request) {
  db := dbConn()
  if r.Method == "POST" {
    name := r.FormValue("name")
    city := r.FormValue("city")
    id := r.FormValue("uid")
    insForm, err := db.Prepare("UPDATE employee SET name=?, city=? WHERE id=?")
    if err != nil {
        panic(err.Error())
    }
    insForm.Exec(name, city, id)
    log.Println("UPDATE: Name: " + name + " | City: " + city)
  }
  defer db.Close()
  http.Redirect(w, r, "/", 301)
}

func Delete(w http.ResponseWriter, r *http.Request) {
  db := dbConn()
  emp := r.URL.Query().Get("id")
  delForm, err := db.Prepare("DELETE FROM employee WHERE id=?")
  if err != nil {
    panic(err.Error())
  }
  delForm.Exec(emp)
  log.Println("DELETE")
  defer db.Close()
  http.Redirect(w, r, "/", 301)
}

ここで出てきているテンプレートとは次に作るViewの部分になる
これで関数毎に処理を作成できたのでmain関数に追記する

func main() {
  log.Println("Server started on: http://localhost:8080")
  http.HandleFunc("/", Index)
  http.HandleFunc("/show", Show)
  http.HandleFunc("/new", New)
  http.HandleFunc("/edit", Edit)
  http.HandleFunc("/insert", Insert)
  http.HandleFunc("/update", Update)
  http.HandleFunc("/delete", Delete)
  http.ListenAndServe(":8080", nil)
}

これでmain.goは完成
以下がすべてのコード

package main

import (
  "database/sql"
  "log"
  "net/http"
  "text/template"

  _ "github.com/go-sql-driver/mysql"
)

type Employee struct {
  Id   int
  Name string
  City string
}

func dbConn() (db *sql.DB) {
  dbDriver := "mysql"
  dbUser := "root"
  dbPass := "root"
  dbName := "golang"
  db, err := sql.Open(dbDriver, dbUser+":"+dbPass+"@/"+dbName)
  if err != nil {
    panic(err.Error())
  }
  return db
}

var tmpl = template.Must(template.ParseGlob("form/*"))

func Index(w http.ResponseWriter, r *http.Request){
  db := dbConn()
  selDB, err := db.Query("SELECT * FROM employee ORDER BY id DESC")
  if err != nil {
    panic(err.Error())
  }
  emp := Employee{}
  res := []Employee{}
  for selDB.Next(){
    var id int
    var name, city string
    err = selDB.Scan(&id, &name, &city)
    if err != nil {
      panic(err.Error())
    }
    emp.Id = id
    emp.Name = name
    emp.City = city
    res = append(res, emp)
  }
  tmpl.ExecuteTemplate(w, "Index", res)
  defer db.Close()
}

func Show(w http.ResponseWriter, r *http.Request){
  db := dbConn()
  nId := r.URL.Query().Get("id")
  selDB, err := db.Query("SELECT * FROM employee WHERE id=?",nId)
  if err != nil {
    panic(err.Error())
  }
  emp := Employee{}
  for selDB.Next() {
    var id int
    var name, city string
    err = selDB.Scan(&id, &name, &city)
    if err != nil {
        panic(err.Error())
    }
    emp.Id = id
    emp.Name = name
    emp.City = city
  }
  tmpl.ExecuteTemplate(w, "Show", emp)
  defer db.Close()
}

func New(w http.ResponseWriter, r *http.Request){
  tmpl.ExecuteTemplate(w, "New", nil)
}

func Edit(w http.ResponseWriter, r *http.Request){
  db := dbConn()
  nId := r.URL.Query().Get("id")
  selDB, err := db.Query("SELECT * employee WHERE id=?",nId)
  if err != nil {
      panic(err.Error())
  }
  emp := Employee{}
  for selDB.Next(){
    var id int
    var name, city string
    err = selDB.Scan(&id, &name, &city)
    if err != nil {
        panic(err.Error())
    }
    emp.Id = id
    emp.Name = name
    emp.City = city
  }
  tmpl.ExecuteTemplate(w, "Edit", emp)
  defer db.Close()
}

func Insert(w http.ResponseWriter, r *http.Request) {
  db := dbConn()
  if r.Method == "POST" {
    name := r.FormValue("name")
    city := r.FormValue("city")
    insForm, err := db.Prepare("INSERT INTO employee(name, city) VALUES(?,?)")
    if err != nil {
        panic(err.Error())
    }
    insForm.Exec(name, city)
    log.Println("INSERT: Name: " + name + " | City: " + city)
  }
  defer db.Close()
  http.Redirect(w, r, "/", 301)
}

func Update(w http.ResponseWriter, r *http.Request) {
  db := dbConn()
  if r.Method == "POST" {
    name := r.FormValue("name")
    city := r.FormValue("city")
    id := r.FormValue("uid")
    insForm, err := db.Prepare("UPDATE employee SET name=?, city=? WHERE id=?")
    if err != nil {
        panic(err.Error())
    }
    insForm.Exec(name, city, id)
    log.Println("UPDATE: Name: " + name + " | City: " + city)
  }
  defer db.Close()
  http.Redirect(w, r, "/", 301)
}

func Delete(w http.ResponseWriter, r *http.Request) {
  db := dbConn()
  emp := r.URL.Query().Get("id")
  delForm, err := db.Prepare("DELETE FROM employee WHERE id=?")
  if err != nil {
    panic(err.Error())
  }
  delForm.Exec(emp)
  log.Println("DELETE")
  defer db.Close()
  http.Redirect(w, r, "/", 301)
}

func main() {
  log.Println("Server started on: http://localhost:8080")
  http.HandleFunc("/", Index)
  http.HandleFunc("/show", Show)
  http.HandleFunc("/new", New)
  http.HandleFunc("/edit", Edit)
  http.HandleFunc("/insert", Insert)
  http.HandleFunc("/update", Update)
  http.HandleFunc("/delete", Delete)
  http.ListenAndServe(":8080", nil)
}

パッケージで使用しているコードの書き方、Goになれるまではここらへんで躓きそう。

次に先程書いたよtemplateを使用してGoのViewとなる部分を作る

$ mkdir form
$ cd form
$ touch  Header.tmpl Footer.tmpl Index.tmpl Menu.tmpl Show.tmpl New.tmpl Edit.tmpl

テンプレートの中身を作成
Index.tmpl

{{ define "Index" }}
  {{ template "Header" }}
    {{ template "Menu"  }}
    <h2> Registered </h2>
    <table border="1">
      <thead>
      <tr>
        <td>ID</td>
        <td>Name</td>
        <td>City</td>
        <td>View</td>
        <td>Edit</td>
        <td>Delete</td>
      </tr>
       </thead>
       <tbody>
    {{ range . }}
      <tr>
        <td>{{ .Id }}</td>
        <td> {{ .Name }} </td>
        <td>{{ .City }} </td> 
        <td><a href="/show?id={{ .Id }}">View</a></td>
        <td><a href="/edit?id={{ .Id }}">Edit</a></td>
        <td><a href="/delete?id={{ .Id }}">Delete</a><td>
      </tr>
    {{ end }}
       </tbody>
    </table>
  {{ template "Footer" }}
{{ end }}

Header.tmpl

{{ define "Header" }}
<!DOCTYPE html>
<html lang="en-US">
    <head>
        <title>Golang Mysql Curd Example</title>
        <meta charset="UTF-8" />
    </head>
    <body>
        <h1>Golang Mysql Curd Example</h1>   
{{ end }}

Footer.tmpl

{{ define "Footer" }}
    </body>
</html>
{{ end }}

Menu.tmpl

{{ define "Menu" }}
  <a href="/">HOME</a> | 
  <a href="/new">NEW</a>
{{ end }}

Show.tmpl

{{ define "Show" }}
  {{ template "Header" }}
    {{ template "Menu"  }}
    <h2> Register {{ .Id }} </h2>
      <p>Name: {{ .Name }}</p>
      <p>City:  {{ .City }}</p><br /> <a href="/edit?id={{ .Id }}">Edit</a></p>
  {{ template "Footer" }}
{{ end }}

New.tmpl

{{ define "New" }}
  {{ template "Header" }}
    {{ template "Menu" }} 
   <h2>New Name and City</h2>  
    <form method="POST" action="insert">
      <label> Name </label><input type="text" name="name" /><br />
      <label> City </label><input type="text" name="city" /><br />
      <input type="submit" value="Save user" />
    </form>
  {{ template "Footer" }}
{{ end }}

Edit.tmpl

{{ define "Edit" }}
  {{ template "Header" }}
    {{ template "Menu" }} 
   <h2>Edit Name and City</h2>  
    <form method="POST" action="update">
      <input type="hidden" name="uid" value="{{ .Id }}" />
      <label> Name </label><input type="text" name="name" value="{{ .Name }}"  /><br />
      <label> City </label><input type="text" name="city" value="{{ .City }}"  /><br />
      <input type="submit" value="Save user" />
    </form><br />    
  {{ template "Footer" }}
{{ end }}
$ go run main.go

これで実装完了、本来はバリデーションなども加える必要があるし
モジュール毎にファイルを分けて管理するのがベストな方法
しかしながら今回は参考リンクの通りに実装を進めてみた

まとめ

実装してみた所感としては関数毎に処理を書けるので汎用性が高い
前述したようにExpress.js同様に初期段階で構造をきちんと決めておく必要がある
ルールが緩い言語ほど人を選ぶ傾向にあるのでGoも同様かなという印象。設計がきちんとできていれば素晴らしいものを作れるし、反対にルールが緩い分設計をミスると結構カオスなものが出来上がったりする
そういう部分ではJavaScriptに似ているかなという感じである。
これもまたどんなパッケージが存在しているのか知らなければいけないと思う。そういう意味での引き出しを増やす必要性はあるかな
コンパイルがかなり速いので今後Node.jsの代替えとなっていくのかなぁという感じです
マイクロサービス開発向けで汎用性の高いものが作れるので、ボイラープレートを作っておいてすぐに自分の作りたいものを作れるようにしておいてもいいかも

関連記事