20190227のGoに関する記事は4件です。

Go言語を学習し始めて、簡単なCRUDアプリをクリーンアーキテクチャで作成するまで③

このシリーズ目次

はじめに

Goを学習し始めて、簡単なCRUDアプリをクリーンアーキテクチャで作成するまで③|Wano Group Developers Blog
にあげたものと同一の内容ですが、Qiitaにもアップいたします。

Go言語を学習し始めて、簡単なCRUDアプリをクリーンアーキテクチャで作成するまで② - Qiita
の続きとなります。

しかしこの記事から読まれても全く支障がないので、お好きに読まれてください!
今回は、②で作ったCRUDのWebアプリをクリーンアーキテクチャに書き換えてみる編です。

③クリーンアーキテクチャで②のアプリ作り変え

1.まずクリーンアーキテクチャとやらを学ぶ

まずボブおじさんのブログ読む

The Clean Architectureとは、ボブおじさん(Robert C. Martin)によって提唱されたもののようです。
依存関係をコントロールし持続可能なソフトウェアを実現するための体系的な手法。
メリットは
- フレームワークに依存しない
- テストが可能で書きやすい
- UIから独立
- データベースから独立
- 外部機能独立

なるほど。
Clean Coder Blog

CleanArchitecture.jpg
(上記サイトより引用)

上記が有名なクリーンアーキテクチャの同心円の図なのですが、正直さっぱりわからん、、 となりました。

前提である、DIとSOLIDの法則を学ぶ

【必須科目 DI】DIの仕組みをGoで実装して理解する - Qiita
【ボブおじさんのClean Architectureまとめ】オブジェクト指向 ~SOLIDの原則~ - Qiita
とりあえず前提らしいのでDIとSOLIDの法則の概念をつかむことにしました。
上記の@yoshinori_hisakawaさんの記事がとても感覚を掴むのに役に立ちました。
つまづきポイントは、DIとDIP(SOLIDの法則のD)だと思うのですが、双方の解説が非常にわかりやすい。

  • DI(Dependency Injection)
post_service.go@fukubaka0825

package service

import (
    "sample-clean-arch/domain/model"
    "sample-clean-arch/usecase/presenter"
    "sample-clean-arch/usecase/repository"
)

type postService struct {
    PostRepository repository.PostRepository
    PostPresenter  presenter.PostPresenter
}

type PostService interface {
    Create(post *model.Post) error
    Get(posts []*model.Post) ([]*model.Post, error)
    GetForEditPost(id int) (*model.Post, error)
    Update(post *model.Post) error
    Delete(post *model.Post) error
}

func NewPostService(repo repository.PostRepository, pre presenter.PostPresenter) PostService {
    return &postService{repo, pre}
}
//以降でpostServiceをPostServiceたらしめるが略
main.go

func main(){
    //repoはrepository.PostRepositoryインターフェース実装されてればなんでも良い
    repo := xxx...
    //preはpresenter.PostPresenterインターフェース実装されてればなんでも良い
    pre  := xxx...
    se := service.NewPostService(repo,pre)
}

mainで直接service呼ぶなんてことないけど、説明の便宜上そうしてみています。

上のソース(post_service.go)のように、postService構造体のメンバがrepositoryとpresenterそれぞれのインターフェース型であるおかげで、NewPostServiceコンストラクタでの引数もそれぞれの当然インターフェース型。
なのでmainでpostServiceをNewするときも、引数であるrepoとpreはそれぞれのインターフェース満たしてればなんでもいいことになります。実装じゃなくて抽象(インターフェース)に依存ってやつです。

こうすることで抽象に依存させて、それぞれの層を疎結合(相手の中身を知らない)にして、独立してテストしやすかったり変更できたりとメリットを生み出すことができるようですね。なんでDIしなきゃいけないんだっけってことから思考すると掴みやすいと思います(僕が掴んでるとはいってない)

  • DIP(依存性逆転の原則)

<ルール>

上位のモジュールは下位のモジュールに依存してはならない。

どちらのモジュールも「抽象」に依存すべきである。

「抽象」は実装の詳細に依存してはならない。

実装の詳細が「抽象」に依存すべきである。

