20190127のGoに関する記事は7件です。

第3回「FirebaseAuthによるログイン・ログアウト導入」@FirebaseAuth+Nuxt.js+Go(v1.11)+GAE開発シリーズ

お題

前回でFirebaseAuth導入前の準備が出来たので、今回からようやくFirebaseAuthを導入する。
今回は、ログイン・ログアウト。ただし、メールアドレスとパスワードでログインするパターンのみ。

前提

以下は他にいくらでもよい記事があるので省略。

  • 開発環境の構築(GolangやらYarnやらのインストール等)
  • Google Cloud Platform環境の取得(App Engine有効化)
  • Firebase環境の取得
  • Vue.jsやNuxt.jsのチュートリアル

開発環境

# OS

$ cat /etc/os-release 
NAME="Ubuntu"
VERSION="18.04.1 LTS (Bionic Beaver)"

# 依存モジュールのバージョン

package.jsonの内容より、以下の通り。

    "nuxt": "^2.3.4",
    "vuetify": "^1.3.14",
    "vuetify-loader": "^1.0.8",
    "@nuxtjs/axios": "^5.3.6"

# Yarn

$ yarn -v
1.12.3

# Golang

$ go version
go version go1.11.4 linux/amd64

実践

■FirebaseAuthをフロントエンドに導入

自作のWebサイトにFirebaseAuthの認証機能を使うにあたって、まず、Firebaseをフロントエンドのプロジェクトに導入する必要がある。
下記によると、FirebaseAPIを使うためのAPIキーやら認証ドメインやらの設定を書いてfirebase SDKのアプリ初期化コードを実行するとよいらしい。
https://firebase.google.com/docs/web/setup?hl=ja

導入コマンドと結果確認

$ yarn add firebase
  〜〜省略〜〜

$ yarn add firebase-admin
  〜〜省略〜〜
[package.json]
$ cat package.json 
{
  "name": "frontend",
   〜〜省略〜〜
  "dependencies": {
   〜〜省略〜〜
    "firebase": "^5.8.0",
    "firebase-admin": "^6.5.0",
   〜〜省略〜〜
  },
   〜〜省略〜〜
}

プラグインとして初期化コードを実装

Firebaseアプリの初期化は1回きりでいいのでプラグインとして実装

[frontend/plugins/firebase.js]
import firebase from 'firebase'

if (!firebase.apps.length) {
  firebase.initializeApp(
    {
      apiKey: process.env.apiKey,
      authDomain: process.env.authDomain,
      databaseURL: process.env.databaseURL,
      projectId: process.env.projectId,
      storageBucket: process.env.storageBucket,
      messagingSenderId: process.env.messagingSenderId
    }
  );
}

export default firebase

APIキー等の情報はprocess.envから取得
実際の値は下記の環境に応じて適用されるファイルに記載(間違ってもpublicなGitHubリポジトリにアップしてはならない・・・)
※このあたり、ローカルでだけ動作確認するレベルならローカルストレージに実値を持たせておけばいいのだけど、GAEデプロイするとなるとそうもいかない。。。

$ tree -L 1 frontend/
frontend/
├── README.md
├── assets
├── components
├── dist
├── env.development.js
├── env.production.js
├── jest.config.js
├── layouts
 〜〜省略〜〜
[frontend/env.development.js]
module.exports = {
  apiBaseUrl: 'http://localhost:8080/api/v1',
  apiKey: "<API_KEY>",
  authDomain: "<PROJECT_ID>.firebaseapp.com",
  databaseURL: "https://<DATABASE_NAME>.firebaseio.com",
  projectId: "<PROJECT_ID>",
  storageBucket: "<BUCKET>.appspot.com",
  messagingSenderId: "<SENDER_ID>",
}
[frontend/env.productio.js]
module.exports = {
  apiBaseUrl: 'https://<PROJECT_ID>.appspot.com/api/v1',
  apiKey: "<API_KEY>",
  authDomain: "<PROJECT_ID>.firebaseapp.com",
  databaseURL: "https://<DATABASE_NAME>.firebaseio.com",
  projectId: "<PROJECT_ID>",
  storageBucket: "<BUCKET>.appspot.com",
  messagingSenderId: "<SENDER_ID>",
}

■メールアドレス・パスワードを用いたSignIn

FirebaseAuthコンソールで事前のユーザ作成

なにはともあれ、最初のユーザがいないとログインができないので作っておく。

screenshot-console.firebase.google.com-2019-01-20-20-49-51-641.png

screenshot-console.firebase.google.com-2019-01-20-20-53-22-905.png

screenshot-console.firebase.google.com-2019-01-20-20-54-01-738.png

loginコンポーネントにFirebaseAuthのSignIn機能を実装

frontend/components/login.vueにSignInロジックを追加

[frontend/components/login.vue(<template>部分は省略)]
<script>
import firebase from '~/plugins/firebase'

