20200602のGoに関する記事は8件です。

【Go】コンテナを理解するためにコンテナを自分で”造る”【初心者】

コンテナを理解するためにコンテナを自分で"造る"

今回はこちらを参考にして自作コンテナを作ってみる。

コンテナとは

詳しくは他の方の説明を受けてください。

Goでコンテナを"造る"

環境

今回はVMwareUbuntuを稼働させ、VM上で造りました。

Ubuntu 20.04 LTS 64-bit

go version go1.13.8 linux/amd64

まずはコンテナのルートディレクトリとなるFile Systemを作る

/home/rootfsディレクトリ下に今回のルートディレクトリとなるFile System(fs)を作成します。

/home/rootfs/
  ├── bin
  ├── lib
  ├── lib32
  ├── lib64
  ├── proc   \\空のディレクトリ
  └── sbin

bin,lib,lib32,lib64,sbinディレクトリは/usrディレクトリ下と同じものをコピーしました。

procディレクトリは空の状態で作成しておきます。

※今回はただコンテナとして動くことだけを目的としているので最小のfsにしています。

コンテナを稼働させるsrc【Go】

src次のようにしました。

package main

import (
    "fmt"
    "os"
    "os/exec"
    "syscall"
)

func main() {
    switch os.Args[1] {
    case "run":
        run()
    case "child":
        child()
    default:
        panic("what??")
    }
}

func run() {
    cmd := exec.Command("/proc/self/exe", append([]string{"child"}, os.Args[2:]...)...)
    cmd.Stdin = os.Stdin
    cmd.Stdout = os.Stdout
    cmd.Stderr = os.Stderr
    cmd.SysProcAttr = &syscall.SysProcAttr{
        Cloneflags: syscall.CLONE_NEWUTS | syscall.CLONE_NEWPID,
    }

    must(cmd.Run())
}

func child() {
    fmt.Printf("running %v as PID %d\n", os.Args[2:], os.Getpid())

    cmd := exec.Command(os.Args[2], os.Args[3:]...)
    cmd.Stdin = os.Stdin
    cmd.Stdout = os.Stdout
    cmd.Stderr = os.Stderr

    must(syscall.Chroot("/home/rootfs"))
    must(os.Chdir("/"))
    must(syscall.Mount("proc", "proc", "proc", 0, ""))
    must(cmd.Run())
}

func must(err error) {
    if err != nil {
        panic(err)
    }
}

実際にコンテナを起動してみる

まずはホストのfsProcessの確認

Terminalでホストのfsを確認しましょう。

~$ cd /
~$ ls
bin    dev   lib    libx32      mnt   root  snap      sys  var
boot   etc   lib32  lost+found  opt   run   srv       tmp
cdrom  home  lib64  media       proc  sbin  swapfile  usr

次に、ホストのProcessを確認

~$ ps
    PID TTY          TIME CMD
   1927 pts/0    00:00:00 bash
   2950 pts/0    00:00:00 ps

PID(Process ID)が大きい値であるのが確認できます。

コンテナを起動

満を持してコンテナを起動しましょう。

先ほどのsrcがあるディレクトリに移動し、Terminalで次のようにコマンドを入力します。

~:/home/{username}/{directoryname}$ sudo go run main.go run /bin/bash
running [/bin/bash] as PID 1
bash-5.0# 

おめでとうございます!

コンテナが起動しました!

コンテナがコンテナであるか確認する

ホストの時と同じようにfsProcessを確認してみましょう。

bash-5.0# cd /
bash-5.0# ls
bin  lib  lib32  lib64  proc  sbin

コンテナ内で/ディレクトリに移動してもホストの/home/rootfsディレクトリに移動しますね。

bash-5.0# ps
    PID TTY          TIME CMD
      1 ?        00:00:00 exe
      6 ?        00:00:00 bash
      8 ?        00:00:00 ps

PIDちゃんと小さい値です。

ついでにhistoryも確認しておきましょう。

bash-5.0# history 
    1  cd /
    2  ls
    3  ps
    4  history 

historyもコンテナ内の記録しか表示されません。

最後にコンテナを停止するには

コンテナ内でexitと入力すればOK

bash-5.0# exit
~:/home/{username}/{directoryname}$
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

マイクロサービスでも使えそうなエラー処理を考えてみた

元ネタ

考えてみたといっても全くのオリジナルではなくて、GoogleさんのDeveloper PageのError Handlingみてたらこれ応用したら汎用的に使えそうだなという感じでつくってみました。

元ネタは以下のようなものです。

Pattern.1 Cloud API

ref. https://cloud.google.com/apis/design/errors

jsonだとこのようになります。

codeにHTTP Status
statusにgRPCのステータス
messageにエラーの内容
詳細は配列で複数記載できる様になっています

{
  "error": {
    "code": 401,
    "message": "Request had invalid credentials.",
    "status": "UNAUTHENTICATED",
    "details": [{
      "@type": "type.googleapis.com/google.rpc.RetryInfo",
      ...
    }]
  }
}

Protobuf だとこのように定義されてました