実装の詳細が抽象に依存すべきなのは、DIの感覚があれば理解できるのかなと思います。
しかし、外から中(下位から上位)にしか依存しちゃダメだよってしても以下みたいな不都合が起きてきます。
「usecase層のservice、下位のinfrasrtucture層のrepositoryに依存しちゃいたい」(ダメな例)


そういう時に依存性逆転は使えます。

「usecase層にrepository IFを公開して、そこにserviceを依存させる。下位層のrepositoryはそのIFを元に実装する」(良い例)

今までなら上位モジュールが下位モジュールに依存してしまっていたところを逆転させて、安定した関係性に正せる。 こうすることでそれぞれのレイヤーが高い独立性をもてるようになる(僕の理解ですので間違っているかもしれません。)


SOLID-DIP .jpg
(画像は@yoshinori_hisakawaさんの記事からの引用)

再度ボブおじさんの記事読む

結構スラスラ読めるようになっていました(これが人間の慣れか)
大事なところはたくさんあるのですが、以下抜粋。

This layer is where all the details go. The Web is a detail. The database is a detail. We keep these things on the outside where they can do little harm.

This layerはinfrastructure層。
ここ凄い大事だなと思いました。DBとかWebとか詳細にスギナイヨネ。

Only Four Circles?

という疑問に対しては

The inner most circle is the most general.

であればそんなことないよって。4つのレイヤーで思考停止しないようにしよう。

2.作ったCRUDアプリをクリーンアーキテクチャに改造

Clean ArchitectureでAPI Serverを構築してみる - Qiita
[DDD]ドメイン駆動 + オニオンアーキテクチャ概略 - Qiita
Goでクリーンアーキテクチャを試す | POSTD
GitHub - bxcodec/go-clean-arch: Go (Golang) Clean Architecture based on Reading Uncle Bob's Clean Architecture

ここら辺を参考に以下を作成
GitHub - takafk9/sample-clean-arch

やっぱり手を動かして自分で作ってみると全然理解度が変わってきますね。(理解できたとはいってない)

クリーンアーキテクチャの書籍を読んだのでAPIサーバを実装してみた - Qiita

3.DIコンテナ
registryではコンストラクタインジェクションを用いて全ての依存を解決している。
ここも以下の記事にまとめたので参照してほしい
https://qiita.com/yoshinori_hisakawa/items/a944115eb77ed9247794

一部抜粋すると、@yoshinori_hisakawaさんの自作DIコンテナを参考に実装しました。パッケージ構成もかなり参考にさせていただきました。(大大感謝)

app_registry.go@fukubaka0825

package registry

import "github.com/jinzhu/gorm"

type interactor struct {
    memberInteractor
    postInteractor
}

type Interactor interface {
    MemberInteractor
    PostInteractor
}
func NewInteractor(conn *gorm.DB) Interactor {

    return &interactor{
        memberInteractor{conn},
          postInteractor{conn},
    }
}

post_registry.go@fukubaka0825

package registry

import (
    "github.com/jinzhu/gorm"
    "sample-clean-arch/infrastructure/web/handler"
    "sample-clean-arch/infrastructure/datastore"
    "sample-clean-arch/interface/controllers"
    "sample-clean-arch/interface/presenters"
    "sample-clean-arch/usecase/presenter"
    "sample-clean-arch/usecase/repository"
    "sample-clean-arch/usecase/service"
)

type postInteractor struct {
    conn *gorm.DB
}

type PostInteractor interface {
    NewPostHandler() handler.PostHandler
}

func NewPostInteractor(conn *gorm.DB) PostInteractor {
    return &postInteractor{conn}
}


func (i *postInteractor) NewPostHandler() handler.PostHandler {
    return handler.NewPostHandler(i.NewPostController())
}

func (i *postInteractor) NewPostController() controllers.PostController {
    return controllers.NewPostController(i.NewPostService())
}

func (i *postInteractor) NewPostService() service.PostService {
    return service.NewPostService(i.NewPostRepository(), i.NewPostPresenter())
}

func (i *postInteractor) NewPostRepository() repository.PostRepository {
    return datastore.NewPostRepository(i.conn)
}

func (i *postInteractor) NewPostPresenter() presenter.PostPresenter {
    return presenters.NewPostPresenter()
}

