20200206のGoに関する記事は6件です。

Golang Echoでのテスト駆動のAPI開発

https://qiita.com/kiyc/items/fc65d999f86d4d59354c

の続きです。
Echoで簡単なAPI、CRUDのCRを作ってみます(DB保存までしませんが)。

https://echo.labstack.com/guide/routing
https://echo.labstack.com/guide/request
https://echo.labstack.com/guide/response
https://echo.labstack.com/guide/testing

Golangのテスト

まずはその前にHTTPのテストが出来るようにします。

server.go
package main

import (
    "net/http"
    "github.com/labstack/echo"
)

func main() {
    router := NewRouter()

    router.Logger.Fatal(router.Start(":8080"))
}

func NewRouter() *echo.Echo {
    e := echo.New()

    e.GET("/hello", helloHandler)

    return e
}

func helloHandler(c echo.Context) error {
    return c.String(http.StatusOK, "Hello")
}
server_test.go
package main

import (
    "net/http"
    "net/http/httptest"
    "testing"
    "github.com/stretchr/testify/assert"
)

func TestHelloHandler(t *testing.T) {
    router := NewRouter()

    req := httptest.NewRequest("GET", "/hello", nil)
    rec := httptest.NewRecorder()

    router.ServeHTTP(rec, req)

    assert.Equal(t, http.StatusOK, rec.Code)
    assert.Equal(t, "Hello, Echo World!!", rec.Body.String())
}
$ docker-compose exec app go test -v
# runtime/cgo
exec: "gcc": executable file not found in $PATH
FAIL    _/go/src [build failed]

なんかエラー。
環境変数の変更が必要らしい。

FROM golang:1.13.6-alpine

ENV CGO_ENABLED=0

WORKDIR /go/src

COPY ./src /go/src

RUN apk update && apk add git
$ docker-compose build
$ docker-compose up -d
$ docker-compose exec app go test -v
=== RUN   TestHelloHandler
--- FAIL: TestHelloHandler (0.00s)
    server_test.go:20: 
            Error Trace:    server_test.go:20
            Error:          Not equal: 
                            expected: "Hello, Echo World!!"
                            actual  : "Hello"

                            Diff:
                            --- Expected
                            +++ Actual
                            @@ -1 +1 @@
                            -Hello, Echo World!!
                            +Hello
            Test:           TestHelloHandler
FAIL
exit status 1
FAIL    _/go/src    0.004s

もちろんアサーションはエラーになりました。
なのでserver.goを修正します。

server.go
package main

import (
    "net/http"
    "github.com/labstack/echo"
)

func main() {
    router := NewRouter()

    router.Logger.Fatal(router.Start(":8080"))
}

func NewRouter() *echo.Echo {
    e := echo.New()

    e.GET("/hello", helloHandler)

    return e
}

func helloHandler(c echo.Context) error {
    return c.String(http.StatusOK, "Hello, Echo World!!")
}
$ docker-compose exec app go test -v
=== RUN   TestHelloHandler
--- PASS: TestHelloHandler (0.00s)
PASS
ok      _/go/src    0.004s

成功しました:thumbsup:

API実装

本題です。
APIのテストを書きます。

server_test.go
package main

import (
    "net/url"
    "net/http"
    "net/http/httptest"
    "strings"
    "testing"
    "github.com/labstack/echo"
    "github.com/stretchr/testify/assert"
)

func TestUserIndexHandler(t *testing.T) {
    router := NewRouter()

    req := httptest.NewRequest(http.MethodGet, "/users", nil)
    rec := httptest.NewRecorder()

    router.ServeHTTP(rec, req)

    assert.Equal(t, http.StatusOK, rec.Code)
    assert.JSONEq(t, `[{"name": "Taro", "email": "taro@example.com"}, {"name": "Jiro", "email": "jiro@example.com"}]`, rec.Body.String())
}

func TestUserShowHandler(t *testing.T) {
    router := NewRouter()

    req := httptest.NewRequest(http.MethodGet, "/users/jiro", nil)
    rec := httptest.NewRecorder()

    router.ServeHTTP(rec, req)

    assert.Equal(t, http.StatusOK, rec.Code)
    assert.JSONEq(t, `{"name": "Jiro", "email": "jiro@example.com"}`, rec.Body.String())
}

func TestUserCreateHandler(t *testing.T) {
    router := NewRouter()

    form := make(url.Values)
    form.Set("name", "Saburo")
    form.Set("email", "saburo@example.com")
    req := httptest.NewRequest(http.MethodPost, "/users", strings.NewReader(form.Encode()))
    req.Header.Set(echo.HeaderContentType, echo.MIMEApplicationForm)
    rec := httptest.NewRecorder()

    router.ServeHTTP(rec, req)

    assert.Equal(t, http.StatusCreated, rec.Code)
    assert.JSONEq(t, `{"name": "Saburo", "email": "saburo@example.com"}`, rec.Body.String())
}
$ docker-compose exec app go test -v
=== RUN   TestUserIndexHandler
--- FAIL: TestUserIndexHandler (0.00s)
    server_test.go:21: 
            Error Trace:    server_test.go:21
            Error:          Not equal: 
                            expected: 200
                            actual  : 404
            Test:           TestUserIndexHandler
    server_test.go:22: 
            Error Trace:    server_test.go:22
            Error:          Not equal: 
                            expected: []interface {}([]interface {}{map[string]interface {}{"email":"taro@example.com", "name":"Taro"}, map[string]interface {}{"email":"jiro@example.com", "name":"Jiro"}})
                            actual  : map[string]interface {}(map[string]interface {}{"message":"Not Found"})
            Test:           TestUserIndexHandler