// The error schema for Google REST APIs. NOTE: this schema is not used for
// other wire protocols.
message Error {
  // This message has the same semantics as `google.rpc.Status`. It has an extra
  // field `status` for backward compatibility with Google API Client Library.
  message Status {
    // This corresponds to `google.rpc.Status.code`.
    int32 code = 1;
    // This corresponds to `google.rpc.Status.message`.
    string message = 2;
    // This is the enum version for `google.rpc.Status.code`.
    google.rpc.Code status = 4;
    // This corresponds to `google.rpc.Status.details`.
    repeated google.protobuf.Any details = 5;
  }
  // The actual error payload. The nested message structure is for backward
  // compatibility with Google API client libraries. It also makes the error
  // more readable to developers.
  Status error = 1;
}

Pattern.2 Calender API

ref. https://developers.google.com/calendar/v3/errors

多少の際はあるものの、考え方は同じなようです。
レスポンスがJSONになる前提なためかstatusはありません。

{
 "error": {
  "errors": [
   {
    "domain": "calendar",
    "reason": "fullSyncRequired",
    "message": "Sync token is no longer valid, a full sync is required.",
    "locationType": "parameter",
    "location": "syncToken",
    }
  ],
  "code": 410,
  "message": "Sync token is no longer valid, a full sync is required."
 }
}

コンセプト

jsonのレスポンス

基本形(単一サービスの場合)

元ネタの形とほとんど同じになります。 errorsの配列の中身と、他のエラー内容はほぼ一致してます。

{
 "error": {
  "errors": [
   {
    "domain": "サービスの識別名",
    "reason": "エラー原因の区分",
    "message": "エラーの説明"
   }
  ],
  "code": "HTTPステータス",
  "status": "gRPCのステータス",
  "message": "エラーの説明"
 }
}

複数のサービスを経由した場合

(ユーザのリクエスト) <-> サービスA <-> サービスB <-> サービスC という経路でリクエストがあった場合に、サービスBでエラーが発生したためにエラーを返すケースを考えてみます。

code、status、messageはほぼ同じですが、errrorsが異なります。
errorsはstack traceのような扱いで、サービスをまたいでエラーを追いかけられるようにしてみました。

{
 "error": {
  "errors": [
   {
    "domain": "サービスAの識別名",
    "reason": "サービスAのエラー原因の区分",
    "message": "サービスAのエラーの説明(サービスBでエラーが発生したためにサービスAで発生したエラー)"
   },
   {
    "domain": "サービスBの識別名",
    "reason": "サービスBのエラー原因の区分",
    "message": "サービスBのエラーの説明"
   }
  ],
  "code": "最終的にユーザに返却するHTTPステータス",
  "status": "最終的にユーザに返却するgRPCのステータス",
  "message": "サービスAのエラーの説明"
 }
}

エラーレスポンスをエラーとして扱う

上記のエラーレスポンスの受け渡しだけだと、このレスポンスを格納した変数やstructureをどこかに保存しておいて、受け渡しのたびにこれを使うことになります。
Goなどの言語ではエラー用のメソッドをはやしてあげることで、エラー扱いができます。
これによってエラーレスポンスをエラーとして引き回して最終的にJSONレスポンスとしてユーザに返却するだけで済みます。

実装

共通処理

Struct

メイン部分の構造体はこのようになります。
jsonレスポンスの一番外にあるerrorは普段あると不便なので、外しています。
こちらに関してはjsonのところで説明します。

ポイントとしてはCause.messageが、error型になっていることです。
ただのstringだと文字列で書かれている情報のみになってしまいますが、errorだと文字列以外の情報(エラーの発生場所など)も持てるのでこちらのほうが利点があります。

それに対してDetail.Messageは通常のstringです。
これは、サービス感で情報の受け渡しがあったときに、ここをerrorにしていたとしても文字列以外の情報はjsonレスポンスにした際に欠損してしまうので、errorにしていたところであまり利点がなく、逆に扱いづらくなってしまうためです。

// Cause ...
type Cause struct {
    code    int
    status  string
    message error
    details []Detail
}

// Detail ...
type Detail struct {
    Domain  string
    Reason  string
    Message string
}

エラー処理

先程の構造体をエラーとして取り扱えるようにしてあげます。
Goの場合はダックタイピングで func Error() string をはやしてあげるとエラー扱いになるので、それを用いてエラーにします。
Go 1.13からfmt.Errorf%wを使うことでエラーをWrapできるようになったのでこの機能を用いてエラー原因とメッセージを結合して出力してあげるようにしています

// Error return error message
func (c *Cause) Error() string {
    reason := ""
    if len(c.details) > 0 {
        reason = c.details[0].Reason
    }
    return fmt.Errorf("%s : %w", reason, c.message).Error()
}

ただそれだけだと扱いづらいので、Goの1.13から標準実装されたUnwrapも使用してmessageをそのまま出力してあげるものも作りました

// Unwrap implements errors.Unwrap method
func (c *Cause) Unwrap() error {
    return c.message
}

json

jsonレスポンスの受け渡しの説明です。

まずはjsonレスポンス用の構造体を作ってあげます。
外枠をjsonResponseで定義して、Causeに当たる部分をjsonResponseElement
Causeをそのまま使っていないのは、errorとかjsonで扱いづらいところがあるためで、jsonの受け渡し専用なので、扱いやすいように型などを一部変更しています。
最初Marshal/Unmarshal内で無名Structとして作っていたのですが、扱いづらかったので、名前をつけてあげました。

これで、jsonに変換したときに、コンセプトのときの形ができあがります。

// jsonResponse ...
type jsonResponse struct {
    Error jsonResponseElement `json:"error"`
}