コンポーネント関係で、handler呼び出す時にとても冗長になってしまうので、こいつで一行でnewできるようにしました。

DIPとDIについて、だから必要なのかぁと本当に慣れたのはこのフェーズでですね。

アクターが違うから、interface層としてpresenterとcontrollerは分けるべきで、usecase層のserviceが同じ層のpresenterIFに依存して、そのserviceにcontrollerが依存するから間接的にcontrollerはpresenterIFに依存するという部分が面白かった。

前回と同様至らないところはたくさんありますが、クリーンアーキテクチャってどういう特徴があって、どういう旨味があるかというところを実感できたので十分かなと思います。

3.これから実務で理解を深め、サービスに落とし込んでいく

印象としては、こんな簡単なものでもこのコード量になるのかぁって感じではありました。

工数との兼ね合いはありそうですが、サービスがスケールしていくと仮定して、MVCでベタベタにモノシリックに作ってしまった場合の改修や保守コスト、技術的負債を返していく労力を考えれば極力クリーンな構造で作りたいなぁと思いました。

しかし理想ばっかりインプットしても机上の空論なので、こういった視点も深めつつ、とりあえずは一刻も早くVideo Kicksチームの一員として戦力になれるよう、業務で悪戦苦闘しながら実装力メインで鍛えていきます。

個人としては僕はもともと金融系SIで働いていて、Dev|絶壁|Ops開発だったことの反動で、devops思想に取り憑かれているので、インフラのコード化やその辺の組織論文化論を中心に深掘っていき、その辺のアウトプットもしていこうと思っています。ここら辺を徐々に自分の強い領域にして業務でも貢献していきたい。

Web系一年目、頑張りの軌跡もWanoの技術ブログにもちょこちょこアウトプットしていきますので暖かく見守りながら、愛のまさかりをぶん投げてください。

次回は、「AWS 認定ソリューションアーキテクト – アソシエイトとってみた」で来月投稿予定です!(これでとれなかったらどうしよう、、、笑)

== 以下雑談 ==


先日いってきたmercarigoというイベントで、二つも可愛いgopher君ステッカーを手に入れました。
イベントも素晴らしく勉強になったし次回も参加必須(多分当たらないけど笑)。
うちでも僕が脱ビギナーできたら主催したいな)

あとこのiphoneケース欲しい、、、

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

beego 入門 model/migration 編 その①

はじめに

今回はModelの作成とDBへのマイグレーションの仕方を書く。

前提

  • beeコマンドでapiプロジェクトの作成が完了している(今回はbapiというPJを作成)
  • プロジェクト生成時に作成された、user model/controllerが削除済みである
  • mysqlのインストールが完了している

DB準備

ドライバのインストール

go get github.com/go-sql-driver/mysql

conf/app.confへの設定

appname = bapi
httpport = 8080
runmode = dev
autorender = false
copyrequestbody = true
EnableDocs = true
sqlconn =

// ここから追記
mysqluser = 接続したいユーザ名
mysqlpass = 上記ユーザのPW
mysqlurl = 繋ぎ先のIP等
mysqldb = 繋ぎ先のDB

main.go への設定

公式ドキュメントにも接続設定をどこへ書けばよいのか記載がないので、他の人の設定を真似てます


package main