export default {
  data() {
    return {
      email: '',
      password: '',
      errMsg: ''
    }
  },

  methods: {
    async login() {
      await firebase
        .auth()
        .signInWithEmailAndPassword(this.email, this.password)
        .then(res => {
          // ログイン正常終了時はログイン後の初期画面に遷移する。
          this.$router.push('/')
        })
        .catch(error => {
          this.errMsg = error.message
          console.log(
            'errorCode:' + error.code + ', errorMessage:' + error.message
          )
        })
    }
  }
}
</script>

動作確認

FirebaseAuthコンソールで事前登録したユーザのメールアドレスとパスワードを入力して「LOGIN」ボタン押下すると


screenshot-localhost-3000-2019-01-20-21-02-51-731.png


ログイン後トップ画面に遷移する。


screenshot-localhost-3000-2019-01-20-21-03-34-380.png


FirebaseAuthコンソールにて先ほど作成したユーザのログイン状況を見てみると

screenshot-console.firebase.google.com-2019-01-20-21-04-01-901.png

ちゃんとログイン日が記載されている。

試しにメールアドレスとパスワードなしでログインしてみると


screenshot-localhost-3000-2019-01-20-21-09-21-755.png


ちゃんとFirebaseAuthのSDKからエラーメッセージが取得できる。

■SignOut

SignInが終わったら、今度は当然SignOut

defaultレイアウトにFirebaseAuthのSignOut機能を実装

frontend/layouts/default.vueにSignOutロジックを追加

[frontend/layouts/default.vue]
<template>
  <v-app light>
    <v-btn
      class="mx-1 my-2 px-3 py-2 lime"
      @click="logout"
    >
      LOGOUT
    </v-btn>
    <nuxt />
  </v-app>
</template>

<script>
import firebase from '~/plugins/firebase'

export default {
  methods: {
    async logout() {
      await firebase
        .auth()
        .signOut()
        .then(res => {
          // ログアウト正常終了時はログイン画面に遷移する。
          this.$router.push('/login')
        })
        .catch(error => {
          console.log(
            'errorCode:' + error.code + ', errorMessage:' + error.message
          )
        })
    }
  }
}
</script>

動作確認

「LOGOUT」ボタンを押下すると


screenshot-localhost-3000-2019-01-20-22-27-45-393.png


SignOutが正常終了してログイン画面に遷移する。


screenshot-localhost-3000-2019-01-20-22-29-51-058.png


■ここまでの全ソース

https://github.com/sky0621/Dotato-di-una-libreria/tree/f88a29e52e06a81665fc669eb9cf503322fa9555/frontend

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

第2回「FirebaseAuth導入前(ログインフォーム実装とバックエンドプロジェクトのガワ作成)」@FirebaseAuth+Nuxt.js+Go(v1.11)+GAE開発シリーズ

お題

前回に続き、今回もまだFirebaseAuth導入前。ログインフォームを実装する。
また、フロントだけでなくバックエンドともゆくゆく連携が必要なのでガワを作っておく。

前提

以下は他にいくらでもよい記事があるので省略。

  • 開発環境の構築(GolangやらYarnやらのインストール等)
  • Google Cloud Platform環境の取得(App Engine有効化)
  • Firebase環境の取得
  • Vue.jsやNuxt.jsのチュートリアル

開発環境

# OS

$ cat /etc/os-release 
NAME="Ubuntu"
VERSION="18.04.1 LTS (Bionic Beaver)"

# 依存モジュールのバージョン

package.jsonの内容より、以下の通り。

    "nuxt": "^2.3.4",
    "vuetify": "^1.3.14",
    "vuetify-loader": "^1.0.8",
    "@nuxtjs/axios": "^5.3.6"

# Yarn

$ yarn -v
1.12.3

# Golang

$ go version
go version go1.11.4 linux/amd64

実践

frontend/components/login.vueを下記の通り修正してみる。
せっかくvuetifyを使っていても、デザインは適当。。。
とりあえず、FirebaseAuthを使うにあたって、べたな「メールアドレス」と「パスワード」での認証を試す予定なので、フォームの要素もそれに合わせる。
※バリデーションやエラーハンドリングなど、お試しレベルでは不要なものは未実装。

修正後のソース

[frontend/components/login.vue]
<template>
  <v-layout
    class="py-3"
  >
    <v-form>
      <v-text-field
        v-model="email"
        label="Email"
        class="py-2"
      />
      <v-text-field
        v-model="password"
        label="Password"
        :type="`password`"
        class="py-2"
      />
      <v-btn
        class="mx-1 my-2 px-3 py-2 lime"
        @click="login"
      >
        LOGIN
      </v-btn>
    </v-form>
  </v-layout>
</template>

<script>
export default {
  data() {
    return {
      email: '',
      password: ''
    }
  },

  methods: {
    login: function() {
      console.log('login')
      // FIXME: ここにFirebaseAuthを用いたログイン処理を書く想定

      // ログイン正常終了時はログイン後の初期画面に遷移する。
      this.$router.push('/')
    }
  }
}
</script>