// jsonResponseElement
type jsonResponseElement struct {
    Code    int      `json:"code"`
    Status  string   `json:"status"`
    Message string   `json:"message"`
    Details []Detail `json:"details"`
}

Marshalerの処理はとてもかんたんです。
Causeの内容をjsonResonseに置き換えてからjson.Marshalをかけてあげるだけです。

// MarshalJSON implements the json.Marshaler interface
func (c *Cause) MarshalJSON() ([]byte, error) {
    return json.Marshal(jsonResponse{
        Error: jsonResponseElement{
            Code:    c.code,
            Status:  c.status,
            Message: c.message.Error(),
            Details: c.details,
        },
    })
}

Unmarshalerはまず、レスポンスを格納した、jsonResponseを*Causeに変換してあげます。
その後、Appendでエラーをくっつけてあげます。
Appendは*Cause.detailsを参照して、呼び出し元のdetailsにappendするように作っています。

// UnmarshalJSON implements the json.Unmarshaler interface
func (c *Cause) UnmarshalJSON(b []byte) (err error) {
    var je jsonResponse
    if err = json.Unmarshal(b, &je); err != nil {
        return
    }
    ce := &Cause{
        code:    je.Error.Code,
        status:  je.Error.Status,
        message: errors.New(je.Error.Message),
        details: je.Error.Details,
    }
    c.Append(ce)
    return nil
}

一般的にAppendはjsonレスポンス経由で使われますが、手動でエラーをくっつけてあげることもできます。
その場合、エラーは必ずしも*Causeであるとは限りません。
なので、その場合は、デフォルトでBackend Error (Internal Server Error)にerrorを格納したDetailを生成してからdetailsにappendするようにしました。

この場合、引数となるerrorはdomainを知りません。
そのため、ServiceDomainというグローバル変数を用意しています。
これによってDomainを補完して設定することができます。

// Append one or more elements onto the end of details
func (c *Cause) Append(e error) {
    if c == nil || c.IsZero() {
        c.set(e)
        return
    }

    if v, ok := e.(*Cause); ok {
        c.details = append(c.details, v.details...)
        return
    }
    c.details = append(c.details, Detail{
        Domain:  ServiceDomain,
        Reason:  StatusBackendError,
        Message: e.Error(),
    })
}

Causeエラーの生成

Causeエラーを新規作成する処理を考えてみます。

func New(err error, domain string, code int, status string, reason string) error {
    return &Cause{
        code:    code,
        status:  status,
        message: err,
        details: []Detail{
            {
                Domain:  domain,
                Reason:  reason,
                Message: err.Error(),
            },
        },
    }
}

これだと、生成はできるものの引数が多いので扱いづらいです。
codestatusreasonはグルーピングできそうなので、グルーピングするstructを作ってあげました。
外部のパッケージでカスタマイズできるようにpublicにしています。

// ErrCase ...
type ErrCase struct {
    Code   int
    Status string
    Reason string
} 

// NewCause ...
func NewCause(err error, domain string, c ErrCase) error {
    return NewCauseWithStatus(err, domain, c.Code, c.Status, c.Reason)
}

// NewCauseWithStatus ...
func NewCauseWithStatus(err error, domain string, code int, status string, reason string) error {
    return &Cause{
        code:    code,
        status:  status,
        message: err,
        details: []Detail{
            {
                Domain:  domain,
                Reason:  reason,
                Message: err.Error(),
            },
        },
    }
}

全体像

package errors

import (
    "encoding/json"
    "errors"
    "fmt"
    "net/http"
)

// Status
const (
    // ServiceDomainGlobal ...
    ServiceDomainGlobal = "global"

    // Status

    // StatusUnauthenticated ...
    StatusUnauthenticated = "UNAUTHENTICATED"
    // StatusPermissionDenied ...
    StatusPermissionDenied = "PERMISSION_DENIED"
    // StatusNotFound ...
    StatusNotFound = "NOT_FOUND"
    // StatusAborted ...
    StatusAborted = "ABORTED"
    // StatusAlreadyExists ...
    StatusAlreadyExists = "ALREADY_EXISTS"
    // StatusResourceExhausted ...
    StatusResourceExhausted = "RESOURCE_EXHAUSTED"
    // StatusUnavailable ...
    StatusUnavailable = "UNAVAILABLE"
    // StatusBackendError ...
    StatusBackendError = "INTERNAL"

    // Reason

    // ReasonUnauthenticated ...
    ReasonUnauthenticated = "unauthenticated"
    // ReasonPermissionDenied ...
    ReasonPermissionDenied = "permissionDenied"
    // ReasonNotFound ...
    ReasonNotFound = "notFound"
    // ReasonAborted ...
    ReasonAborted = "abourtedRequest"
    // ReasonAlreadyExists ...
    ReasonAlreadyExists = "alreadyExists"
    // ReasonResourceExhausted ...
    ReasonResourceExhausted = "userRateLimitExceeded"
    // ReasonUnavailable ...
    ReasonUnavailable = "unavailable"
    // ReasonBackendError ...
    ReasonBackendError = "backendError"
)