=== RUN   TestUserShowHandler
--- FAIL: TestUserShowHandler (0.00s)
    server_test.go:33: 
            Error Trace:    server_test.go:33
            Error:          Not equal: 
                            expected: 200
                            actual  : 404
            Test:           TestUserShowHandler
    server_test.go:34: 
            Error Trace:    server_test.go:34
            Error:          Not equal: 
                            expected: map[string]interface {}{"email":"jiro@example.com", "name":"Jiro"}
                            actual  : map[string]interface {}{"message":"Not Found"}

                            Diff:
                            --- Expected
                            +++ Actual
                            @@ -1,4 +1,3 @@
                            -(map[string]interface {}) (len=2) {
                            - (string) (len=5) "email": (string) (len=16) "jiro@example.com",
                            - (string) (len=4) "name": (string) (len=4) "Jiro"
                            +(map[string]interface {}) (len=1) {
                            + (string) (len=7) "message": (string) (len=9) "Not Found"
                             }
            Test:           TestUserShowHandler
=== RUN   TestUserCreateHandler
--- FAIL: TestUserCreateHandler (0.00s)
    server_test.go:49: 
            Error Trace:    server_test.go:49
            Error:          Not equal: 
                            expected: 201
                            actual  : 404
            Test:           TestUserCreateHandler
    server_test.go:50: 
            Error Trace:    server_test.go:50
            Error:          Not equal: 
                            expected: map[string]interface {}{"email":"saburo@example.com", "name":"Saburo"}
                            actual  : map[string]interface {}{"message":"Not Found"}

                            Diff:
                            --- Expected
                            +++ Actual
                            @@ -1,4 +1,3 @@
                            -(map[string]interface {}) (len=2) {
                            - (string) (len=5) "email": (string) (len=18) "saburo@example.com",
                            - (string) (len=4) "name": (string) (len=6) "Saburo"
                            +(map[string]interface {}) (len=1) {
                            + (string) (len=7) "message": (string) (len=9) "Not Found"
                             }
            Test:           TestUserCreateHandler
FAIL
exit status 1
FAIL    _/go/src    0.006s

当たり前ですが全部404でエラーになります。
次にserver.goにAPIを追加します。

server.go
package main

import (
    "net/http"
    "github.com/labstack/echo"
)

func main() {
    router := NewRouter()

    router.Logger.Fatal(router.Start(":8080"))
}

func NewRouter() *echo.Echo {
    e := echo.New()

    e.GET("/users", userIndexHandler)
    e.GET("/users/:name", userShowHandler)
    e.POST("/users", userCreateHandler)

    return e
}

type User struct {
    Name  string `json:"name" form:"name"`
    Email string `json:"email" form:"email"`
}

type Users []User

func userIndexHandler(c echo.Context) error {
    var users Users

    users = append(users, User {
        Name:  "Taro",
        Email: "taro@example.com",
    })
    users = append(users, User {
        Name:  "Jiro",
        Email: "jiro@example.com",
    })

    return c.JSON(http.StatusOK, users)
}

func userShowHandler(c echo.Context) error {
    var user User
    name := c.Param("name")

    if name == "taro" {
        user = User {
            Name:  "Taro",
            Email: "taro@example.com",
        }
    } else if name == "jiro" {
        user = User {
            Name:  "Jiro",
            Email: "jiro@example.com",
        }
    }

    return c.JSON(http.StatusOK, user)
}

func userCreateHandler(c echo.Context) (err error) {
    user := new(User)

    if err = c.Bind(user); err != nil {
        return
    }

    return c.JSON(http.StatusCreated, user)
}
$ docker-compose exec app go test -v
=== RUN   TestUserIndexHandler
--- PASS: TestUserIndexHandler (0.00s)
=== RUN   TestUserShowHandler
--- PASS: TestUserShowHandler (0.00s)
=== RUN   TestUserCreateHandler
--- PASS: TestUserCreateHandler (0.00s)
PASS
ok      _/go/src    0.006s

テスト成功しました。テストがシンプルすぎるのは見逃してください。
c.Bind(user)が新鮮に感じます。
普段はPHPを書いていて、そこまで型に厳密ではないので、Golangの型は厳しくも安心感があります。
ただファイルが分かれているけど、server_test.goの中でserver.goNewRouter()を呼べるというのは、server_test.goのテスト対象がserver.goだから?ということでしょうか。
まだまだGolangの道は奥が深そうです。。

参考URL

https://qiita.com/theoden9014/items/ac8763381758148e8ce5
https://qiita.com/JpnLavender/items/21b4574a7513472903ea

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

gorm.DB.FirstでIDしか取って来れなかった話

TL; DR

gorm.Open するときに &parseTime=trueパラメタをつけよう

問題

該当コード

var passedAuth AuthData
err := json.NewDecoder(r.Body).Decode(&passedAuth)
if err != nil {
    fmt.Println(err)
    return
}
var auth AuthData
db.First(&auth, &AuthData{UserID: passedAuth.UserID})
fmt.Printf("%+v\n", auth)

こうなって欲しかった

{Model:{ID:1 CreatedAt:2020-02-06 09:50:59 +0000 UTC UpdatedAt:2020-02-06 09:50:59 +0000 UTC DeletedAt:<nil>} UserID:hogehoge Password:hogehoge}

こうなった

{Model:{ID:1 CreatedAt:0001-01-01 00:00:00 +0000 UTC UpdatedAt:0001-01-01 00:00:00 +0000 UTC DeletedAt:<nil>} UserID: Password:}

IDしか取得できてない!

原因

よくログを見てみると...

sql: Scan error on column index 1, name "created_at": unsupported Scan, storing driver.Value type []uint8 into type *time.Time

教えてくれてた。無視しててごめん。

解決

見た感じ一般的なエラーっぽかったので、まるっとコピペしてググってみたらこの記事にたどり着いた。
Gormでunsupported Scan的なエラーが出た

どうやらgorm.Open するときに &parseTime=trueパラメタを付けないとらしい。

該当箇所を見てみる。

db, err := gorm.Open("mysql", "root:admin@tcp(mysql:3306)/data_base?charset=utf8mb4")

ついてない。これだ。修正。。

db, err := gorm.Open("mysql", "root:admin@tcp(mysql:3306)/data_base?charset=utf8mb4&parseTime=true")

直った。すごい。

参考文献

Gormでunsupported Scan的なエラーが出た

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

golangでawsのs3のデータをダウンロードするコード

下記を参考にした
https://docs.aws.amazon.com/sdk-for-go/api/service/s3/
https://github.com/awsdocs/aws-doc-sdk-examples/tree/master/go/example_code/s3

package main

import (
    "fmt"
    "os"

    "github.com/aws/aws-sdk-go/aws"
    "github.com/aws/aws-sdk-go/aws/credentials"
    "github.com/aws/aws-sdk-go/aws/session"
    "github.com/aws/aws-sdk-go/service/s3"
    "github.com/aws/aws-sdk-go/service/s3/s3manager"
)

func main() {
    creds := credentials.NewStaticCredentials("アクセスキー", "シークレットキー", "")

    sess := session.Must(session.NewSession(&aws.Config{
        Credentials: creds,
        Region:      aws.String("ap-northeast-1"),
    }))

    filename := "aaa.txt"

    // Create a downloader with the session and default options
    downloader := s3manager.NewDownloader(sess)

    // Create a file to write the S3 Object contents to.
    f, err := os.Create(filename)
    if err != nil {
        fmt.Println(fmt.Errorf("failed to create file %q, %v", filename, err))
        return
    }

    // Write the contents of S3 Object to the file
    n, err := downloader.Download(f, &s3.GetObjectInput{
        Bucket: aws.String("backet-name"),
        Key:    aws.String("/aaa.txt"),
    })
    if err != nil {
        fmt.Println(fmt.Errorf("failed to download file, %v", err))
        return
    }
    fmt.Printf("file downloaded, %d bytes\n", n)

}

exampleに、credentialsの部分の記述が見当たらなかったので、メモ的にアップしました。

それ以外はサンプル通りです。結構簡単ですね。

アップロードもサンプル見ながらやれば出来ると思います。

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

【Go+Gin+Gorm】初心者だからちょっ早で超簡単webサービス作ってみる

なんとなくGo言語を書いてみたくなった。
Mac環境でやります。
劣化版Twitterアプリみたいなのを作ります。
ユーザー認証とかはないです。
今回は、

一覧・詳細表示、登録、削除、更新
バリデーション

を実装していきます。

これが今回作るwebサービスです。
つぶやき

Go言語とは?

これによると、

並列処理、ガベージコレクションを備え、軽快にコンパイルできる言語です。以下のような特徴を持っています:

- 一台のコンピュータ上であっという間に大型のGoプログラムをコンパイルすることができます。
- Goはソフトウェアの構造にモデルを与えます。分析をより簡単にこなすことができ、ファイルやライブラリのincludeといったCスタイルの書き出しにありがちな部分を大幅に省くことができます。
- Goは静的型付け言語です。型に階層の概念が無いのでユーザはその関係に気をとられることもなく、典型的なオブジェクト指向言語よりももっとライトに感じるくらいです。
- Goは完全なガベージコレクションタイプの言語です。また、基本的な並列処理とネットワークをサポートしています。
- Goはマルチプロセッサ対応のソフトウェアを作成できるようデザインされています。

完全に理解した。

ginとは

go言語のフレームワークです。railsとかと違って密結合なオールインワンではないです。
なので、railsやってた方は拍子抜けするかも。
また、これくらいの規模ならFW使わなくても手軽にwebサービス作れちゃうのがgo言語のいい所だと理解しています。

gormとは

GO言語用のORMフレームワークです。
DB周りの処理を実装する時にこのフレームワークを使うと便利です。
ORMを使わないと、SQL文をコード内に書かないといけなくなるので、ぜひORM使いましょう。
ちなみにrailsだとActive RecordがORMにあたります。

ORMの恩恵がいまいちピンとこない方はこちらの記事をみるといいでしょう。
RailsのORM機能について

GoをローカルPCに入れてみる。

ここ見てやってみてください。
ターミナルでgoって二文字を打って、以下のようになってたらOK。
おめでとう!

$ go

Go is a tool for managing Go source code.

Usage:

    go <command> [arguments]

The commands are:

    bug         start a bug report
    build       compile packages and dependencies
    clean       remove object files and cached files
    doc         show documentation for package or symbol
    env         print Go environment information
    fix         update packages to use new APIs
    fmt         gofmt (reformat) package sources
    generate    generate Go files by processing source
    get         add dependencies to current module and install them
    install     compile and install packages and dependencies
    list        list packages or modules
    mod         module maintenance
    run         compile and run Go program
    test        test packages
    tool        run specified go tool
    version     print Go version
    vet         report likely mistakes in packages

Use "go help <command>" for more information about a command.

Additional help topics:

    buildmode   build modes
    c           calling between Go and C
    cache       build and test caching
    environment environment variables
    filetype    file types
    go.mod      the go.mod file
    gopath      GOPATH environment variable
    gopath-get  legacy GOPATH go get
    goproxy     module proxy protocol
    importpath  import path syntax
    modules     modules, module versions, and more
    module-get  module-aware go get
    module-auth module authentication using go.sum
    module-private module configuration for non-public modules
    packages    package lists and patterns
    testflag    testing flags
    testfunc    testing functions

Use "go help <topic>" for more information about that topic.

MySQLをローカルにいれる。

railsとかと違って、MySQLは自分で用意しないといけない。はず。
以下のコードをターミナルで打ってMySQLに入れたらもうインストール済です。よかったですね。

$ mysql -uroot -p

入れなかった人は、インストールしましょう。
某はこちらの記事を見てインストールしました。
【超簡単】macへMySQLをインストール
ありがたやー。
インストールできたらターミナル画面はそのままで次へ

MySQLセットアップ

次はデータベースを作ったり、ユーザーを作ったりします。

データベース名:test

ユーザー名:test
パスワード:12345678

以下ターミナルに打ち込んでください。

// データベース作成
mysql> create database test;

// ユーザー作成
mysql> create user 'test'@'localhost' IDENTIFIED BY '12345678';

// データベースにアクセスする権限をユーザーに付与
mysql> grant all privileges on test.* to 'test'@'localhost';
mysql> flush privileges;
mysql> exit

テーブル作らなくていいの?と思った方。
後述のmain.go内のdbInit()でautoMigrateを使ってTweetsテーブルを自動で作ってます。
他にテーブルが必要な場合は、autoMigrateに都度書いていくという感じです。

ディレクトリ構成図

cd $GOPATH/srcして、以下のディレクトリやファイルを作ってください。
ファイルの中身は今は空で構いません。

mytweet/
┣ views/
┃  ┣ delete.html
┃  ┣ detail.html
┃  ┣ index.html
┗ main.go

外部ライブラリの導入

go getというコマンドを使います。
外部ライブラリを導入するには、go getコマンドを利用すると便利です。

go get パッケージ名
go getコマンドを発行すると、以下の処理が自動的に行われます。

・指定したパッケージのGitリモートリポジトリを$GOPATH/srcへダウンロード
・依存パッケージのGitリモートリポジトリを$GOPATH/srcへダウンロード
・ソースコードのビルド(go installコマンド相当)

参照:はじめてのGO言語
以下ターミナルで実行

// ginフレームワーク
$ go get github.com/gin-gonic/gin

// mysql用ドライバー
$ go get github.com/go-sql-driver/mysql

// gorm
$ go get github.com/jinzhu/gorm

go getした際にソースはGOPATH/src配下に、インストールされます。
ちなみに、実行ファイル(コンパイルされたやつ)はGOPATH/binにインストールされます。

完成版ソースコード一気見せ

完成したソースコードを載せます。いきなり雑になってごめんなさい。
さっき作成したファイルにコピペしてください。

main.go
package main

import (
    "log"
    "net/http"
    "strconv"

    "github.com/gin-gonic/gin"
    _ "github.com/go-sql-driver/mysql" //直接的な記述が無いが、インポートしたいものに対しては"_"を頭につける決まり
    "github.com/jinzhu/gorm"
)

// Tweetモデル宣言
// モデルはDBのテーブル構造をGOの構造体で表したもの
type Tweet struct {
    gorm.Model
    Content string `form:"content" binding:"required"`
}

func gormConnect() *gorm.DB {
    DBMS := "mysql"
    USER := "test"
    PASS := "12345678"
    DBNAME := "test"
    // MySQLだと文字コードの問題で"?parseTime=true"を末尾につける必要がある
    CONNECT := USER + ":" + PASS + "@/" + DBNAME + "?parseTime=true"
    db, err := gorm.Open(DBMS, CONNECT)

    if err != nil {
        panic(err.Error())
    }
    return db
}

// DBの初期化
func dbInit() {
    db := gormConnect()

    // コネクション解放解放
    defer db.Close()
    db.AutoMigrate(&Tweet{}) //構造体に基づいてテーブルを作成
}

// データインサート処理
func dbInsert(content string) {
    db := gormConnect()

    defer db.Close()
    // Insert処理
    db.Create(&Tweet{Content: content})
}

//DB更新
func dbUpdate(id int, tweetText string) {
    db := gormConnect()
    var tweet Tweet
    db.First(&tweet, id)
    tweet.Content = tweetText
    db.Save(&tweet)
    db.Close()
}

// 全件取得
func dbGetAll() []Tweet {
    db := gormConnect()

    defer db.Close()
    var tweets []Tweet
    // FindでDB名を指定して取得した後、orderで登録順に並び替え
    db.Order("created_at desc").Find(&tweets)
    return tweets
}

//DB一つ取得
func dbGetOne(id int) Tweet {
    db := gormConnect()
    var tweet Tweet
    db.First(&tweet, id)
    db.Close()
    return tweet
}

//DB削除
func dbDelete(id int) {
    db := gormConnect()
    var tweet Tweet
    db.First(&tweet, id)
    db.Delete(&tweet)
    db.Close()
}

func main() {
    router := gin.Default()
    router.LoadHTMLGlob("views/*.html")

    dbInit()

    //一覧
    router.GET("/", func(c *gin.Context) {
        tweets := dbGetAll()
        c.HTML(200, "index.html", gin.H{"tweets": tweets})
    })

    //登録
    router.POST("/new", func(c *gin.Context) {
        var form Tweet
        // ここがバリデーション部分
        if err := c.Bind(&form); err != nil {
            tweets := dbGetAll()
            c.HTML(http.StatusBadRequest, "index.html", gin.H{"tweets": tweets, "err": err})
            c.Abort()
        } else {
            content := c.PostForm("content")
            dbInsert(content)
            c.Redirect(302, "/")
        }
    })

    //投稿詳細
    router.GET("/detail/:id", func(c *gin.Context) {
        n := c.Param("id")
        id, err := strconv.Atoi(n)
        if err != nil {
            panic(err)
        }
        tweet := dbGetOne(id)
        c.HTML(200, "detail.html", gin.H{"tweet": tweet})
    })

    //更新
    router.POST("/update/:id", func(c *gin.Context) {
        n := c.Param("id")
        id, err := strconv.Atoi(n)
        if err != nil {
            panic("ERROR")
        }
        tweet := c.PostForm("tweet")
        dbUpdate(id, tweet)
        c.Redirect(302, "/")
    })

    //削除確認
    router.GET("/delete_check/:id", func(c *gin.Context) {
        n := c.Param("id")
        id, err := strconv.Atoi(n)
        if err != nil {
            panic("ERROR")
        }
        tweet := dbGetOne(id)
        c.HTML(200, "delete.html", gin.H{"tweet": tweet})
    })

    //削除
    router.POST("/delete/:id", func(c *gin.Context) {
        n := c.Param("id")
        id, err := strconv.Atoi(n)
        if err != nil {
            panic("ERROR")
        }
        dbDelete(id)
        c.Redirect(302, "/")

    })

    router.Run()
}

index.html
<!DOCTYPE html>
<html lang="ja">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <meta http-equiv="X-UA-Compatible" content="ie=edge" />
    <title>一覧ページ</title>
  </head>
  <body>
    <header>
      <h1>つぶやき</h1>
    </header>
    <div class="wrap">
      <div class="input">
        <p>{{.err}}</p>
        <form action="/new" method="post">
          <p>いま思っていること :<input type="text" name="content" size="30" placeholder="つぶやくこと"/></p>
          <p><input type="submit" value="つぶやく" /></p>
        </form>
      </div>
      <div class="indexGet">
        <ul>
          {{range.tweets}}
          <li>
            {{.Content}}
            <label><a href="/detail/{{.ID}}">編集</a></label>
            <label><a href="/delete_check/{{.ID}}">削除</a></label>
          </li>
          {{end}}
        </ul>
      </div>
    </div>
  </body>
</html>
detail.html
<body>
    <h2>詳細</h2>

    <form method="post" action="/update/{{.tweet.ID}}">
        <p>内容<input type="text" name="tweet" size="30" value="{{.tweet.Content}}" ></p>
        <p><input type="submit" value="Send"></p>
    </form>
</body>
delete.html
<body>
    <h1>削除確認</h1>
    <p>本当に削除しますか?</p>
    <ul>
        <li>内容: {{.tweet.Content}}</li>
        <li>作成時間: {{.tweet.CreatedAt}}</li>
    </ul>

    <form method="post" action="/delete/{{.tweet.ID}}">
        <p><input type="submit" value="削除"></p>
        <p><a href="/">戻る</a></p>
    </form>
</body>

起動

mytweetディレクトリに入って、以下を実行!

go run main.go

http://localhost:8080
を開いて、できてたらOK!

go runコマンドとは?

Go言語はコンパイル言語ですが、go runコマンドを用いると一時ファイルとしてコンパイルしたプログラムをその場で実行することができます。
実行ファイルの作り方などはここを参照しましょう。

最後に

バリデーション部分がまだイマイチだなーという感じです。
今度はログイン機能などを追加してみたいと思います。

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

【Go+Gin+Gorm】初心者だから超簡単webサービス作ってみる

なんとなくGo言語を書いてみたくなった。
Mac環境でやります。
劣化版Twitterアプリみたいなのを作ります。
ユーザー認証とかはないです。
今回は、

・ 一覧・詳細表示、登録、削除、更新
・ 空の投稿ができないようにするバリデーション

を実装していきます。

これが今回作るwebサービスです。
つぶやき

Go言語とは?

これによると、

並列処理、ガベージコレクションを備え、軽快にコンパイルできる言語です。以下のような特徴を持っています:

- 一台のコンピュータ上であっという間に大型のGoプログラムをコンパイルすることができます。
- Goはソフトウェアの構造にモデルを与えます。分析をより簡単にこなすことができ、ファイルやライブラリのincludeといったCスタイルの書き出しにありがちな部分を大幅に省くことができます。
- Goは静的型付け言語です。型に階層の概念が無いのでユーザはその関係に気をとられることもなく、典型的なオブジェクト指向言語よりももっとライトに感じるくらいです。
- Goは完全なガベージコレクションタイプの言語です。また、基本的な並列処理とネットワークをサポートしています。
- Goはマルチプロセッサ対応のソフトウェアを作成できるようデザインされています。

完全に理解した。

ginとは

go言語のフレームワークです。railsとかと違って密結合なオールインワンではないです。
なので、railsやってた方は拍子抜けするかも。
また、これくらいの規模ならFW使わなくても手軽にwebサービス作れちゃうのがgo言語のいい所だと理解しています。

gormとは

GO言語用のORMフレームワークです。
DB周りの処理を実装する時にこのフレームワークを使うと便利です。
ORMを使わないと、SQL文をコード内に書かないといけなくなるので、ぜひORM使いましょう。
ちなみにrailsだとActive RecordがORMにあたります。

ORMの恩恵がいまいちピンとこない方はこちらの記事をみるといいでしょう。
RailsのORM機能について

GoをローカルPCに入れてみる。

ここ見てやってみてください。
ターミナルでgoって二文字を打って、以下のようになってたらOK。
おめでとう!

$ go

Go is a tool for managing Go source code.

Usage:

    go <command> [arguments]

The commands are:

    bug         start a bug report
    build       compile packages and dependencies
    clean       remove object files and cached files
    doc         show documentation for package or symbol
    env         print Go environment information
    fix         update packages to use new APIs
    fmt         gofmt (reformat) package sources
    generate    generate Go files by processing source
    get         add dependencies to current module and install them
    install     compile and install packages and dependencies
    list        list packages or modules
    mod         module maintenance
    run         compile and run Go program
    test        test packages
    tool        run specified go tool
    version     print Go version
    vet         report likely mistakes in packages

Use "go help <command>" for more information about a command.

Additional help topics:

    buildmode   build modes
    c           calling between Go and C
    cache       build and test caching
    environment environment variables
    filetype    file types
    go.mod      the go.mod file
    gopath      GOPATH environment variable
    gopath-get  legacy GOPATH go get
    goproxy     module proxy protocol
    importpath  import path syntax
    modules     modules, module versions, and more
    module-get  module-aware go get
    module-auth module authentication using go.sum
    module-private module configuration for non-public modules
    packages    package lists and patterns
    testflag    testing flags
    testfunc    testing functions

Use "go help <topic>" for more information about that topic.

MySQLをローカルにいれる。

railsとかと違って、MySQLは自分で用意しないといけない。はず。
以下のコードをターミナルで打ってMySQLに入れたらもうインストール済です。よかったですね。

$ mysql -uroot -p

入れなかった人は、インストールしましょう。
某はこちらの記事を見てインストールしました。
【超簡単】macへMySQLをインストール
ありがたやー。
インストールできたらターミナル画面はそのままで次へ

MySQLセットアップ

次はデータベースを作ったり、ユーザーを作ったりします。

データベース名:test

ユーザー名:test
パスワード:12345678

以下ターミナルに打ち込んでください。

// データベース作成
mysql> create database test;

// ユーザー作成
mysql> create user 'test'@'localhost' IDENTIFIED BY '12345678';

// データベースにアクセスする権限をユーザーに付与
mysql> grant all privileges on test.* to 'test'@'localhost';
mysql> flush privileges;
mysql> exit

テーブル作らなくていいの?と思った方。
後述のmain.go内のdbInit()でautoMigrateを使ってTweetsテーブルを自動で作ってます。
他にテーブルが必要な場合は、autoMigrateに都度書いていくという感じです。

ディレクトリ構成図

cd $GOPATH/srcして、以下のディレクトリやファイルを作ってください。
ファイルの中身は今は空で構いません。

mytweet/
┣ views/
┃  ┣ delete.html
┃  ┣ detail.html
┃  ┣ index.html
┗ main.go

外部ライブラリの導入

go getというコマンドを使います。
外部ライブラリを導入するには、go getコマンドを利用すると便利です。

go get パッケージ名
go getコマンドを発行すると、以下の処理が自動的に行われます。

・指定したパッケージのGitリモートリポジトリを$GOPATH/srcへダウンロード
・依存パッケージのGitリモートリポジトリを$GOPATH/srcへダウンロード
・ソースコードのビルド(go installコマンド相当)

参照:はじめてのGO言語
以下ターミナルで実行

// ginフレームワーク
$ go get github.com/gin-gonic/gin

// mysql用ドライバー
$ go get github.com/go-sql-driver/mysql

// gorm
$ go get github.com/jinzhu/gorm

go getした際にソースはGOPATH/src配下に、インストールされます。
ちなみに、実行ファイル(コンパイルされたやつ)はGOPATH/binにインストールされます。

完成版ソースコード一気見せ

完成したソースコードを載せます。いきなり雑になってごめんなさい。
さっき作成したファイルにコピペしてください。

main.go
package main

import (
    "log"
    "net/http"
    "strconv"

    "github.com/gin-gonic/gin"
    _ "github.com/go-sql-driver/mysql" //直接的な記述が無いが、インポートしたいものに対しては"_"を頭につける決まり
    "github.com/jinzhu/gorm"
)

// Tweetモデル宣言
// モデルはDBのテーブル構造をGOの構造体で表したもの
type Tweet struct {
    gorm.Model
    Content string `form:"content" binding:"required"`
}

func gormConnect() *gorm.DB {
    DBMS := "mysql"
    USER := "test"
    PASS := "12345678"
    DBNAME := "test"
    // MySQLだと文字コードの問題で"?parseTime=true"を末尾につける必要がある
    CONNECT := USER + ":" + PASS + "@/" + DBNAME + "?parseTime=true"
    db, err := gorm.Open(DBMS, CONNECT)

    if err != nil {
        panic(err.Error())
    }
    return db
}

// DBの初期化
func dbInit() {
    db := gormConnect()

    // コネクション解放解放
    defer db.Close()
    db.AutoMigrate(&Tweet{}) //構造体に基づいてテーブルを作成
}

// データインサート処理
func dbInsert(content string) {
    db := gormConnect()

    defer db.Close()
    // Insert処理
    db.Create(&Tweet{Content: content})
}

//DB更新
func dbUpdate(id int, tweetText string) {
    db := gormConnect()
    var tweet Tweet
    db.First(&tweet, id)
    tweet.Content = tweetText
    db.Save(&tweet)
    db.Close()
}

// 全件取得
func dbGetAll() []Tweet {
    db := gormConnect()

    defer db.Close()
    var tweets []Tweet
    // FindでDB名を指定して取得した後、orderで登録順に並び替え
    db.Order("created_at desc").Find(&tweets)
    return tweets
}

//DB一つ取得
func dbGetOne(id int) Tweet {
    db := gormConnect()
    var tweet Tweet
    db.First(&tweet, id)
    db.Close()
    return tweet
}

//DB削除
func dbDelete(id int) {
    db := gormConnect()
    var tweet Tweet
    db.First(&tweet, id)
    db.Delete(&tweet)
    db.Close()
}

func main() {
    router := gin.Default()
    router.LoadHTMLGlob("views/*.html")

    dbInit()

    //一覧
    router.GET("/", func(c *gin.Context) {
        tweets := dbGetAll()
        c.HTML(200, "index.html", gin.H{"tweets": tweets})
    })

    //登録
    router.POST("/new", func(c *gin.Context) {
        var form Tweet
        // ここがバリデーション部分
        if err := c.Bind(&form); err != nil {
            tweets := dbGetAll()
            c.HTML(http.StatusBadRequest, "index.html", gin.H{"tweets": tweets, "err": err})
            c.Abort()
        } else {
            content := c.PostForm("content")
            dbInsert(content)
            c.Redirect(302, "/")
        }
    })

    //投稿詳細
    router.GET("/detail/:id", func(c *gin.Context) {
        n := c.Param("id")
        id, err := strconv.Atoi(n)
        if err != nil {
            panic(err)
        }
        tweet := dbGetOne(id)
        c.HTML(200, "detail.html", gin.H{"tweet": tweet})
    })

    //更新
    router.POST("/update/:id", func(c *gin.Context) {
        n := c.Param("id")
        id, err := strconv.Atoi(n)
        if err != nil {
            panic("ERROR")
        }
        tweet := c.PostForm("tweet")
        dbUpdate(id, tweet)
        c.Redirect(302, "/")
    })

    //削除確認
    router.GET("/delete_check/:id", func(c *gin.Context) {
        n := c.Param("id")
        id, err := strconv.Atoi(n)
        if err != nil {
            panic("ERROR")
        }
        tweet := dbGetOne(id)
        c.HTML(200, "delete.html", gin.H{"tweet": tweet})
    })

    //削除
    router.POST("/delete/:id", func(c *gin.Context) {
        n := c.Param("id")
        id, err := strconv.Atoi(n)
        if err != nil {
            panic("ERROR")
        }
        dbDelete(id)
        c.Redirect(302, "/")

    })

    router.Run()
}

index.html
<!DOCTYPE html>
<html lang="ja">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <meta http-equiv="X-UA-Compatible" content="ie=edge" />
    <title>一覧ページ</title>
  </head>
  <body>
    <header>
      <h1>つぶやき</h1>
    </header>
    <div class="wrap">
      <div class="input">
        <p>{{.err}}</p>
        <form action="/new" method="post">
          <p>いま思っていること :<input type="text" name="content" size="30" placeholder="つぶやくこと"/></p>
          <p><input type="submit" value="つぶやく" /></p>
        </form>
      </div>
      <div class="indexGet">
        <ul>
          {{range.tweets}}
          <li>
            {{.Content}}
            <label><a href="/detail/{{.ID}}">編集</a></label>
            <label><a href="/delete_check/{{.ID}}">削除</a></label>
          </li>
          {{end}}
        </ul>
      </div>
    </div>
  </body>
</html>
detail.html
<body>
    <h2>詳細</h2>

    <form method="post" action="/update/{{.tweet.ID}}">
        <p>内容<input type="text" name="tweet" size="30" value="{{.tweet.Content}}" ></p>
        <p><input type="submit" value="Send"></p>
    </form>
</body>
delete.html
<body>
    <h1>削除確認</h1>
    <p>本当に削除しますか?</p>
    <ul>
        <li>内容: {{.tweet.Content}}</li>
        <li>作成時間: {{.tweet.CreatedAt}}</li>
    </ul>

    <form method="post" action="/delete/{{.tweet.ID}}">
        <p><input type="submit" value="削除"></p>
        <p><a href="/">戻る</a></p>
    </form>
</body>

起動

mytweetディレクトリに入って、以下を実行!

go run main.go

http://localhost:8080
を開いて、できてたらOK!

go runコマンドとは?

Go言語はコンパイル言語ですが、go runコマンドを用いると一時ファイルとしてコンパイルしたプログラムをその場で実行することができます。
実行ファイルの作り方などはここを参照しましょう。

最後に

バリデーションメッセージがrailsのように親切じゃない。
自分でメッセージも用意しないといけないのかな。

今度はログイン機能などを追加してみたいと思います。

次回へ続く。。。かも。

参考

https://blog.kannart.co.jp/programming/2235/

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

golang dbr

どうしてもdbrを使わないといけない場面に出くわしたので、忘れそうなものを記録する
SELECTの情報が少なすぎる。
どうやって、SQLビルダーから、文字列を追い出すか、それが問題だ・・・

①godocから探る
https://godoc.org/github.com/gocraft/dbr

①テストから、探る
https://github.com/gocraft/dbr/blob/master/select_test.go

※例なのでテーブル名、カラム名に変数は使わず記述している

Joinしたテーブルに別名をつける

LeftJoin(dbr.I("users").As("staffs"), "staff.id=companies.user_id").

サブクエリにエイリアスをつける。サブクエリをJoinする

    subQuery := Select("company_id").
        From("users").
        Where("id = ?", userId).
        As("staffs")

    _, err := Select("*")
        From("companies").
        LeftJoin(subQuery, "staffs.company_id=companies.id").
        Load(&results)

構造体にではなく、単一のカラムをプリミティブな型のスライスに取得

一時変数を、いちいち構造体定義するのは面倒。また構造体をスライスに変換する手間もコードが汚れる
取得したらIN句などで利用する

    var myCompanyUserIds []int64
    _, err := Select("users.id").
        From("users").
        Where("users.company_id = ?", myCompanyID).
        Load(&myCompanyUserIds)

プレースホルダを使わず Whereを記述する

Where(dbr.Eq("company.id", myCompanyID))
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む