修正後の画面表示


screenshot-localhost-3000-2019-01-20-16-25-55-927.png


■ログイン後トップ画面のデザイン修正

固定文字列を表示していたfrontend/pages/index.vueを修正する。
ログイン後には、お知らせくらい表示するだろうということで、「お知らせコンポーネント」を追加。
それを使うことにする。

修正後のソース

[frontend/pages/index.vue]
<template>
  <v-content>
    <notice />
  </v-content>
</template>

<script>
import notice from '~/components/notice.vue'

export default {
  components: {
    notice
  }
}
</script>
[frontend/components/notice.vue]
<template>
  <v-layout
    class="py-3"
  >
    <v-list two-line>
      <v-list-tile v-for="notice in notices" :key="notice">
        <v-list-tile-content class="mb-2">
          <v-list-tile-title>{{ notice.sentence }}</v-list-tile-title>
        </v-list-tile-content>
      </v-list-tile>
    </v-list>
  </v-layout>
</template>

<script>
export default {
  data() {
    return {
      notices: []
    }
  },

  mounted() {
    this.$axios
      .get(process.env.apiBaseUrl + '/notices')
      .then(res => {
        console.log(res.data)
        this.notices = res.data
      })
      .catch(err => {
        console.log(err)
      })
  }
}
</script>

お知らせの内容は axiosを用いてバックエンド(GoでWebAPIを実装)から取得する。

修正後の画面表示


screenshot-localhost-3000-2019-01-20-16-26-44-061.png


■バックエンド(GoによるWebAPI実装)のディレクトリ構造

巷ではクリーン・アーキテクチャが流行りだけど、お試しプロジェクトでそこまでの汎用性は不要なので、当初はmain.goだけで済まそうとした。
ただ、一応、ある程度機能拡張する可能性も考慮し、MVC+S くらいの作りにはしておくことに。
※MVC+Sに関しては過去に記事化していた。
https://qiita.com/sky0621/items/c7b196a1ba0e126cc3f5

$ tree -L 2 backend/
backend/
├── README.md
├── app.yaml
├── controller
│   ├── apierror.go
│   ├── form
│   ├── notice.go
│   ├── response
│   └── router.go
├── go.mod
├── go.sum
├── index.yaml
├── logger
│   └── logger.go
├── main.go
├── middleware
│   ├── basic.go
│   └── custom.go
├── model
│   ├── dto.go
│   └── notice.go
├── service
│   └── notice.go
├── system
│   ├── local.go
│   └── setting.go
├── util
│   ├── stringbuilder.go
│   └── util.go
└── view

★controller -> service -> model の呼び出し関係
★viewの下にはfrontendでビルドしたファイルを出力する。

■バックエンドの使用フレームワーク

■バックエンドのつくり

この時点ではFirebaseAuthとの絡みは出てこないので省略。
実際のソースは下記。
https://github.com/sky0621/Dotato-di-una-libreria/tree/38261c1768ca8da0aba77ced9d5a1b795172e89c/backend

■ここまでの全ソース

https://github.com/sky0621/Dotato-di-una-libreria/tree/38261c1768ca8da0aba77ced9d5a1b795172e89c

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

Golangのエラーハンドリングの基本

想定している読者

Goのエラーハンドリングについて体系だった記事が見つからなかったので、色々調べたことを整理して備忘録も兼ねて記事にしました。
以下のような方を読者対象に記事を書いています。

  • Goのエラーハンドリングの基本的なやり方を知りたい
  • スタックトレースを標準エラー出力したい
  • エラーの種類に応じてステータスコードを変えたい

まずデファクトになっているエラーパッケージpkg/errorsの使い方を確認して、実際のWebアプリケーションで追加で実装しないといけないことを説明していきます。

そもそもerrorとは?

goのエラーは以下のようなエラーメッセージを返す関数を実装していれば満たせるインターフェースです。

// cf. https://golang.org/pkg/builtin/#error
type error interface {
  Error() string
}

実際には返されたエラーがnilかどうかで条件分岐して、nilでない場合はerror.Error()でエラー内容を出力するような使われ方をします。
例として、引数で与えられたファイルを開いて、それをJSONとして扱って、map[string]interface{}型の値(jsonMap)に変換してみます。

import (
    "encoding/json"
    "fmt"
    "io/ioutil"
)

func main() {
  // 中身が空のファイルをJSONとして扱うとjson.Unmarshalでエラーになる
    if _, err := unmarshalToMap("src.json"); err != nil {
    // err.Error()の結果が出力される
    fmt.Println(err)
    // unexpected end of JSON input
  }
}

func unmarshalToMap(src string) (map[string]interface{}, error) {
    jsonMap := map[string]interface{}{}
  // ファイルパスからファイルを読み込む
    data, err := ioutil.ReadFile(src)
    if err != nil {
        return jsonMap, err
    }
  // ファイルの内容をJSONとみなして、key, valueの対応をmap[string]interface{}型の値にする
    if err := json.Unmarshal(data, &jsonMap); err != nil {
        return nil, err
    }
    return jsonMap, nil
}