var (
    // ServiceDomain is default service domain name
    ServiceDomain = ServiceDomainGlobal

    // CaseUnauthenticated ...
    CaseUnauthenticated = ErrCase{
        Code:   http.StatusUnauthorized,
        Status: StatusUnauthenticated,
        Reason: ReasonUnauthenticated,
    }

    // CasePermissionDenied ...
    CasePermissionDenied = ErrCase{
        Code:   http.StatusForbidden,
        Status: StatusPermissionDenied,
        Reason: ReasonPermissionDenied,
    }

    // CaseNotFound ...
    CaseNotFound = ErrCase{
        Code:   http.StatusNotFound,
        Status: StatusNotFound,
        Reason: ReasonNotFound,
    }

    // CaseAborted ...
    CaseAborted = ErrCase{
        Code:   http.StatusConflict,
        Status: StatusAborted,
        Reason: ReasonAborted,
    }

    // CaseAlreadyExists ...
    CaseAlreadyExists = ErrCase{
        Code:   http.StatusConflict,
        Status: StatusBackendError,
        Reason: ReasonBackendError,
    }

    // CaseResourceExhausted ...
    CaseResourceExhausted = ErrCase{
        Code:   http.StatusTooManyRequests,
        Status: StatusResourceExhausted,
        Reason: ReasonResourceExhausted,
    }

    // CaseUnavailable ...
    CaseUnavailable = ErrCase{
        Code:   http.StatusServiceUnavailable,
        Status: StatusUnavailable,
        Reason: ReasonUnavailable,
    }

    // CaseBackendError ...
    CaseBackendError = ErrCase{
        Code:   http.StatusInternalServerError,
        Status: StatusBackendError,
        Reason: ReasonBackendError,
    }
)

// Cause ...
type Cause struct {
    code    int
    status  string
    message error
    details []Detail
}

// Detail ...
type Detail struct {
    Domain  string
    Reason  string
    Message string
}

// ErrCase ...
type ErrCase struct {
    Code   int
    Status string
    Reason string
}

// jsonResponse ...
type jsonResponse struct {
    Error jsonResponseElement `json:"error"`
}

// jsonResponseElement
type jsonResponseElement struct {
    Code    int      `json:"code"`
    Status  string   `json:"status"`
    Message string   `json:"message"`
    Details []Detail `json:"details"`
}

// NewCause ...
func NewCause(err error, domain string, c ErrCase) error {
    return NewCauseWithStatus(err, domain, c.Code, c.Status, c.Reason)
}

// NewCauseWithStatus ...
func NewCauseWithStatus(err error, domain string, code int, status string, reason string) error {
    return &Cause{
        code:    code,
        status:  status,
        message: err,
        details: []Detail{
            {
                Domain:  domain,
                Reason:  reason,
                Message: err.Error(),
            },
        },
    }
}

// Error return error message
func (c *Cause) Error() string {
    reason := ""
    if len(c.details) > 0 {
        reason = c.details[0].Reason
    }
    return fmt.Errorf("%s : %w", reason, c.message).Error()
}

// Unwrap implements errors.Unwrap method
func (c *Cause) Unwrap() error {
    return c.message
}

// Append one or more elements onto the end of details
func (c *Cause) Append(e error) {
    if c == nil || c.IsZero() {
        c.set(e)
        return
    }

    if v, ok := e.(*Cause); ok {
        c.details = append(c.details, v.details...)
        return
    }
    c.details = append(c.details, Detail{
        Domain:  ServiceDomain,
        Reason:  StatusBackendError,
        Message: e.Error(),
    })
}

// MarshalJSON implements the json.Marshaler interface
func (c *Cause) MarshalJSON() ([]byte, error) {
    return json.Marshal(jsonResponse{
        Error: jsonResponseElement{
            Code:    c.code,
            Status:  c.status,
            Message: c.message.Error(),
            Details: c.details,
        },
    })
}

// UnmarshalJSON implements the json.Unmarshaler interface
func (c *Cause) UnmarshalJSON(b []byte) (err error) {
    var je jsonResponse
    if err = json.Unmarshal(b, &je); err != nil {
        return
    }
    ce := &Cause{
        code:    je.Error.Code,
        status:  je.Error.Status,
        message: errors.New(je.Error.Message),
        details: je.Error.Details,
    }
    c.Append(ce)
    return nil
}

// IsZero checks empty
func (c *Cause) IsZero() bool {
    return (c.code == 0 || c.status == "" || c.message == nil || len(c.details) == 0)
}

// set overwrites error
func (c *Cause) set(e error) {
    v, ok := e.(*Cause)
    if !ok {
        v = NewCause(e, ServiceDomain, CaseBackendError).(*Cause)
    }

    // c = v <== fail staticcheck. SA4006: this value of `c` is never used
    c.code = v.code
    c.status = v.status
    c.message = v.message
    c.details = v.details
}

各サービスごとの処理

今までは共通処理としてのエラーを見てきました。各サービスでは、ServiceDomainなどは固定できそうなので、そういった箇所をWrapしたエラーを用意します

const (
    // ServiceDomain ...
    ServiceDomain = "foobar"
)

func init() {
    errors.ServiceDomain = ServiceDomain
}

// NewCause ...
func NewCause(err error, c errors.ErrCase) error {
    return errors.NewCause(err, ServiceDomain, c)
}

// NewCauseWithStatus ...
func NewCauseWithStatus(err error, code int, status string, reason string) error {
    return errors.NewCauseWithStatus(err, ServiceDomain, code, status, reason)
}

後書き

土日に思いつきで作ったものなので、まだまだ改良の余地はありそうですが、よくあるCodeとメッセージだけのエラーなどよりは置いやすいのではないでしょうか?

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

【Golang】ゴルーチン④range,close