import (
    "bee_api/models"
    _ "bee_api/routers"
    "fmt"

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

func main() {
    setupDB()
    if beego.BConfig.RunMode == "dev" {
        beego.BConfig.WebConfig.DirectoryIndex = true
        beego.BConfig.WebConfig.StaticDir["/swagger"] = "swagger"
    }
    beego.Run()
}

// 接続設定と起動時のマイグレーションを行う設定
func setupDB() {
    // 接続設定
    orm.RegisterDriver("mysql", orm.DRMySQL)

    mysqlUser := beego.AppConfig.String("mysqluser")
    mysqlPass := beego.AppConfig.String("mysqlpass")
    mysqlHost := beego.AppConfig.String("mysqlurls")
    mysqldb := beego.AppConfig.String("mysqldb")

    orm.RegisterDataBase("default", "mysql", mysqlUser+":"+mysqlPass+"@"+mysqlHost+"/"+mysqldb+"?charset=utf8")

        // マイグレーション設定。
    orm.RegisterModel(
        // サーバ起動時にマイグレーションしたいモデルを記述する
        new(models.User),
    )
    err := orm.RunSyncdb("default", false, true)
    if err != nil {
        fmt.Println(err)
    }
}

model作成

% cd $GOPATH/src/bapi # 作成したapiプロジェクトに移動

# model作成コマンド
# fieldsで構造体のプロパティを設定できる
% bee generate model user -fields="id:int64, userName:string, age:int"
______
| ___ \
| |_/ /  ___   ___
| ___ \ / _ \ / _ \
| |_/ /|  __/|  __/
\____/  \___| \___| v1.10.0
2019/03/02 04:19:57 INFO     ▶ 0001 Using 'User' as model name
2019/03/02 04:19:57 INFO     ▶ 0002 Using 'models' as package name
        create   $GOPATH/src/bapi/models/user.go
2019/03/02 04:19:57 SUCCESS  ▶ 0003 Model successfully generated!

生成されたmodel

基本的なCRUD処理はデフォルトで生成される。

package models

import (
    "errors"
    "fmt"
    "reflect"
    "strings"

    "github.com/astaxie/beego/orm"
)

type User struct {
    Id       int64
    UserName string `orm:"size(128)"`
    Age      int
}

// 以下CRUD処理が自動生成

次回から作成したmodelへの設定、マイグレーションを行う

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

beego 入門 環境構築編

はじめに

フルスタックのフレームワークが使いたい!!って思って色々試した結果、
beegoがいい感じだったので勉強した内容を書き溜めていく。

beegoとは?

https://beego.me

astaxieさんが作られたGo言語のRESTful HTTPフレームワーク。
コードの自動生成機能やORMやマイグレーション機能の使い勝手が良い。

他フレームワークと比較した場合の優位性

  • CLIツールが多機能

    • Model, View, Controller生成(CRUDレベルの処理ならコードも生成してくれる)
    • プロジェクト生成
  • ORM,マイグレーション機能が使いやすい

    • サーバ起動時にマイグレーション実行なんてことができる
    • 構造体で定義した内容がそのままDBに反映される
 // user
 type User struct {
    Id      int64     `orm:"auto" json:"id"`
    Name    string    `orm:"size(128)" json:"name"`
    Age     int       `orm:"default(0)" json:"age"`
    Created time.Time `orm:"auto_now_add;type(datetime)" json:"created"`
    Updated time.Time `orm:"auto_now_add;type(datetime)" json:"updateds"`
  }
-- 生成されたテーブル@DDL
CREATE TABLE `user` (
  `id` bigint(20) NOT NULL AUTO_INCREMENT,
  `name` varchar(128) NOT NULL DEFAULT '',
  `age` int(11) NOT NULL DEFAULT '0',
  `created` datetime NOT NULL,
  `updated` datetime NOT NULL,
  PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=3 DEFAULT CHARSET=utf8
  • swaggerが便利!

環境構築

前提

  • Go 1.1以上がインストールされている
  • GOPATHが設定されている
# beego自体のインストール
% go get -u github.com/astaxie/beego

# beeというCLIツールのインストール
% go get -u github.com/beego/bee

# バージョン確認
% bee version
______
| ___ \
| |_/ /  ___   ___
| ___ \ / _ \ / _ \
| |_/ /|  __/|  __/
\____/  \___| \___| v1.10.0

├── Beego     : 1.11.1
├── GoVersion : go1.11.5
├── GOOS      : darwin
├── GOARCH    : amd64
├── NumCPU    : 8
├── GOPATH    : 設定したGOPATH
├── GOROOT    : /usr/local/Cellar/go/1.11.5/libexec
├── Compiler  : gc
└── Date      : Friday, 1 Mar 2019

簡単な動作確認(API作成)

% cd $GOPATH/src

# apiプロジェクトの作成
% bee api bapi
______
| ___ \
| |_/ /  ___   ___
| ___ \ / _ \ / _ \
| |_/ /|  __/|  __/
\____/  \___| \___| v1.10.0
2019/03/01 00:55:00 INFO     ▶ 0001 Creating API...
    create   $GOPATH/src/bapi
    create   $GOPATH/src/bapi/conf
    create   $GOPATH/src/bapi/controllers
    create   $GOPATH/src/bapi/tests
    create   $GOPATH/src/bapi/conf/app.conf
    create   $GOPATH/src/bapi/models
    create   $GOPATH/src/bapi/routers/
    create   $GOPATH/src/bapi/controllers/object.go
    create   $GOPATH/src/bapi/controllers/user.go
    create   $GOPATH/src/bapi/tests/default_test.go
    create   $GOPATH/src/bapi/routers/router.go
    create   $GOPATH/src/bapi/models/object.go
    create   $GOPATHo/src/bapi/models/user.go
    create   $GOPATH/src/bapi/main.go
2019/03/01 00:55:00 SUCCESS  ▶ 0002 New API successfully creat

% cd api
# 起動後  127.0.0.1:8080/swagger にアクセス
# テスト画面が確認できたら、127.0.0.1:8080/v1/user/ にアクセス
# user_11111のJSONが表示されればOK(サンプルコードにベタ書きされてるのでDBは不要)
% bee run -downdoc=true -gendoc=true
______
| ___ \
| |_/ /  ___   ___
| ___ \ / _ \ / _ \
| |_/ /|  __/|  __/
\____/  \___| \___| v1.10.0
2019/03/01 01:04:18 INFO     ▶ 0001 Using 'bapi' as 'appname'
2019/03/01 01:04:18 INFO     ▶ 0002 Downloading 'https://github.com/beego/swagger/archive/v3.zip' to 'swagger.zip'...
2019/03/01 01:04:20 SUCCESS  ▶ 0003 477861 bytes downloaded!
2019/03/01 01:04:20 INFO     ▶ 0004 Unzipping 'swagger.zip'...
2019/03/01 01:04:20 SUCCESS  ▶ 0005 Done! Deleting 'swagger.zip'...
2019/03/01 01:04:20 INFO     ▶ 0006 Initializing watcher...
bapi/routers
bapi
2019/03/01 01:04:21 INFO     ▶ 0007 Generating the docs...
2019/03/01 01:04:21 SUCCESS  ▶ 0008 Docs generated!
2019/03/01 01:04:23 SUCCESS  ▶ 0009 Built Successfully!
2019/03/01 01:04:23 INFO     ▶ 0010 Restarting 'bapi'...
2019/03/01 01:04:23 SUCCESS  ▶ 0011 './bapi' is running...
2019/03/01 01:04:23.029 [I] [router.go:271]  $GOPATH/src/bapi/controllers no changed
2019/03/01 01:04:23.029 [I] [router.go:271]  $GOPATH/src/bapi/controllers no changed
2019/03/01 01:04:23.034 [I] [asm_amd64.s:1333]  http server Running on http://:8080

swagger画面

作成したAPIのテストが簡単に行える機能
スクリーンショット 2019-03-01 1.13.35.png

出力されるJSON

{
  "user_11111": {
    "Id": "user_11111",
    "Username": "astaxie",
    "Password": "11111",
    "Profile": {
      "Gender": "male",
      "Age": 20,
      "Address": "Singapore",
      "Email": "astaxie@gmail.com"
    }
  }
}

所感

フルスタックのフレームワークが使いたく、gobuffaloの勉強をしていたのですが、
やはり格段にこちらの方が使いやすい。
中華系だってだけで避けていたのですが、個人的にはGo言語で一番のフレームワークだと思う。

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

構造体型をJSONシリアライズ時にbool型に変換して出力

encoding/json パッケージの Marshal では、

構造体のメンバのなかで、JSONシリアライズ対象にしたくないメンバには

このように json:"-" タグを付与すればよいですが、

type S struct {
    R R `json:"r"`
    T *T `json:"-"`
}

ここで、 *T の中身は具体的にシリアライズしたくはないけれど、
nil の場合は false , 実体を持つ場合は true ,
という存在判定のbool値としてJSON出力したい場合は、

func (t *T) MarshalJSON() ([]byte, error) {
    if t != nil {
        return []byte("true"), nil
    }
    return []byte("false"), nil
}

こういう風に *TMarshalJSON() ([]byte, error) を実装してあげることで、
JSON出力結果をブール値にできます。

https://play.golang.org/p/LZz4bq1L5wA

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