しかし、引数で渡されたファイルsrc.jsonは何も書かれていないので(JSONの書式に沿っていないので)エラーが返ってきます。

$ go run main.go
unexpected end of JSON input

このように、Error() stringさえ実装してエラーメッセージを返せればerror型を満たすことができます。

一見、上記のような方法でも問題がなさそうですが、実際のアプリケーションは実装が多くなり、複雑になりがちにも関わらず、unexpected end of JSON input というエラー内容だけだと、どのファイルの内容をjson.Umarshalした時に起こったのか分からないとデバッグが困難になります。そこで重要なのが、返ってきたエラーに「何を」、「どこで」、「どんな処理で」起こったのかコンテキスト情報を付与することです。

コンテキスト情報をエラー内容に含める

返ってきたエラーにコンテキスト情報を付与する一番簡単な方法はfmt.Errorを利用することです。
これは、指定したフォーマットにしたがって、第二引数以降をフォーマットし、新しいエラーメッセージを持つエラーを作って返すことができます。

先ほどの実装でfmt.Errorfを使ってみます。

func unmarshalToMap(src string) (map[string]interface{}, error) {
    jsonMap := map[string]interface{}{}
    // ファイルパスからファイルを読み込む
    data, err := ioutil.ReadFile(src)
    if err != nil {
        return jsonMap, err
    }
    if err := json.Unmarshal(data, &jsonMap); err != nil {
        return nil, fmt.Errorf("read %s, %s", src, err) // ここをfmt.Errorfに置き換えた
    }
    return jsonMap, nil
}

これによって以下のようにどのファイルを読み込んでエラーがでたのかメッセージに含めることができます。

$ go run main.go
read src.json, unexpected end of JSON input

このようにfmt.Errorfを使うことで「どこで」、「どんな処理で」エラーが起こったのか知ることができます。

fmt.Errorfの問題点

しかし、これで問題なしかというとそうではありません。理由はfmt.Errorfは元のerrorインターフェースを実装するある型と値を消失させるからです。
fmt.Errorfの実装を見ると、内部でerrors.Newを呼び出していることがわかります。

// Errorf formats according to a format specifier and returns the string
// as a value that satisfies error.
func Errorf(format string, a ...interface{}) error {
    return errors.New(Sprintf(format, a...))
}

errors.Newとは、errorインターフェースを実装したエラーメッセージだけを持つ構造体を返します。

// cf. https://golang.org/pkg/errors/#New

// New returns an error that formats as the given text.
func New(text string) error {
    return &errorString{text}
}

// errorString is a trivial implementation of error.
type errorString struct {
    s string
}

func (e *errorString) Error() string {
    return e.s
}

しかし、ライブラリによって、独自のerrorインターフェースを実装した構造体を定義して、エラーメッセージ以外の情報を付与していることがよくあります。これによって、受け取った(errorインターフェース型に抽象化された)エラーを型アサーションして元の型に戻して、付加された値を取り出すことができます。fmt.Errorfは上記のerrorString構造体に作り直してしまうのでこれをできなくしてしまいます。

例えば、JSONのKey, Valueの対応はSliceで表現することはできないのでUnmarshalするとエラーになりますが、そのエラーはJSONのバイト列におけるエラーが起こった位置やUnmarshalしようとした型の名前をエラーメッセージ以外を持っています。

具体的には、以下のUnmarshalTypeErrorがerrorインターフェースを実装しており、それが返ってきます。

// cf. https://golang.org/pkg/encoding/json/#UnmarshalTypeError

// An UnmarshalTypeError describes a JSON value that was
// not appropriate for a value of a specific Go type.
type UnmarshalTypeError struct {
    Value  string       // description of JSON value - "bool", "array", "number -5"
    Type   reflect.Type // type of Go value it could not be assigned to
    Offset int64        // error occurred after reading Offset bytes
    Struct string       // name of the struct type containing the field
    Field  string       // name of the field holding the Go value
}

func (e *UnmarshalTypeError) Error() string {
    if e.Struct != "" || e.Field != "" {
        return "json: cannot unmarshal " + e.Value + " into Go struct field " + e.Struct + "." + e.Field + " of type " + e.Type.String()
    }
    return "json: cannot unmarshal " + e.Value + " into Go value of type " + e.Type.String()
}

実際に、対応していない型にUnmarshalするとUnmarshalTypeError型に型アサーションして、エラーメッセージ以外を取り出してみます。

import (
    "encoding/json"
    "fmt"
)

var jsonData = []byte(`
{
    "name": "user",
    "password": "pass"
}
`)