【Golang】ゴルーチン④range,close

Golangの基礎学習〜Webアプリケーション作成までの学習を終えたので、復習を兼ねてまとめていく。 基礎〜応用まで。

package main

//channelのrangeとclose
//結果を随時出力

import (
    "fmt"
)

func goroutin2(s []int, c chan int) {
    sum := 0
    for _, v := range s {
        //全部足す
        sum += v
        //すぐに送る
        c <- sum
    }
    //終わったらchannelを閉じる
    //これ以上ない時は閉じる
    //閉じないと、中身以上取り出そうとしエラーになる
    close(c)
}

func main() {
    s := []int{1, 2, 3, 4, 5, 6, 7, 8, 9, 10}
    //実質1つしか入らないので、バッファは1でもいい
    c := make(chan int, len(s))

    goroutin2(s, c)

    //go goroutine1(s, c)の結果をforで回す
    //channelの場合、インデックスは無く、値を出力
    //並列処理なので、値が入る度に出力される
    //回数分i変数に入力
    for i := range c {
        fmt.Println(i)
    }

    //中は0になる
    fmt.Println(len(c))

    /*
        1
        3
        6
        10
        15
        21
        28
        36
        45
        55
        0
    */
}

/*
別パターンが本にはある。スターティングGo言語 P191~P192
*/

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

【GCP】Cloud Functions の Go 1.13 でファイル構造に変更があった

Cloud Functions Go 1.13 GA :tada:

GCF/Go 1.13 が GA になったので Go 1.11 から移行してみた。しかし、デプロイに失敗するようになった。

https://cloud.google.com/functions/docs/release-notes#May_28_2020

原因

コード内で設定ファイルを読み込むようにしていたが、GCF/Go 1.13 からファイルの構造が変更され読み込みエラーが起きてしまった。

調査

どのようにファイルの構造が変更があったのか調べたところ、ファイルの構造を調査したリポジトリを発見。

https://github.com/bcollard/find-go-cloud-function

どうやら、静的ファイル保存ディレクトリは以下に保存されるようだ。
/workspace/serverless_function_source_code/

ファイル構造について、公式ドキュメントでは見つけることはできなかった。

対応

コードの静的ファイルを指定している箇所を下記のように変更した。

- ./xxx.json
+ /workspace/serverless_function_source_code/xxx.json

無事、デプロイに成功した。

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

【Heroku】デプロイ後にcode=H14 desc="No web processes running"が発生する場合はProcfileを作成して対処すればOK

発生したエラー

2020-06-02T00:41:22.345388+00:00 heroku[router]:
at=error code=H14 desc="No web processes running"
method=POST path="/xxxx" host=xxxx.herokuapp.com request_id=xxxx
fwd="xx.xx.xx.xx" dyno= connect= service= status=503 bytes= protocol=https

私の場合

Go/echoで開発しており、メインのアプリの他にアプリケーションのディレクトリ内にmain関数を持つgoファイルを作成してデプロイしたら発生しました。
main関数が2つになり、Herokuがどちらを実行すべきが判定できなくなったんですかね。(適当)

対処方法1

公式ドキュメントに基づき、アプリケーションのルートディレクトリで、以下のコマンドを実行する

$ heroku ps:scale web=1

対処方法2

1でダメな場合は、アプリケーションのルートディレクトリで、以下のようにProcfileを作成する

$ cd `your-app-dir`
$ echo "web: $(basename `pwd`)" > Procfile

補足:Procfileとは

【公式】The Procfile

Heroku apps include a Procfile that specifies the commands that are executed by the app on startup. You can use a Procfile to declare a variety of process types, including:

参考

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

Goの組込みデータ型を整理した

コードらしいコードは書いてない...

概略

  • 論理型
    • bool
  • 文字列型
    • string
  • 整数型
    • 符号付き整数型
    • int
    • int8
    • int16
    • int32 ( = rune)
    • int64
    • 符号なし整数型
    • uint
    • uint8 ( = byte)
    • uint16
    • uint32
    • uint64
    • uintptr
  • 浮動小数点型
    • float32
    • float64
  • 複素数型

    • complex64
    • complex128
  • エラー型

    • Error

論理型

bool

trueまたはfalseしか取りうる値が存在しない最も単純なデータ型

文字列型

string

  • 8bit/byteの文字列型
  • UTF-8 encodedではない場合もある
  • 空文字でもnilでもよい
  • immutableである

整数型

符号付き整数型

int

最低32bit以上の符号付き整数型だが、int32型のエイリアスではない。

int8

8bitの符号付き整数型で、-128~127の範囲の整数を表現できる。

int16

16bitの符号付き整数型で、-32768~32767の範囲の整数を表現できる。

int32 ( = rune)

32bitの符号付き整数型で、-2147483648~2147483647の範囲の整数を表現できる。

runeint32型のエイリアスで、これについてはThe Go Blogに記述がある。

To summarize, here are the salient points:

  • Go source code is always UTF-8.
  • A string holds arbitrary bytes.
  • A string literal, absent byte-level escapes, always holds valid UTF-8 sequences.
  • Those sequences represent Unicode code points, called runes.
  • No guarantee is made in Go that characters in strings are normalized.

和訳した。