func main() {
  // []string型はJSONのKey, Valueの内容を持つことができないのでUnmarshalするとエラーになる
    valueOfInvalidType := make([]string, 0)
    err := json.Unmarshal(jsonData, &valueOfInvalidType)
    switch err := err.(type) {
    case *json.UnmarshalTypeError:
        fmt.Printf("type: %s\n", err.Type)
        fmt.Printf("offet: %d\n", err.Offset)
        fmt.Printf("Error(): %s\n", err)
    default:
        fmt.Println(err)
    }
}
$ go run main.go
type: []string
offet: 2
Error(): json: cannot unmarshal object into Go value of type []string

このようにしたくとも、fmt.Errorfが既存のError()の結果以外の情報を切り捨ててしまうので、以下のような問題に直面します。

  • errorインターフェースの元の型に応じて条件分岐ができなくなる
  • errorインターフェースの元の型の持つコンテキスト情報が消失する

これらの問題を解決する手段でかつ、現在(2019/01/22)でデファクトとなっているエラーパッケージとして、pkg/errorsがあります。

pkg/errorsを使う

では、具体的にpkg/errorsで上記問題を解決できるのかというと、以下のWrapCauseを用います。

func Wrap(err error, message string) error
func Cause(err error) error

まず、Wrapを使うことで元のerrorインターフェースを実装した型と値を保持して、エラーメッセージだけコンテキスト情報を追加した新しいものにできます。

if err := json.Unmarshal(data, &jsonMap); err != nil {
    // failed to unmarshal src.json: unexpected end of JSON input
        return nil, errors.Wrap(err, "failed to unmarshal src.json")
}

この結果だけ見れば、fmt.Errorf("failed to unmarshal scr.json: %s", err)した結果と同じですが、%+vでフォーマットするとStackTraceを出力することもできます。

// cf. https://godoc.org/github.com/pkg/errors#hdr-Formatted_printing_of_errors
if err := json.Unmarshal(data, &jsonMap); err != nil {
    fmt.Printf("%+v", err)
}
main.unmarshalToMap
        /go/src/github.com/shoichiimamura/error-handling-example/main.go:30
main.main
        /go/src/github.com/shoichiimamura/error-handling-example/main.go:18
runtime.main
        /usr/local/go/src/runtime/proc.go:198
runtime.goexit

さらにCauseを使うことでWrapされたエラーから元のerrorインターフェースを実装した型と値を取り出すことができます。
より厳密に言うと、causerインターフェースを実装していない一番最後のerrorインターフェース型を取り出します。

// cf. https://github.com/pkg/errors/blob/master/errors.go#L269
func Cause(err error) error {
    type causer interface {
        Cause() error
    }

    for err != nil {
        cause, ok := err.(causer)
        if !ok {
            break
        }
        err = cause.Cause()
    }
    return err
}

これを使うと、以下のように元のエラーの型に応じた条件分岐が可能になります。

switch err := errors.Cause(err).(type) {
case *json.UnmarshalTypeError:
  fmt.Println(err.Offset)
case *json.InvalidUnmarshalError:
  fmt.Println(err.Type)
default:
  fmt.Println(err)
}

このCauseでエラーの型に応じた条件分岐ができることがわかりました。

しかし、実際のAPIを持つアプリケーションサーバーの開発では、エラーの種類に応じてHTTPステータスコードを変えたいことがよくあります。これを実現するためには、pkg/errorsを拡張したエラーパッケージを作る必要があります。次はpkg/errorsを利用して、エラーの種類の応じたHTTPステータスコードを返す実装をしてみます。

エラーに応じてステータスコードを選ぶ

pkg/errorsを使った以下のようなerrorインターフェースを実装した構造体を作ることで、WrapCauseを使えつつ、エラータイプを取得できるようになります。errors.Wrapfなどは説明を簡単にするためにラップしていませんが、同じような要領で実装することもできます。

import (
    "github.com/pkg/errors"
)

// ErrorType エラーの種類
type ErrorType uint

const (
    Unknown ErrorType = iota
    InvalidArgument
    Unauthorized
    ConnectionFailed
)

// ErrorTypeを返すインターフェース
type typeGetter interface {
    Type() ErrorType
}

// ErrorTypeを持つ構造体
type customError struct {
    errorType     ErrorType
    originalError error
}

// New 指定したErrorTypeを持つcustomErrorを返す
func (et ErrorType) New(message string) error {
    return customError{errorType: et, originalError: errors.New(message)}
}

// Wrap 指定したErrorTypeと与えられたメッセージを持つcustomErrorにWrapする
func (et ErrorType) Wrap(err error, message string) error {
    return customError{errorType: et, originalError: errors.Wrap(err, message)}
}

// Error errorインターフェースを実装する
func (e customError) Error() string {
    return e.originalError.Error()
}

// Type typeGetterインターフェースを実装する
func (e customError) Type() ErrorType {
    return e.errorType
}

// Wrap 受け取ったerrorがErrorTypeを持つ場合はそれを引き継いで与えられたエラーメッセージを持つcustomErrorにWrapする
func Wrap(err error, message string) error {
    we := errors.Wrap(err, message)
    if ce, ok := err.(typeGetter); ok {
        return customError{errorType: ce.Type(), originalError: we}
    }
    return customError{errorType: Unknown, originalError: we}
}

// Cause errors.CauseのWrapper
func Cause(err error) error {
    return errors.Cause(err)
}

// GetType ErrorTypeを持つ場合はそれを返し、無ければUnknownを返す
func GetType(err error) ErrorType {
    for {
        if e, ok := err.(typeGetter); ok {
            return e.Type()
        }
        break
    }
    return Unknown
}

これによって、任意のErrorTypeを持つエラーを作って、Controller層でそれを取り出し、対応するステータスコードを選ぶことができます。

func main() {
  err := Unauthorized.New("ある認証の処理内で返されたエラー")
  fmt.Println(statusCode(err)) // 401
}

func statusCode(err error) int {
  switch GetType(err) {
  case ConnectionFailed:
    return http.StatusInternalServerError // 500
  case Unauthorized:
    return http.StatusUnauthorized // 401
  default:
    return http.StatusBadRequest // 400
  }
}

Panicでアプリケーションをクラッシュさせない

panicとは関数呼び出し元の処理を連続的に中断するgoの組み込み関数のことです。
明示的に呼び出すこともできますし、他にはnilポインタにメソッド呼び出しをした時などでも起きます。

type User struct {
  Name string
}
func (u *User) Name() string {
  return u.Name
}

var user *models.User
user.Name() // panic

panicが起こるとdefer内でrecover(後述)しないと、プログラムはCrashしてしまうので、そうさせないようにPanicが起きた旨をエラーにして返してあげるようにします。そのためには、まず、deferとrecoverについて概要を押さえる必要があります。

deferは、関数を登録することができ、その定義元の関数がreturnされた後に呼び出され、panicが起こった場合も呼びされます。
そのため以下の実装は、returnされた値(i)をdeferでインクリメントして出力しているので、結果は2になります。

func a() (i int) {
    defer func() {
        i++
        fmt.Printf("%d\n", i)
    }()
    return 1
}

また、deferに登録した関数は後入れ先出し(Last in First out)で呼び出されるので、以下の実装は3210を出力します。

func b() {
    for i := 0; i < 4; i++ {
        defer fmt.Print(i)
    }
}

そして、recoverはpanicが起こったgoroutineを再び制御する組み込み関数で、panicが起こった後にdeferが呼ばれ、その中でpanicの伝播を止める役割を担います。defer外でrecoverしてもpanic時には呼び出されず、nilを返すだけなので、この使われ方以外はなさそうです。

以下は、panicが起こった時にdefer内でrecoverを呼び出し、panicの伝播を止めてエラーを返しています。

import (
    "fmt"
    "github.com/pkg/errors"
)

func panicAndRecover() (err error) {
    defer func() {
        if r := recover(); r != nil {
            err = errors.New(fmt.Sprintf("recovered: %v\n", r))
        }
    }()
    panic("panic at panicAndRecover")
    return
}

func main() {
  err := panicAndRecover()
  fmt.Println(err)
  // recovered: panic at panicAndRecover
}

こうすることで、panicが起こってもエラーとして扱うことができます。

参考

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

daemontools: setlock by katoris2014

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

Vue.js + TypeScript + GAE/Go で SPA 開発をしている感想

自己紹介

Twitter @mochizukikotaro

普段の仕事は、

  • Rails、CakePHP がメイン
  • Laravel はこれから入門
  • Vue.js、AWS、GCP、k8s、Go は少し触ります

プライベートで、

  • SPA の Webアプリケーションを作り中
  • 英語勉強中。2019年中に TOEIC 900点をとりたい(615点/2018年)

構成


Vue.js + TypeScript + Go + GAE/Go 1.11

1年前
Vue.js + Vuex + Go + heroku


変更の理由

  • わりと勉強がてらのプロジェクトなので、深い理由はなく。
  • TS はみんなが良いっていうから。
  • GAE/Go は Go の runtime を動かすだけなら heroku よりカンタンなイメージだから。
  • Vuex は前しんどくて、今はつくり直し中でそこまで大きくないから。なので、今後導入する可能性は高い。

Vue.js + TypeScirpt + Vuex 使っている方いたら、感触教えてください :bow:


良い点、学んだ点

  • TS 気持ちいいい
  • GAE/Go へのデプロイが楽
  • 開発環境の CORS は vue の proxy で対応

困った点

  • TS の declaration file がない問題
  • GAE/Go のスタンダード環境だとインスタンスに ssh できない(そんな困ってない)
  • GAE/Go へのデプロイ gcloud app deploy が遅い

良い点、学んだ点


TS 気持ちいい

あんまり頑張ってないです。使えるところは使う感じです。
例えば interface 使うので、オブジェクトがどんなものか分かるし、エディタの補完も気持ちいい。

IUser.vue
<script lang="ts">
export interface IUser {
  ID: number;
  ScreenName: string;
  TwitterID: number;
  ProfileImage: string;
}
</script>