要約すると、以下のようになります。

  • Goのソースコードは常にUTF-8です。
  • 文字列は任意のバイトを保持します。
  • 文字列リテラルは、バイトレベルのエスケープを除いて、常に有効な UTF-8 シーケンスを保持します。
  • これらのシーケンスは、runeと呼ばれる Unicode コードポイントを表しています。
  • Goでは、文字列内の文字が正規化されていることを保証するものではありません。

まだちょっとわからないので調べると、こんな記述を見つけた。(出典:String と Rune — プログラミング言語 Go | text.Baldanders.infoにて)

文字列を文字単位で扱うには rune 型を使う2

この記事も見ると良さそう。

int64

64bitの符号付き整数型で、-9223372036854775808~9223372036854775807の範囲の整数を表現できる。

符号なし整数型

uint

最低32bit以上の符号なし整数型だが、uint32型のエイリアスではない。

uint8 ( = byte)

8bitの符号なし整数型で、0~255の範囲の整数を表現できる。

byteuint8のエイリアスで、公式Docでは以下のように説明されている。

byte is an alias for uint8 and is equivalent to uint8 in all ways. It is used, by convention, to distinguish byte values from 8-bit unsigned integer values.

byte は uint8 の別名であり、すべての点で uint8 と同等です。慣習的に、バイト値を8ビットの符号なし整数値と区別するために使用されます。

uint16

16bitの符号なし整数型で、0~65535の範囲の整数を表現できる。

uint32

32bitの符号なし整数型で、0~4294967295の範囲の整数を表現できる。

uint64

64bitの符号なし整数型で、0~18446744073709551615の範囲の整数を表現できる。

uintptr

ドキュメントを直訳すると「uintptr は、任意のポインタのビットパターンを保持するのに十分な大きさの整数型です。」となる(DeepL様様)。

どんな場面で使うんだ...と思ってたが、StackOverflowで触れられていた。

The short answer is "never use uintptr". ?

The long answer is that uintptr is there to bypass the type system and allow the Go implementors to write Go runtime libraries, including the garbage collection system, in Go, and to call C-callable code including system calls using C pointers that are not handled by Go at all.