interface に対して I prefix を使う使わない問題みたいのもあるようですが、ぼくは結論 I をつけています。


GAE/Go へのデプロイが楽

app.yaml
runtime: go111
$ gcloud app deploy

これだけで、GAE にデプロイができる。


開発環境の CORS について

開発環境は docker-compose でやっていますので、node(frontend) と go(backend) のコンテナがあります。

docker-compose.yml
version: '3'
services:
  golang:
    ports: 
      - "8081:8080"
      ...
  note:
    ports:
      - "8080:8080" 
      ...

node は loalhost:8080 で server は localhost:8081 で動かしているので、フロントからサーバーへのリクエストで CORS 問題がおきます。


以前はサーバー側でなんとかしてました ?

server.go
e.Use(middleware.CORSWithConfig(middleware.CORSConfig{
    AllowOrigins:     []string{"http://localhost:8080"},
    AllowCredentials: true,
}))

今は vue の proxy で対応 ?

vue.config.js
module.exports = {
  devServer: {
    proxy: {
      '/api': {
        target: 'http://golang:8080'
      }
    }
  }
}

これで、 AllowOrigins とか書かなくて済みました。

Vue CLI 3 Configuration Reference: devServer.proxy


困った点


TS declaration file がない問題

なにかしらのパッケージを使っている状態で yarn build などすると現れるエラー。

import Prism from "vue-prism-component";
$ yarn build
...
Could not find a declaration file for module '...'.

PullRequest を見てみると

スクリーンショット 2019-01-27 13.27.29.png

ちゃんとでています。が、マージはされていません。

image.png

マージされるのを祈るばかり...?


結局どうしたのか

今回使いたかったのは、 vue-prism-component というコードハイライターの Prismjs を vue で使いやすくするパッケージでした。

image.png

なので、vue-prism-component を使わずに直接 Prismjs を使ってごにょごにょしようとおもったのですが...。

なんかうまく動かなかったのでフロントでやるのをやめて、サーバーサイドで対応することにしました ?

chroma: A general purpose syntax highlighter in pure Go

結果、フロントのコンポーネント構成などがスッキリしてよかったです...。


GAE/Go 1.11 の困りごと

  • スタンダード環境だと ssh できないけどフレキシブル環境だといけそう(ためしてません)
  • gcloud app deploy は普通に遅いと思いますが、 .gcloudignore で適切に ignore しておかないと、本当に無駄な時間をすごくことになるとおもいます。(泣きそうでした ?
.gcloudignore
frontend/node_modules/
frontend/public/
frontend/src/
vendor/

以上ありがとうございました!


補足(全体のファイル構成)

$ tree . -L 2
.
├── Gopkg.lock
├── Gopkg.toml
├── README.md
├── app.yaml
├── containers
│   ├── golang
│   └── node
├── db
│   └── db.go
├── docker-compose.yml
├── frontend
│   ├── README.md
│   ├── babel.config.js
│   ├── dist
│   ├── node_modules
│   ├── package.json
│   ├── postcss.config.js
│   ├── public
│   ├── src
│   ├── tsconfig.json
│   ├── vue.config.js
│   ├── yarn-error.log
│   └── yarn.lock
├── handler
│   ├── about.go
│   ├── auth.go
│   ├── note.go
│   └── user.go
├── highlight
│   ├── transform.go
│   ├── transformNote.go
│   ├── transformNote_test.go
│   └── transform_test.go
├── migrations
│   ├── 1812151700_add_users_table.down.sql
│   ├── 1812151700_add_users_table.up.sql
│   ├── 1812160000_add_notes_table.down.sql
│   └── 1812160000_add_notes_table.up.sql
├── model
│   ├── note.go
│   └── user.go
├── repository
│   └── note.go
├── server.go
├── templates
│   └── index.html
└── vendor
    ├── github.com
    └── google.golang.org
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

goとUNIXドメインソケットでpub/subっぽいことができるコマンドをつくる

UNIXのパイプの延長で、

  • 書き込み側プロセスはひとつ
  • 読み込み側プロセスは複数
  • 書き込み側プロセスで行を書き込むと、読み込み側プロセス全て書き込んだ行が出力される
  • 読み込み側プロセスは任意のタイミングで読み込みを開始できる(読み込みを開始した以降に書き込みされた行を読み込む)

という挙動のものがほしい。
(pub/subとはちょっと違うかもしれない)

これはtail -f -n 0で達成できますが、何らかの方法でファイルをときどき消すなどしてやらないとファイルが無限にでかくなっていきます。

UNIXドメインソケットを使うと良いかもと思ったのですが、UNIXドメインソケットを読み書きできるいい感じのコマンドがなかったのでgoで実装してみました。

コード

pub

pub.go
package main

import (
    "bufio"
    "io"
    "log"
    "net"
    "os"
)

var connections = make(map[net.Conn]bool)

func main() {
    go acceptConnections()

    stdin := bufio.NewReader(os.Stdin)
    for {
        b, err := stdin.ReadByte()
        if err == io.EOF {
            break
        }
        for conn := range connections {
            _, err := conn.Write([]byte{b})
            if err != nil {
                log.Println("socket write error:", err)
                conn.Close()
                delete(connections, conn)
                log.Println("connections count: ", len(connections))
            }
        }
        //log.Println("wrote:", b)
    }
}

func acceptConnections() {
    listener, err := net.Listen("unix", "./sock")
    if err != nil {
        log.Println("Listen error: ", err)
        return
    }
    defer listener.Close()

    for {
        conn, err := listener.Accept()
        if err != nil {
            log.Println("Accept error: ", err)
            continue
        }
        log.Println("Accepted")
        connections[conn] = true
        log.Println("connections count: ", len(connections))
    }
}

sub

sub.go
package main

import (
    "fmt"
    "net"
)

func main() {
    conn, err := net.Dial("unix", "./sock")
    if err != nil {
        fmt.Printf("Dial error: %s\n", err)
        return
    }
    defer conn.Close()

    buf := make([]byte, 1024)
    for {
        n, err := conn.Read(buf)
        if n == 0 {
            break
        }
        if err != nil {
            fmt.Printf("Read error: %s\n", err)
        }
        fmt.Print(string(buf[:n]))
    }
}

使ってみる

ビルドします。

ビルド
$ go build pub.go
$ go build sub.go

書き込み側プロセスを起動します。
1秒ごとに時刻を出力するワンライナーの出力をパイプでつないでみます。

送信側
$ while :; do date; sleep 1; done | ./pub

読み込み側のプロセスを起動すると、dateの結果がずらずらと流れてきます。

受信側
$ ./sub
2019年 1月27日 日曜日 05時47分16秒 JST
2019年 1月27日 日曜日 05時47分17秒 JST
2019年 1月27日 日曜日 05時47分19秒 JST
2019年 1月27日 日曜日 05時47分20秒 JST

別のターミナルを起動してもうひとつ./subをたちあげると、起動したタイミング以降に書き込まれたdateの結果がずらずらと流れてきます。

ToDo

  • 終了処理をちゃんとやる
  • UNIXドメインソケットのパスを引数で指定できるようにする
  • 入出力が逆バージョンもつくったら便利なのでは、複数のセンサの値を読んでひとつのログに書き出したりとかに使えそう
  • コマンド名はやはりpub/subではない気がするし、なんか考える
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

AWS Lambda Goで画像アップローダを作る

AWS Serverless Uploader
https://aws-serverless-uploader.netlify.com/

フロントエンド

  • React
  • Netlify

ReactプロジェクトをNetlifyにデプロイ。
CSSフレームワークはBulma。
API通信はAxiosを使用。

バックエンド

  • AWS Lambda Go
  • API Gateway
  • S3
  • DynamoDB
  • SAM CLI

Serverless frameworkかSAM CLIかは好きな方で良さそう。
自分的にはAWS謹製という点に惹かれてSAM CLIを選定。

sam init --runtime go

でプロジェクトの雛形をバッと作ってくれてすぐにデプロイできるのと、AWS上の構成をコード管理できる(=infrastructure as code)ので重宝した。

GoでS3

aws-sdk-goを使用。

package main

import (
    "bytes"

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

func s3Put(img []byte) error {
    svc := s3.New(session.New(), &aws.Config{
        Region: aws.String("ap-northeast-1"),
    })
    if _, err := svc.PutObject(&s3.PutObjectInput{
        Bucket:               aws.String("バケット名"),
        ACL:                  aws.String("private"),
        ServerSideEncryption: aws.String("AES256"),
        Key:                  aws.String("キー名"),
        Body:                 bytes.NewReader(img),
    }); err != nil {
        return err
    }
    return nil
}

GoでDynamoDB

素のaws-sdk-goだけの実装はダルいようなのでgregu/dynamoライブラリを使用。

package main

import (
    "github.com/aws/aws-sdk-go/aws"
    "github.com/aws/aws-sdk-go/aws/session"
    "github.com/guregu/dynamo"
)

type Image struct {
    Name      string `json:"name" dynamo:"Name"`
    URL       string `json:"url" dynamo:"Url"`
    CreatedAt string `json:"createdAt" dynamo:"CreatedAt"`
}

// 書き込み
func dynamoPut(data Image) error {
    db := dynamo.New(session.New(), &aws.Config{Region: aws.String("ap-northeast-1")})
    if err := db.Table("テーブル名").Put(data).Run(); err != nil {
        return err
    }
    return nil
}

// 全件取得
func dynamoGetAll(data *[]Image) error {
    db := dynamo.New(session.New(), &aws.Config{Region: aws.String("ap-northeast-1")})
    if err := db.Table("テーブル名").Scan().All(data); err != nil {
        return err
    }
    return nil
}

次はAPIGateway+Lambda(Go)+Cognitoで認証処理を検証する予定。

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