If you're acting as an implementor—e.g., providing access to system calls on a new OS—you'll need uintptr. You will also need to know all the special magic required to use it, such as locking your goroutine to an OS-level thread if the OS is going to do stack-ish things to OS-level threads, for instance. (If you're using it with Go pointers, you may also need to tell the compiler not to move your goroutine stack, which is done with special compile-time directives.)

どうやら低レイヤーの処理を実装しない限りはめったに使うことがないらしい。

当面は気にしないことにした。

浮動小数点型

IEEE-754式については 以下を参照。

float32

IEEE-754式の32bit浮動小数点型

float64

IEEE-754式の64bit浮動小数点型

ちなみに数学計算するためのmathパッケージで定義された関数のほとんどは引数も返り値もfloat64型でしか定義されていないで、注意が必要。まあGoにはオーバーロードがないので当然っちゃぁ当然。

複素数型

複素数型の扱いを確認するならこの辺の記事を見ると良さそう。

complex64

実部と虚部をfloat32型で表現する複素数型

complex128

実部と虚部をfloat64型で表現する複素数型

エラー型

error

Goにはtry-catch-finallyといったエラーハンドリング表現が存在しないので、このerror型のinterfaceに対してエラー処理を実装する。

type error interface {
    Error() string
}
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Goでよく使われるgormを理解する:目次

はじめに

golangでアプリケーション開発を行なっている方だと、gormを使用されている方も多いのではないでしょうか。
自分もここ最近、golangを使ったWEBアプリケーションの開発を業務で行なっていますが、十分にgormの仕様を理解できておらず、実装で詰まってしまうことがあったので、この辺りで改めてgormのドキュメントと向き合い、Qiitaにまとめていければと思っています。

基本的には、GORMドキュメントに沿って挙動を確認していきますので、ドキュメントも合わせて見ていただけるとよいかと思います。ただし、個人的に確認不要と判断した箇所については割愛していますので、予めご了承ください。

目次

はじめに
はじめに

CRUD Interface
Goでよく使われるgormを理解する:Query編

Associations
Goでよく使われるgormを理解する:Associations編
Goでよく使われるgormを理解する:Preloading編

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

Goでよく使われるgormを理解する:Query編

目次

はじめに
はじめに

CRUD Interface
Goでよく使われるgormを理解する:Query編

Associations
Goでよく使われるgormを理解する:Associations編
Goでよく使われるgormを理解する:Preloading編

Query

Queryを用いることで、データの抽出条件を細かく設定することができます。

Where

gormのドキュメントには、AND検索について以下のような例が載っていますが、

// AND
db.Where("name = ? AND age >= ?", "jinzhu", "22").Find(&users)
//// SELECT * FROM users WHERE name = 'jinzhu' AND age >= 22;

gormの場合、Whereをつなげて記述したときのデフォルトがANDなので、以下のように記述することもできます。

db.Where("name = ?", jinzhu).Where("age >= ?", "22").Find(&users)

例えば、以下のようなstructがあったとします。

type User struct {
    Model
    UserName       *string     `gorm:"" json:"userName"`  // ユーザー名
    CompanyID      *int        `gorm:"" json:"companyId"` // 所属企業ID
    Company        *Company    `gorm:"" json:"company"`
    Languages      []*Language `gorm:"many2many:user_languages;association_autoupdate:false" json:"language"` // 使用可能言語
    UserBirthday   *time.Time  `gorm:"" json:"userBirthday"`                                                  // 生年月日
    Bio            *string     `gorm:"" json:"bio"`                                                           // 自己紹介文
    GithubAccount  *string     `gorm:"" json:"githubAccount"`                                                 // Githubアカウント
    TwitterAccount *string     `gorm:"" json:"twitterAccount"`                                                // Twitterアカウント
}

ここで、DBに誕生日(UserBirthday)の登録があり(NULLではなく)、所属企業のID(company_id)が1のユーザーを探す場合、以下のように記述することができます。

func GetAllUsers() (ml []*User, err error) {
    tx := db.Begin()

    tx = tx.Where("user_birthday is not null").Where("company_id = ?", 1)
    err = tx.Find(&ml).Commit().Error

    return ml, err
}
ログ
SELECT * FROM `users`  WHERE `users`.`deleted_at` IS NULL AND ((user_birthday is not null) AND (company_id = ?))[1] 1

gormのドキュメントに記載されている書き方だと、以下のようになりますが、ログを見ても基本的には同じSQLが発行されているのがわかります。
※前述の書き方だと、「user_birthday is not null」と「company_id = ?」がそれぞれ括弧で囲われており、以下の書き方だとその括弧がなくなっているという違いはありますが、括弧は処理のまとまりを表しているだけなので、おそらくパフォーマンス等への影響はないと思います。

func GetAllUsers(limit int64, offset int64) (ml []*User, err error) {
    tx := db.Begin()

    tx = tx.Where("user_birthday is not null AND company_id = ?", 1)
    err = tx.Find(&ml).Commit().Error

    return ml, err
}
ログ
SELECT * FROM `users`  WHERE `users`.`deleted_at` IS NULL AND ((user_birthday is not null AND company_id = ?))[1] 1

FirstOrInit

最初にマッチしたレコードを得る、または渡された条件 (構造体、マップの場合のみ機能) を元にレコードを作成します

例えば、以下のようなstructがあり、

type User struct {
    Model
    UserName       *string     `gorm:"" json:"userName"`  // ユーザー名
}

Firstを使用して、DBに登録されていないIDを取得しようとすると、

func GetUser(UserID int64) (user User, err error) {
    err = db.First(&user, UserID).Error
    return user, err
}

該当するレコードがないため、

ログ
SELECT * FROM `users`  WHERE `users`.`deleted_at` IS NULL AND ((`users`.`id` = 6)) ORDER BY `users`.`id` ASC LIMIT 1[] 0

通常は、以下のようなエラーが返ってきます。

出力
// => record not found

しかし、FirstOrInit()を使用すると、

func GetUser(UserID int64) (user User, err error) {
    err = db.FirstOrInit(&user, UserID).Error
    return user, err
}

該当するレコードがなくても、

SELECT * FROM `users`  WHERE `users`.`deleted_at` IS NULL AND ((`users`.`id` = 6)) ORDER BY `users`.`id` ASC LIMIT 1[] 0

設定された内容を元に、仮のレコードを返してくれるみたいです。
※「設定された内容」と書きましたが、今回は設定をしていないので項目はすべてゼロ値になっています。「設定」については、次項のAttrsで行うことができます。

{
  "id": 0,
  "createdAt": "0001-01-01T00:00:00Z",
  "updatedAt": "0001-01-01T00:00:00Z",
  "deletedAt": null,
  "userName": null,
  "companyId": null,
  "company": null,
  "language": null,
  "userBirthday": null,
  "bio": null,
  "githubAccount": null,
  "twitterAccount": null
}

なお、gormのドキュメントには、「レコードが作成されます」と書いてありましたが、DBを確認したところレコード自体は作成されていませんでした。

Attrs

レコードが取得できたかどうかによって初期化するかどうかが決まります

先ほどと同様、以下のようなstructがあったとします。

type User struct {
    Model
    UserName       *string     `gorm:"" json:"userName"`  // ユーザー名
}

前項で使用したFirstOrInit()にAttrsの記述を追加して、以下のように書くと、

func GetUser(UserID int64) (user User, err error) {
    userName := "No Name"
    err = db.Attrs(User{UserName: &userName}).FirstOrInit(&user, UserID).Error
    return user, err
}

該当するレコードがある場合は、DBのレコードを返しますが、

出力
{
  "id": 1,
  "createdAt": "2020-06-01T20:32:19+09:00",
  "updatedAt": "2020-06-01T20:32:19+09:00",
  "deletedAt": null,
  "userName": "user1"
}
ログ
SELECT * FROM `users`  WHERE `users`.`deleted_at` IS NULL AND ((`users`.`id` = 1)) ORDER BY `users`.`id` ASC LIMIT 1[] 1

該当するレコードがない場合は、Attrsで設定した内容をvalueに含んだ仮のレコードを返します。

出力
{
  "id": 0,
  "createdAt": "0001-01-01T00:00:00Z",
  "updatedAt": "0001-01-01T00:00:00Z",
  "deletedAt": null,
  "userName": "No Name"
}
ログ
SELECT * FROM `users`  WHERE `users`.`deleted_at` IS NULL AND ((`users`.`id` = 6)) ORDER BY `users`.`id` ASC LIMIT 1[] 0

FirstOrCreate

最初にマッチしたレコードを得る、または渡された条件 (構造体、マップの場合のみ機能) を元にレコードを作成します。

gormのドキュメントの説明だけ見ると、FirstOrInitと何が違うの!?と思いますが(笑)、こちらは該当するレコードがない場合、正真正銘DBにレコードが作成されます。

func GetUser(UserID int64) (user User, err error) {
    userName := "No Name"
    err = db.Attrs(User{UserName: &userName}).FirstOrCreate(&user, UserID).Error
    return user, err
}
出力
{
  "id": 6,
  "createdAt": "2020-06-01T23:19:25.2557082+09:00",
  "updatedAt": "2020-06-01T23:19:25.2557082+09:00",
  "deletedAt": null,
  "userName": "No Name",
  "companyId": null,
  "company": null,
  "language": null,
  "userBirthday": null,
  "bio": null,
  "githubAccount": null,
  "twitterAccount": null
}

ログを見ると、たしかに、先ほどまでと同様のSELECT文に加え、INSERT文でAttrsで設定した内容を含むレコードが作成されていることがわかります。

ログ
SELECT * FROM `users`  WHERE `users`.`deleted_at` IS NULL AND ((`users`.`id` = 6)) ORDER BY `users`.`id` ASC LIMIT 1[] 0
INSERT INTO `users` (`created_at`,`updated_at`,`deleted_at`,`user_name`,`company_id`,`user_birthday`,`bio`,`github_account`,`twitter_account`) VALUES (?,?,?,?,?,?,?,?,?)[2020-06-01 23:19:25.2557082 +0900 JST m=+16.576992801 2020-06-01 23:19:25.2557082 +0900 JST m=+16.576992801 <nil> 0xc0000204d0 <nil> <nil> <nil> <nil> <nil>] 1

高度なクエリ

SubQuery

SubQueryを使用することで、より複雑なデータの抽出を行うことができるようになります。
例えば、以下のようなstructがあり、Activity側からTourのIDに応じたActivityを抽出したいとします。

type Activity struct {
    Model
    ActivityName *string `gorm:"" json:"activityName"`
}
type ActivityPlan struct {
    Model
    ActivityPlanName *string `gorm:"" json:"activityPlanName"`
    TourScheduleID   *int    `gorm:"" json:"tourScheduleId"`
    ActivityID       *int      `gorm:"" json:"activityId"`
    Activity         *Activity `gorm:"" json:"activity"`
}
type TourSchedule struct {
    Model
    TourScheduleDate *time.Time      `gorm:"" json:"tourScheduleDate"`
    TourID           *int            `gorm:"" json:"tourId"`
    ActivityPlans    []*ActivityPlan `gorm:"" json:"activityPlan"`
}
type Tour struct {
    Model
    TourName      *string         `gorm:"" json:"tourName"`
    TourSchedules []*TourSchedule `gorm:"" json:"tourSchedule"`
}

先ほどまでのものに比べると、抽出の仕方が少々複雑な気がしますね。
TourScheduleはTourと一対多の関係にあり、ActivityPlanはTourScheduleと一対多の関係にあります。また、今回Getのfunctionを記述したいActivityのstructは直接TourIDを持っておらず、TourScheduleのstructにあるTourIDをうまく条件に組み込む必要がありそうです。

例えば、以下のような形はどうでしょう。

func GetAllActivities(tourID int64) (ml []*Activity, err error) {
    tx := db.Begin()

    // ツアーIDに紐づくActivityを抽出
    tx = tx.Where("id in (?)",
        tx.Table("activity_plans").
            Select("distinct(activity_id)").
            Joins("left join tour_schedules on activity_plans.tour_schedule_id = tour_schedules.id").
            Where("tour_schedules.tour_id = ?", tourID).
            SubQuery())

    err = tx.Find(&ml).Commit().Error

    return ml, err
}

すると、以下のようなデータが出力されました。

出力
[
  {
    "id": 7,
    "createdAt": "2020-06-01T23:38:47+09:00",
    "updatedAt": "2020-06-01T23:38:47+09:00",
    "deletedAt": null,
    "activityName": "アクティビティG"
  },
  {
    "id": 8,
    "createdAt": "2020-06-01T23:38:47+09:00",
    "updatedAt": "2020-06-01T23:38:47+09:00",
    "deletedAt": null,
    "activityName": "アクティビティH"
  },
  {
    "id": 9,
    "createdAt": "2020-06-01T23:38:47+09:00",
    "updatedAt": "2020-06-01T23:38:47+09:00",
    "deletedAt": null,
    "activityName": "アクティビティI"
  },
  {
    "id": 10,
    "createdAt": "2020-06-01T23:38:47+09:00",
    "updatedAt": "2020-06-01T23:38:47+09:00",
    "deletedAt": null,
    "activityName": "アクティビティJ"
  }
]

ログを見ると、activity_plansテーブルとtour_schedulesテーブルをleft joinし、tour_schedulesテーブルのtour_idが1になるレコードを探してきていますね。また、重複しない(distinct)ようにactivity_plansテーブルのactivity_idを抽出し、そのactivity_id郡と一致するactivitiesのレコードを抽出できているので、目的通りの抽出ができていそうです。
このように、SubQueryを使用することで、複雑な抽出条件の設定が可能になります。

ログ
SELECT * FROM `activities`  WHERE `activities`.`deleted_at` IS NULL AND ((id in ((SELECT distinct(activity_id) FROM `activity_plans` left join tour_schedules on activity_plans.tour_schedule_id = tour_schedules.id WHERE (tour_schedules.tour_id = ?)))))[1] 4
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む