20200212のGoに関する記事は9件です。

Go の gRPC で Redis のデータを削除 (Delete)

設定ファイル、サーバープログラム、クライアントプログラムの3つが必要です。

$ tree
.
├── redis_delete
│   └── redis_delete.proto
├── redis_delete_client
│   └── main.go
└── redis_delete_server
    └── main.go

設定ファイル

redis_delete/redis_delete.proto
こちらと同じ
Python の gRPC で Redis のデータを削除 (Delete)

サーバープログラム

redis_delete_server/main.go
// ---------------------------------------------------------------
//
//  redis_delete_server/main.go
//
//                  Feb/11/2020 
// ---------------------------------------------------------------
package main

import (
    "context"
    "log"
    "net"
    "fmt"
    "os"

    "google.golang.org/grpc"
    pb "../redis_delete"
)

const (
    port = ":50051"
)

type server struct {
    pb.UnimplementedGreeterServer
}

// ---------------------------------------------------------------
func redis_delete_proc (key_in string) string {
    rvalue := key_in

    hostname := "localhost"
    port := "6379"

    conn, err := net.Dial ("tcp", hostname + ":" + port)
    if err != nil {
        fmt.Println(err)
        return rvalue
        }

    _, err = conn.Write ([]byte("del " + key_in + "\r\n"))
    if err != nil {
        fmt.Println(err)
        return rvalue
        }

    conn.Close ()

    return rvalue
}
// ---------------------------------------------------------------
func (s *server) RedisDelete(ctx context.Context, in *pb.RedisRequest) (*pb.RedisReply, error) {
    fmt.Fprintf (os.Stderr,"*** check aaa ***\n")
    key := in.GetKey()
    fmt.Fprintf (os.Stderr,"key = " + key + "\n")
    rvalue := redis_delete_proc (key)
    return &pb.RedisReply{Key: rvalue }, nil
}

// ---------------------------------------------------------------
func main() {
    lis, err := net.Listen("tcp", port)
    if err != nil {
        log.Fatalf("failed to listen: %v", err)
    }
    s := grpc.NewServer()
    pb.RegisterGreeterServer(s, &server{})
    if err := s.Serve(lis); err != nil {
        log.Fatalf("failed to serve: %v", err)
    }
}

// ---------------------------------------------------------------

クライアントプログラム

redis_delete_client/main.go
// ---------------------------------------------------------------
//
//  redis_delete_client/main.go
//
//                  Feb/11/2020
//
// ---------------------------------------------------------------
package main

import (
    "context"
    "log"
    "os"
    "time"

    "google.golang.org/grpc"
    pb "../redis_delete"
)

const (
    address     = "localhost:50051"
    defaultKey = "t0001"
)

// ---------------------------------------------------------------
func main() {
    // Set up a connection to the server.
    conn, err := grpc.Dial(address, grpc.WithInsecure(), grpc.WithBlock())
    if err != nil {
        log.Fatalf("did not connect: %v", err)
    }
    defer conn.Close()
    c := pb.NewGreeterClient(conn)

    // Contact the server and print out its response.
    key := defaultKey
    if len(os.Args) > 1 {
        key = os.Args[1]
    }
    ctx, cancel := context.WithTimeout(context.Background(), time.Second)
    defer cancel()
    r, err := c.RedisDelete(ctx, &pb.RedisRequest{Key: key})
    if err != nil {
        log.Fatalf("could not greet: %v", err)
    }
    rvalue := r.GetKey()
    log.Printf("Rvalue: %s", rvalue)

}

// ---------------------------------------------------------------

gRPC のコードを作成します。

スクリプト

protoc -I redis_delete redis_delete/redis_delete.proto --go_out=plugins=grpc:redis_delete

サーバープログラムの起動

go run redis_delete_server/main.go

クライアントプログラムの実行

$ go run redis_delete_client/main.go t1852
2020/02/12 13:33:08 Rvalue: t1852
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Go の gRPC で Redis のデータを更新 (Update)

設定ファイル、サーバープログラム、クライアントプログラムの3つが必要です。

$ tree
.
├── redis_update
│   └── redis_update.proto
├── redis_update_client
│   └── main.go
└── redis_update_server
    └── main.go

設定ファイル

redis_update/redis_update.proto
こちらと同じ
Python の gRPC で Redis のデータを更新 (Update)

サーバープログラム

redis_update_server/main.go
// ---------------------------------------------------------------
//
//  redis_update_server/main.go
//
//                  Feb/11/2020 
// ---------------------------------------------------------------
package main

import (
    "context"
    "log"
    "net"
    "fmt"
    "os"
    "encoding/json"
    "time"
    "strconv"
    "strings"

    "google.golang.org/grpc"
    pb "../redis_update"
)

const (
    port = ":50051"
)

type server struct {
    pb.UnimplementedGreeterServer
}

// ---------------------------------------------------------------
func get_current_date_proc () string {
    now := time.Now ()
    fmt.Printf ("%s\t" ,now)
    fmt.Printf ("%d-%d-%d\n" ,now.Year (),now.Month(),now.Day())
    today := strconv.Itoa (now.Year()) + "-" +
        fmt.Sprintf ("%d",now.Month()) + "-" +
        strconv.Itoa (now.Day())

    return  today
}

// ---------------------------------------------------------------
func json_update_proc (json_str string,population_in int) string {

    var unit_aa map[string]interface{}
    json.Unmarshal ([]byte(json_str), &unit_aa)
    fmt.Printf ("%s\t",unit_aa["name"])
    fmt.Printf ("%f\t",unit_aa["population"])
    fmt.Printf ("%s\n",unit_aa["date_mod"])

    unit_aa["population"] = population_in
    unit_aa["date_mod"] = get_current_date_proc ()

    output, _ := json.Marshal(unit_aa)

    json_str = string(output)

    return  json_str
}

// ---------------------------------------------------------------
func redis_socket_write_proc (conn net.Conn,key_in string,json_str string) {
    fmt.Println (key_in)
    fmt.Println (json_str)

    comm_aa := "set " + key_in + " '" + json_str + "'\r\n"
    conn.Write([]byte(comm_aa))


    buf := make ([]byte,1024)
    conn.Read (buf[:])

    fmt.Println (string(buf[0:10]))
}

// ---------------------------------------------------------------
func socket_read_proc (conn net.Conn,key_in string) string {
    str_received := ""
    _, err := conn.Write([]byte("get " + key_in + "\r\n"))
    if err != nil {
        fmt.Println(err)
        return str_received
        }

    buf := make([]byte, 1024)
    nn, err := conn.Read(buf[:])
    if err != nil {
        fmt.Println(err)
        return str_received
        }

    str_received = string(buf[0:nn])

    return  str_received
}

// ---------------------------------------------------------------
func redis_socket_read_proc (conn net.Conn,key_in string) string {
    json_str := ""
    str_received := socket_read_proc (conn,key_in)

    lines := strings.Split(str_received,"\n")

    if (! strings.Contains(lines[0],"END")) {
        json_str = lines[1]
        }

    return  json_str
}

// ---------------------------------------------------------------
func redis_update_proc (key_in string,population int32) string {
    str_json := ""

    hostname := "localhost"
    port := "6379"

    conn, err := net.Dial ("tcp", hostname + ":" + port)
    if err != nil {
        fmt.Println(err)
        return str_json
        }

    str_json = redis_socket_read_proc (conn,key_in)

    fmt.Fprintf (os.Stderr,"str_json = " + str_json + "\n")
    str_json_new := json_update_proc (str_json,int(population))
    fmt.Println (str_json_new)
    redis_socket_write_proc (conn,key_in,str_json_new)

    return key_in
}
// ---------------------------------------------------------------
func (s *server) RedisUpdate(ctx context.Context, in *pb.RedisRequest) (*pb.RedisReply, error) {
//  log.Printf("SayHello Received: %v", in.GetName())
    fmt.Fprintf (os.Stderr,"*** check aaa ***\n")
    key := in.GetKey()
    population := in.GetPopulation()
    fmt.Fprintf (os.Stderr,"key = " + key + "\n")
    fmt.Fprintf (os.Stderr,"population = %d\n" , population)
    rkey := redis_update_proc (key,population)
    return &pb.RedisReply{Key: rkey }, nil
}

// ---------------------------------------------------------------
func main() {
    lis, err := net.Listen("tcp", port)
    if err != nil {
        log.Fatalf("failed to listen: %v", err)
    }
    s := grpc.NewServer()
    pb.RegisterGreeterServer(s, &server{})
    if err := s.Serve(lis); err != nil {
        log.Fatalf("failed to serve: %v", err)
    }
}

// ---------------------------------------------------------------

クライアントプログラム

redis_update_client/main.go
// ---------------------------------------------------------------
//
//  redis_update_client/main.go
//
//                  Feb/11/2020
//
// ---------------------------------------------------------------
package main

import (
    "context"
    "fmt"
    "log"
    "os"
    "strconv"
    "time"

    "google.golang.org/grpc"
    pb "../redis_update"
)

const (
    address     = "localhost:50051"
    defaultKey = "t0001"
    defaultPopulation = int32(12345)
)

// ---------------------------------------------------------------
func main() {
    // Set up a connection to the server.
    conn, err := grpc.Dial(address, grpc.WithInsecure(), grpc.WithBlock())
    if err != nil {
        log.Fatalf("did not connect: %v", err)
    }
    defer conn.Close()
    c := pb.NewGreeterClient(conn)

    // Contact the server and print out its response.
    key := defaultKey
    population := defaultPopulation
    if len(os.Args) > 1 {
        key = os.Args[1]
        ppt,_ := strconv.Atoi (os.Args[2])
        population = int32(ppt)
    }
    ctx, cancel := context.WithTimeout(context.Background(), time.Second)
    defer cancel()
    r, err := c.RedisUpdate(ctx, &pb.RedisRequest{Key: key,Population: population})
    if err != nil {
        log.Fatalf("could not greet: %v", err)
    }

    rvalue := r.GetKey()

    fmt.Printf ("%s\n",rvalue)
}

// ---------------------------------------------------------------

gRPC のコードを作成します。

スクリプト

protoc -I redis_update redis_update/redis_update.proto --go_out=plugins=grpc:redis_update

サーバープログラムの起動

go run redis_update_server/main.go

クライアントプログラムの実行

$ go run redis_update_client/main.go t1858 3298700
t1858
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Go の gRPC で Redis のデータを読む (Read)

設定ファイル、サーバープログラム、クライアントプログラムの3つが必要です。

$ tree
.
├── redis_read
│   └── redis_read.proto
├── redis_read_client
│   └── main.go
└── redis_read_server
    └── main.go

設定ファイル

redis_read/redis_read.proto
syntax = "proto3";

package redis_read;

service Greeter {
    rpc RedisRead (RedisRequest) returns (RedisReply) {}
}

message RedisRequest {
    string key = 1;
}

message RedisReply {
    string strjson = 1;
}

サーバープログラム

redis_read_server/main.go
// ---------------------------------------------------------------
//
//  redis_read_server/main.go
//
//                  Feb/11/2020 
// ---------------------------------------------------------------
package main

import (
    "context"
    "log"
    "net"
    "fmt"
    "os"
    "strings"

    "google.golang.org/grpc"
    pb "../redis_read"
)

const (
    port = ":50051"
)

type server struct {
    pb.UnimplementedGreeterServer
}

// ---------------------------------------------------------------
func socket_read_proc (conn net.Conn,key_in string) string {
    str_received := ""
    _, err := conn.Write([]byte("get " + key_in + "\r\n"))
    if err != nil {
        fmt.Println(err)
        return str_received
        }

    buf := make([]byte, 1024)
    nn, err := conn.Read(buf[:])
    if err != nil {
        fmt.Println(err)
        return str_received
        }

    str_received = string(buf[0:nn])

    return  str_received
}

// ---------------------------------------------------------------
func redis_socket_read_proc (conn net.Conn,key_in string) string {
    json_str := ""
    str_received := socket_read_proc (conn,key_in)

    lines := strings.Split(str_received,"\n")

    if (! strings.Contains(lines[0],"END")) {
        json_str = lines[1]
        }

    return  json_str
}

// ---------------------------------------------------------------
func redis_read_proc (key_in string) string {
    str_json := ""

    hostname := "localhost"
    port := "6379"

    conn, err := net.Dial ("tcp", hostname + ":" + port)
    if err != nil {
        fmt.Println(err)
        return str_json
        }

    str_json = redis_socket_read_proc (conn,key_in)

//  str_json = key_in + " aaa " + key_in

    return str_json
}
// ---------------------------------------------------------------
func (s *server) RedisRead(ctx context.Context, in *pb.RedisRequest) (*pb.RedisReply, error) {
    fmt.Fprintf (os.Stderr,"*** check aaa ***\n")
    key := in.GetKey()
    fmt.Fprintf (os.Stderr,"key = " + key + "\n")
    str_json := redis_read_proc (key)
    return &pb.RedisReply{Strjson: str_json }, nil
}

// ---------------------------------------------------------------
func main() {
    lis, err := net.Listen("tcp", port)
    if err != nil {
        log.Fatalf("failed to listen: %v", err)
    }
    s := grpc.NewServer()
    pb.RegisterGreeterServer(s, &server{})
    if err := s.Serve(lis); err != nil {
        log.Fatalf("failed to serve: %v", err)
    }
}

// ---------------------------------------------------------------

クライアントプログラム

redis_read_client/main.go
// ---------------------------------------------------------------
//
//  redis_read_client/main.go
//
//                  Feb/11/2020
//
// ---------------------------------------------------------------
package main

import (
    "context"
    "fmt"
    "strconv"
    "log"
    "os"
    "time"
    "encoding/json"

    "google.golang.org/grpc"
    pb "../redis_read"
)

const (
    address     = "localhost:50051"
    defaultKey = "t0001"
)

// ---------------------------------------------------------------
func main() {
    // Set up a connection to the server.
    conn, err := grpc.Dial(address, grpc.WithInsecure(), grpc.WithBlock())
    if err != nil {
        log.Fatalf("did not connect: %v", err)
    }
    defer conn.Close()
    c := pb.NewGreeterClient(conn)

    // Contact the server and print out its response.
    key := defaultKey
    if len(os.Args) > 1 {
        key = os.Args[1]
    }
    ctx, cancel := context.WithTimeout(context.Background(), time.Second)
    defer cancel()
    r, err := c.RedisRead(ctx, &pb.RedisRequest{Key: key})
    if err != nil {
        log.Fatalf("could not greet: %v", err)
    }
    str_json := r.GetStrjson()
//  log.Printf("Greeting: %s", str_json)

    var unit_aa map[string]interface{}

    json.Unmarshal ([]byte(str_json), &unit_aa)

    fmt.Printf ("%s\t",key)
    population := int(unit_aa["population"].(float64)) 
    str_population := strconv.Itoa(population)
    fmt.Printf ("%s\t",str_population)
    fmt.Printf ("%s\t",unit_aa["name"])
    fmt.Println (unit_aa["date_mod"])
}

// ---------------------------------------------------------------

gRPC のコードを作成します。

スクリプト

protoc -I redis_read redis_read/redis_read.proto --go_out=plugins=grpc:redis_read

サーバープログラムの起動

go run redis_read_server/main.go

クライアントプログラムの実行

$ go run redis_read_client/main.go t1855
t1855   17859   勝山  1921-6-24
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Go の gRPC で Redis のデータを作成 (Create)

設定ファイル、サーバープログラム、クライアントプログラムの3つが必要です。

$ tree
.
├── redis_create
│   └── redis_create.proto
├── redis_create_client
│   └── main.go
└── redis_create_server
    └── main.go

設定ファイル

redis_create/redis_create.proto
syntax = "proto3";

package redis_create;

service Greeter {
    rpc RedisCreate (RedisRequest) returns (RedisReply) {}
}

message RedisRequest {
    string key = 1;
    string strjson = 2;
}

message RedisReply {
    string key = 1;
}

サーバープログラム

redis_create_server/main.go
// ---------------------------------------------------------------
//
//  redis_create_server/main.go
//
//                  Feb/12/2020 
// ---------------------------------------------------------------
package main

import (
    "context"
    "log"
    "net"
    "fmt"
    "os"

    "google.golang.org/grpc"
    pb "../redis_create"
)

const (
    port = ":50051"
)

type server struct {
    pb.UnimplementedGreeterServer
}

// ---------------------------------------------------------------
func redis_socket_write_proc (conn net.Conn,key_in string,json_str string) {
    fmt.Println (key_in)
    fmt.Println (json_str)

    comm_aa := "set " + key_in + " '" + json_str + "'\r\n"
    conn.Write([]byte(comm_aa))


    buf := make ([]byte,1024)
    conn.Read (buf[:])

    fmt.Println (string(buf[0:10]))
}

// ---------------------------------------------------------------
func redis_create_proc (key_in string,str_json string) string {

    hostname := "localhost"
    port := "6379"

    conn, err := net.Dial ("tcp", hostname + ":" + port)
    if err != nil {
        fmt.Println(err)
        return str_json
        }

    fmt.Fprintf (os.Stderr,"str_json = " + str_json + "\n")
    redis_socket_write_proc (conn,key_in,str_json)

    return key_in
}
// ---------------------------------------------------------------
func (s *server) RedisCreate(ctx context.Context, in *pb.RedisRequest) (*pb.RedisReply, error) {
    fmt.Fprintf (os.Stderr,"*** check aaa ***\n")
    key := in.GetKey()
    str_json := in.GetStrjson()
    fmt.Fprintf (os.Stderr,"key = " + key + "\n")
    fmt.Fprintf (os.Stderr,"str_json = " + str_json + "\n")
    redis_create_proc (key,str_json)
    return &pb.RedisReply{Key: key }, nil
}

// ---------------------------------------------------------------
func main() {
    lis, err := net.Listen("tcp", port)
    if err != nil {
        log.Fatalf("failed to listen: %v", err)
    }
    s := grpc.NewServer()
    pb.RegisterGreeterServer(s, &server{})
    if err := s.Serve(lis); err != nil {
        log.Fatalf("failed to serve: %v", err)
    }
}

// ---------------------------------------------------------------

クライアントプログラム

redis_create_client/main.go
// ---------------------------------------------------------------
//
//  redis_create_client/main.go
//
//                  Feb/12/2020
//
// ---------------------------------------------------------------
package main

import (
    "context"
    "fmt"
    "log"
    "os"
    "encoding/json"
    "strconv"
    "time"

    "google.golang.org/grpc"
    pb "../redis_create"
)

const (
    address     = "localhost:50051"
    defaultKey = "t0001"
    defaultName = "aaaa"
    defaultPopulation = 1
    defaultDate_mod = "2000-01-01"
)

// ---------------------------------------------------------------
func main() {
    // Set up a connection to the server.
    conn, err := grpc.Dial(address, grpc.WithInsecure(), grpc.WithBlock())
    if err != nil {
        log.Fatalf("did not connect: %v", err)
    }
    defer conn.Close()
    c := pb.NewGreeterClient(conn)

    // Contact the server and print out its response.
    key := defaultKey
    unit_aa := make(map[string]interface{})
    unit_aa["name"] = defaultName
    unit_aa["population"] = defaultPopulation
    unit_aa["date_mod"] = defaultDate_mod
    if len(os.Args) > 1 {
        key = os.Args[1]
        unit_aa["name"] = os.Args[2]
        unit_aa["population"],_ = strconv.Atoi (os.Args[3])
        unit_aa["date_mod"] = os.Args[4]
    }
    ctx, cancel := context.WithTimeout(context.Background(), time.Second)
    defer cancel()
    output, _ := json.Marshal(unit_aa)
    str_json := string(output)  
    r, err := c.RedisCreate(ctx, &pb.RedisRequest{Key: key,Strjson: str_json})
    if err != nil {
        log.Fatalf("could not greet: %v", err)
    }

    rvalue := r.GetKey()

    fmt.Printf ("%s\n",rvalue)
}

// ---------------------------------------------------------------

gRPC のコードを作成します。

スクリプト

protoc -I redis_create redis_create/redis_create.proto --go_out=plugins=grpc:redis_create

サーバープログラムの起動

go run redis_create_server/main.go

クライアントプログラムの実行

$ go run redis_create_client/main.go t0934  那須烏山 42938 2003-9-22
t0934
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【go + gin + gorm】GAEとCloud SQL for MySQLを使ってデプロイしてみる

【Go+Gin+Gorm】初心者だから超簡単webサービス作ってみる
【go + gin + gorm】webアプリにログイン機能を追加してみる
続きです。

今回は、劣化版ツイッターアプリをGCPを使ってデプロイしてみるという内容です。
GCPで使うのは、GAE standard環境と、Cloud SQL for MySQLです。
コードはgithubに上げています。

go modulesを使う

今回、GAEにデプロイするにあたって、$GOPATH周りでエラーが起こったので、go modulesを使うことにしました。
go modulesは依存モジュール管理ツールです。
Go ではGOPATHという概念があって、Go のコードは外部ライブラリも含めてすべて$GOPATH/src 以下に置くことになっています。
つまり、外部ライブラリと自分のコードが同列の場所にあるということになります。
でもそれだとプロジェクトごとに違うバージョンのライブラリを使いたい時などは困る。開発する度にいちいち外部ライブラリを消して入れ直したりをしなくてはならない。
詳しくは以下のリンク先を読むといいかもしれません。
Go Modules
Go言語の依存モジュール管理ツール Modules の使い方

以下のコマンドを実行していきましょう。

$ cd <このプロジェクト名>
// 依存モジュールの情報は `go.mod``go.sum` という名前のファイルに記載される。
$ go mod init
// 依存モジュールの自動インストール
$ go build

ちなみに、自作パッケージをimport文に記載するときは、
<プロジェクトのディレクトリ名>/<自作パッケージディレクトリ名>
にしないとgo buildのときにエラーになりました。

main.go
// 今回の場合はこんな感じ
mytweet-deploy/crypto

GAEへのデプロイ

goで作られたプログラムがデプロイされる際にGAE上で行われていることは、アプリケーションのビルドに必要なソースコードを $GOPATH 含めて探索し、すべてアップロードした上でリモートでコンパイルするという流れ。
手元でコンパイルしたアプリケーションのバイナリをアップロードするのではないし、主要なソースコードだけをアップロードしてリモートで go get するわけでもないようです。
この辺りは以下のブログで言及されていました。
Google App EngineでGoを動かすときに知っておくべきこと(ソースコード・ビルド編)

ここからはgcloudコマンドがローカルで使えるようになっていることを前提に話を進めます。
GCP上でプロジェクトを作成してください。
app.yamlファイルがある階層にcdして以下を実行。

$ gcloud app deploy

GAEに他のインスタンスがない場合は、GAEのインスタンス名はdefaultでいいですか?
GAEにdefaultという名前のインスタンスがある場合は、GAEのインスタンス名は何にしますか?
などを聞かれると思うので、入力してください。
何を入力したか忘れてもあとでブラウザ上で確認できます。

そのあとY/nという究極の選択を迫られるので、すかさずY
数分かかるので、コーヒーでも飲んで休憩してください。
特にエラーが出なければ、ターミナルで以下を実行。

$ gcloud app browse

ブラウザが現れてお馴染みのハローワールドが出てればOK。

嘘です。
エラーが出てたらOKです。
GCPコンソールのApp Engine上にさっき作ったインスタンス名が存在していればOKです。
エラーが出ているのは、データベースに接続できないからです。
これからCloud SQLにインスタンスを作って、アプリ側からデータベースに接続できるように設定していきます。
つまり、ここまででアプリのデプロイは完了したってことですね。

どんなエラーが出てるか見たいねんって方は、以下を実行。

$ gcloud app logs tail -s <インスタンス名>

おそらくデータベース接続の部分でエラーが出ているはず。。

本番環境とローカル環境との共存

本番環境のMySQLにつなぐ部分と、ローカル環境のMySQLにつなぐ処理は以下のように記述しました。
appengineライブラリのIsAppEngine()関数で、App EngineアプリがApp Engineで実行されているかどうかをbool型で取得することができます。
これでローカルでもGAE上でも動かすことができます。

main.go
    if appengine.IsAppEngine() {
        db, err = gorm.Open("mysql", cloudSQLConnection)
    } else {
        db, err = gorm.Open("mysql", localConnection)
    }
    if err != nil {
        panic(err.Error())
    }

appengineライブラリの公式リファレンス

Cloud SQLにデータベースを作成する。

先ほど言ったように、ただ単にGAEにプログラムをデプロイしてもデータベースがないと今回のプログラムは動きません。
まずは以下のリンクからGoogle Cloud SQL APIを有効にして下さい。
https://cloud.google.com/sql/docs/mysql/admin-api/?hl=ja

ブラウザでGCPのコンソールを開いてください。
左上のハンバーガーメニューからSQLをクリック。
インスタンスを作成をクリック。
MySQLをクリック。
項目を適当に埋めていきます。
作成をクリック。
作成されたインスタンスをクリックすると概要というページに飛びます。
左メニューから接続をクリック。
プライベートIPにチェックを入れて、関連付けられたネットワーキングには、先ほどGAEで作ったインスタンス名を選択して下さい。その後保存をクリック。
次は左メニューからユーザーをクリック。ここではデータベースにアクセスできるユーザーを作成します。
ユーザーアカウントを作成をクリック。ユーザー名、パスワードを入力して下さい。保存しましょう。どこかにメモして下さい。
最後に、左メニューからデータベースをクリック。ここではデータベースを作っておきます。
データベースを作成をクリックして、名前を付けて保存。文字コードはutf8です。

設定は以上です。
概要ページに戻って、このインスタンスに接続という欄のインスタンス接続名をどこかにメモして下さい。
設定を.envに記入していきます。

// インスタンス接続名
mytweet_CONNECTIONNAME=project-id:region-name:instance-name
// 作成したユーザー名とパスワード
mytweet_USER=username
mytweet_PASS=12345678
// 作成したデータベースの名前
mytweet_DBNAME=test

保存をしたら、先ほどのようにGAEにデプロイして下さい。

$ gcloud app deploy
$ gcloud app browse

エラーなく画面が表示されたらデプロイ完了です。
お疲れ様でした。
ユーザー登録してみたり、ログインしてみたりして下さい。

Cloud SQLの中身をローカルで確認する

Cloud SQL Proxy を使用して、ローカルからCloud SQLに接続します。
以下MacOS 64ビットの方用です。それ以外の方はローカルマシンに Cloud SQL Proxy クライアントをインストールするを参照ください。

// プロキシをダウンロードします。
$ curl -o cloud_sql_proxy https://dl.google.com/cloudsql/cloud_sql_proxy.darwin.amd64

// プロキシを実行できるようにします。
$ chmod +x cloud_sql_proxy

// さっきメモしたインスタンス接続名が必要です。
$ ./cloud_sql_proxy -instances=<INSTANCE_CONNECTION_NAME>=tcp:3306

// MySQLを起動します。さっき作ったユーザー名が必要です。
$ mysql -u <USERNAME> -p --host 127.0.0.1 --port 3306

これでCloud SQL上のMySQLを確認することができます。
select文でテーブルの中身を確認したりしてみて下さい。

こちらがCloud SQL Proxyを導入する際の公式ドキュメントです。
ローカルテストにプロキシを使用する場合のクイックスタート

最後に

次はセッションをやろうかなと思ってます。

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

Golang x Beego x Docker x CircleCI x npmで開発環境をサクッと作ってみよう

DockerとCircleCIなどを組み込みつつGolangのBeego開発環境を構築します。
本稿では以下を前提とします。

前提となる知識
・terminalのコマンド操作
・viまたはエディタの操作
・Docker,docker-composeの知識
・gitやgithubの基本的な操作

前提となる条件
・OSはmacOSを前提とします。どうしてもWindowsなどの場合はVagrantでLinuxの仮想環境を立てるなどして自力で対応してみてください。
・バージョン管理のツールはGithubを用います。

この記事で書いていること
・Beegoの0からの環境構築
・Dockerとdocker-composeでコンテナ環境構築
・CircleCIの0からの設定
・フロントエンド環境の0からの構築

この記事に書いてないこと
・Go言語特有の実装方法やtipsなど

Beego環境のセットアップ

Goのインストールの確認

以下のコマンドでGoがローカルに入っているか確認しましょう。

go version

not foundと表示される場合は公式サイトからパッケージのダウンロードをするかbrewでインストールしましょう。
インストール後はGOPATHの設定が必要です。
手順としては大体以下ですが、環境によって異なることがあるので公式を確認しましょう。

  • GOPATHとなるディレクトリを作成しておく
mkdir ~/go/
  • ~/.bash_profileを開く
vim ~/.bash_profile
  • 以下を追記する
export GOPATH=$HOME/go
export PATH=$PATH:$GOPATH/bin
  • 保存後に反映
source ~/.bash_profile

Beegoのインストール

Goのインストールが完了してgoコマンドが使えるようになったことを確認できたらBeegoをインストールしましょう。
公式サイトを参考に必要なツールをインストールする。

go get -u github.com/astaxie/beego
go get -u github.com/beego/bee

正しく実行されると $GOPATH/bin の配下にバイナリファイルが格納されます。

プロジェクトを作成する

  • ソースコード格納用のディレクトリの作成(任意)
mkdir $GOPATH/src
  • プロジェクトを作成
$ cd $GOPATH/src
$ bee new beego-app
$ cd  beego-app
$ ls
conf        controllers main.go     models      routers     static      tests       views
  • プロジェクトを起動
bee run

「アプリケーション“beego-app”へのネットワーク受信接続を許可しますか?」とアラートが表示されると思うが、許可をクリックして http://localhost:8080/ にアクセスして以下のような画面が表示されることを確認する。

localhost_8080.png

ここまでで以下のような構成のプロジェクトが作成されています。
それぞれの役割を詳しく確認したい場合は公式ドキュメントを見てみましょう。

.
├── beego-app
├── conf
│   └── app.conf
├── controllers
│   └── default.go
├── main.go
├── models
├── routers
│   └── router.go
├── static
│   ├── css
│   ├── img
│   └── js
│       └── reload.min.js
├── tests
│   └── default_test.go
└── views
    └── index.tpl

Dockerとdocker-composeでコンテナを立ち上げる

ホスト側でのBeego環境はできましたが、他開発者との共有を簡単に行うことができるようにDockerとdocker-composeを使ってコンテナ環境を立ち上げましょう。

docker-compose.ymlの作成

以下のファイルをプロジェクト直下に配置する

$ touch $GOPATH/src/beego-app/docker-compose.yml
docker-compose.yml
version: '3'
services:
  app:
    container_name: beego-app
    build:
      context: docker
      dockerfile: Dockerfile
    volumes:
      - .:/go/src/beego-app
    ports:
      - 10080:10080

portが他コンテナやサービスにバッティングしてしまう場合は、そのポートをlsofコマンドで確認してkillするかportsのところを任意に書き換えてください。
buildで記載されているcontext直下のDockerfileをビルドします。

docker-compose について詳しく知りたい場合
http://docs.docker.jp/compose/compose-file.html

Dockerfileの作成

dockerというディレクトリを作成してDockerfileを追加します。
Dockerfileについてはこちら

mkdir $GOPATH/src/beego-app/docker
touch $GOPATH/src/beego-app/docker/Dockerfile
# @see::https://hub.docker.com/_/amazonlinux
FROM amazonlinux:2

# システムアップデート
RUN yum update -y

# gitのインストール
RUN yum install -y git

# @see::https://fedoraproject.org/wiki/EPEL
RUN amazon-linux-extras install -y epel

# amazon-linux-extrasでインストールできる最新のgolang
RUN amazon-linux-extras list | grep golang
RUN amazon-linux-extras install -y golang1.11
RUN go version

# beegoのインストール
ENV GOPATH /go
ENV PATH $PATH:$GOPATH/bin
RUN go get -u github.com/beego/bee
RUN go get -u github.com/astaxie/beego

# カレントディレクトリをコンテナに追加する
COPY . /go/src/beego-app

# 作業ディレクトリを指定する
WORKDIR /go/src/beego-app

# コンテナ実行時にコンパイルと実行を行う
CMD bee run

イメージはAWS EC2での運用を想定してamazonlinux:2を用いている。
amazonlinuxベースのコンテナ上にgoとbeegoをインストールし、ホスト側をコンテナ側にマウントし最後にbeegoを実行しています。

コンテナを起動する

コンテナを立ち上げる前に上記のDockerfileのportをmain.goのbeego.Runの引数に渡します。

main.go
package main

import (
    _ "beego-app/routers"
    "github.com/astaxie/beego"
)

func main() {
    beego.Run(":10080")
}

Dockerfileをビルドしてコンテナを起動します。
docker-compose.ymlのディレクトリで以下のコマンドを実行します。

$ docker-compose up -d
Creating network "beego-app_default" with the default driver
Building app
Step 1/14 : FROM amazonlinux:2
2: Pulling from library/amazonlinux

正常に起動できているかpsコマンドで確認してみましょう。

$ docker-compose ps           
  Name           Command         State            Ports          
-----------------------------------------------------------------
beego-app   /bin/sh -c bee run   Up      0.0.0.0:10080->10080/tcp

コンテナの中に入って見てみましょう。

$ docker exec -it beego-app bash
bash-4.2# ls
beego-app  conf  controllers  docker  docker-compose.yml  main.go  models  routers  static  tests  touch  views
bash-4.2# pwd
/go/src/beego-app

もし、うまくいかなくてクリーンな状態からやり直したい場合、 docker-compose down --rmi all --volumes で該当のコンテナ、ネットワーク、ボリューム、イメージだけを削除するか、
もし他のコンテナやイメージもリセットしてしまって良い場合はdockerのアプリケーションを起動してpreferencesから「Reset disk image」で完全にリセットしてしまう方法もあります。

上記うまく行っていたらコンテナの実行ログを見てみましょう。

$ docker logs -f beego-app
______
| ___ \
| |_/ /  ___   ___
| ___ \ / _ \ / _ \
| |_/ /|  __/|  __/
\____/  \___| \___| v1.10.0
2020/02/09 10:21:09 INFO     ▶ 0001 Using 'beego-app' as 'appname'
2020/02/09 10:21:10 INFO     ▶ 0002 Initializing watcher...
beego-app/controllers
beego-app/routers
beego-app
2020/02/09 10:21:12 SUCCESS  ▶ 0003 Built Successfully!
2020/02/09 10:21:12 INFO     ▶ 0004 Restarting 'beego-app'...
2020/02/09 10:21:12 SUCCESS  ▶ 0005 './beego-app' is running...
2020/02/09 10:21:12.975 [I] [asm_amd64.s:1357]  http server Running on http://:10080

http server Running on http://:10080 というところに着目して、10080番ポートにリッスンされていることが確認できたので以下にアクセスして確認してみます。

http://localhost:10080/

ホスト側の時と同じ画面が表示されたら成功。
localhost_10080_.png

CircleCIでCI環境を構築する

最近ではgithub actionsなども出てきているが、とはいえCircleCIを採用している現場もまだまだ多いとおもうので、ここではCircleCIを用いてCI「Continuous Integration(継続的インテグレーション)」環境を構築する。

config.ymlの作成

プロジェクト直下に .circleci というディレクトリを作成し、.circleci直下にconfig.ymlを置く。

$ mkdir $GOPATH/src/beego-app/.circleci
$ touch $GOPATH/src/beego-app/.circleci/config.yml

config.ymlを以下のように作成する

config.yml
# @see::https://circleci.com/docs/ja/2.0/configuration-reference/
version: 2.1


# 実行処理は 1 つ以上の名前の付いたジョブで構成され、それらのジョブの指定は jobs マップで行います。
jobs:
  # CircleCI上のテスト
  test:
    docker:
      - image: circleci/golang:latest
    steps:
      - checkout
      - run: echo "this is jobs"


# Workflow は、ジョブの集まりとその実行順序の定義に関するルールを決めるものです。
workflows:
  build-test-deploy:
    jobs:
      # CircleCI上のテスト
      - test

細かい定義は公式を確認してください。
今回はとりあえず最低限の定義のみ記載します。

実運用ではCircleCI上でテストコードを実行したり、特定のブランチ(masterやstagingなど)のときにデプロイを行い自動化するようなCD「Continuous Delivery(継続的デリバリー)」を実現したりします。

上記ではworkflowsからtestというjobsが実行され、jobsのtestで指定されているdockerイメージ上でstepsに記載されているコマンドが実行されます。

CircleCIにリポジトリを登録する

まずは上記のソースコードを各自のgithub上にリポジトリを作成します。
git?github?な方はこちらや他の記事などもググって参考にしながら進めてみてください。
リポジトリが作成できたらローカルで作成したソースコードをプッシュしましょう。

github.com_kqxgy385_cuddly-octo-meme (1).png

リポジトリの準備ができたらCircleCIに連携しましょう。
CircleCIのアカウントがない場合は https://circleci.com/ からgithubアカウントでサインアップしましょう。

ログインできたらAdd Projectsを見つけて該当のリポジトリの「Set Up Projects」を実行しましょう。
onboarding.circleci.com_project-dashboard_github_kqxgy385.png

既に.circleci/config.ymlを作成しているのでそのままStart Buildingしましょう
onboarding.circleci.com_project-setup_github_kqxgy385_cuddly-octo-meme.png

確認されますがStart Buildingしましょう
onboarding.circleci.com_project-setup_github_kqxgy385_cuddly-octo-meme (1).png

STATUSがRUNNINGからSUCCESSになれば成功です
app.circleci.com_github_kqxgy385_cuddly-octo-meme_pipelines.png

中に入るとSTEPSの詳細が確認できます
app.circleci.com_github_kqxgy385_cuddly-octo-meme_pipelines (1).png

これでプッシュされるたびにconfig.ymlに記載されている内容がcircleci上で実行されます。

フロントエンド環境を構築する

npm(Node Package Manager)でフロントエンドのパッケージを一括管理します。
npmが使えるかどうかは以下のコマンドで確認します。

npm -v && node -v

npmがない場合はまずnode.jsをインストールします。
node.jsをインストールすると一緒についてきます。
node.jsは公式から直接ダウンロードするか、homebrewを使ってダウンロードする方法があります。

参考
https://qiita.com/kyosuke5_20/items/c5f68fc9d89b84c0df09

初期化

プロジェクトのルートディレクトリで以下のコマンドを実行します。

npm init

すると以下のようにコマンドラインでいくつか質問されると思います。
特にこだわりがなければ全てenterでいいです。

package name: (beego-app) 
version: (1.0.0) 
description: 
entry point: (index.js) 
test command: 
git repository: (https://github.com/kqxgy385/cuddly-octo-meme.git) 
keywords: 
author: 
license: (ISC) 
Is this OK? (yes) 

するとpackage.jsonというファイルが生成されると思います。
npmでインストールしたパッケージのバージョン情報がpackage.jsonに格納されます。

以下のような内容になっていると思います。

package.json
{
  "name": "beego-app",
  "version": "1.0.0",
  "description": "",
  "main": "index.js",
  "directories": {
    "test": "tests"
  },
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1"
  },
  "repository": {
    "type": "git",
    "url": "git+https://github.com/kqxgy385/cuddly-octo-meme.git"
  },
  "author": "",
  "license": "ISC",
  "bugs": {
    "url": "https://github.com/kqxgy385/cuddly-octo-meme/issues"
  },
  "homepage": "https://github.com/kqxgy385/cuddly-octo-meme#readme"
}

先ほどのコマンドラインで回答した内容が反映されるようになっています。
作り直したい場合は一度package.jsonを削除してもう一度npm initしてみましょう。

scriptsというところでコマンドを定義しておくことが可能で、デフォルトではtestというechoしてexitするだけのスクリプトが定義されています。
ためしに npm run test と実行してみましょう。
今後ここにjsやcssなどのbuildのコマンドを定義して利用します。

パッケージの復元

package.jsonが置いてあるプロジェクトルートで以下のコマンドを実行します。

npm install

するとpackage.jsonに記載されている依存関係がインストールされ、package-lock.jsonというファイルが生成されます。

パッケージのインストール

必要パッケージのインストール方法を確認します。
以下のコマンドを実行してみます。

npm install webpack

するとnode_modulesというディレクトリが生成され、またpackage.jsonに以下のように追記されていると思います。

package.json
  "dependencies": {
    "webpack": "^4.41.6"
  }

これでプロジェクト内でwebpackを利用することができます。
必要なパッケージはnpm installでインストールしていきます。
node_modulesは一般的にgit管理するものではないのでgitignoreに入れておきましょう。

webpackでフロントエンドをバンドルできるようにしよう

ここから先は細かい説明は省略します。
package.jsonのscriptsとdependenciesを以下にします。

package.json
  "scripts": {
    "dev": "webpack-dev-server --config webpack.config.js",
    "webpack": "webpack --config webpack.config.js"
  },
  "dependencies": {
    "css-loader": "^3.4.0",
    "extract-text-webpack-plugin": "^3.0.2",
    "file-loader": "^5.0.2",
    "html-webpack-plugin": "^3.2.0",
    "node-sass": "^4.13.0",
    "sass-loader": "^8.0.0",
    "style-loader": "^1.1.2",
    "vue": "^2.6.11",
    "vue-loader": "^15.8.3",
    "vue-template-compiler": "^2.6.11",
    "webpack": "^4.41.5",
    "webpack-cli": "^3.3.10",
    "webpack-dev-server": "^3.10.1",
    "write-file-webpack-plugin": "^4.5.1"
  }

プロジェクト直下にwebpack.config.jsというファイルを作成し、内容を以下にしてみましょう。
webpackについては公式をお勧めします。

webpack.config.js
const path = require('path');
const projectRoot = path.resolve(__dirname);
const HtmlWebpackPlugin = require('html-webpack-plugin');
const VueLoaderPlugin = require('vue-loader/lib/plugin');
const WriteFileWebPackPlugin = require('write-file-webpack-plugin');

module.exports = {
    // エントリーポイントを指定する
    entry: './static/js/entry/index.js',
    // bundleファイルをwebpackがどこにどのような名前で出力すればいいのかを指定する
    output: {
        filename: '[name].js',
        path: path.join(projectRoot, 'static/js/dist')
    },
    // webpack-dev-serverのオプションを選択する
    devServer: {
        // 使用するホストを指定する
        host: 'localhost',
        // リクエストをリッスンするポートを指定する
        port: '8000',
        // サーバーに提供するコンテンツを指定する
        contentBase: path.join(__dirname, "static/js/dist"),
    },
    // @see::https://webpack.js.org/configuration/devtool/
    devtool: "cheap-module-eval-source-map",
    // ローダーの設定
    module: {
        rules: [
            {
                test: /\.vue$/,
                loader: 'vue-loader'
            },
            {
                test: /\.scss$/,
                use: ["style-loader", "css-loader", "sass-loader"]
            },
            {
                test: /\.(png|jpg|gif)$/,
                use: [
                    {
                        loader: 'file-loader'
                    }
                ]
            },
        ]
    },
    // プラグインの設定
    plugins: [
        new VueLoaderPlugin(),
        new HtmlWebpackPlugin({
            template: path.resolve(__dirname, 'static/index.html')
        }),
        new WriteFileWebPackPlugin(),
    ]
};

バンドルされたファイルが static/js/dist に生成されるように設定されています。

ここまでできたらもう一度npm installします。

$ npm install

ここまででプロジェクトの構成は以下のようになっているかと思います。

.
├── README.md
├── conf
│   └── app.conf
├── controllers
│   └── default.go
├── docker
│   └── Dockerfile
├── docker-compose.yml
├── main.go
├── models
├── package-lock.json
├── package.json
├── routers
│   └── router.go
├── static
│   ├── css
│   ├── img
│   └── js
│       └── reload.min.js
├── tests
│   └── default_test.go
├── touch
└── views
    └── index.tpl

static直下にindex.htmlというファイルを生成し、内容を以下にしてみましょう。

<!DOCTYPE html>
<html lang="js">

<head>
  <title>beego-app</title>
</head>

<body>
  <div id="app" v-cloak></div>
</body>

</html>

jsというディレクトリの直下にentryというディレクトリとdistというディレクトリを作っておきます。
entryの直下にApp.vueというファイルを生成し、内容を以下にします。

<template>
  <div id="app" class="top">
    <header>
      <div class="title">
        <a href="/">BEEGO-APP</a>
      </div>
    </header>
  </div>
</template>

<script>
    export default {
        name: "app",
    };
</script>

<style lang="scss" scoped>
  .top {
    position: absolute;
    height: 1500px;
    width: 100%;
    background-color: #e8f0ff;
    top: 0;
    left: 0;

    header {
      display: flex;
      position: fixed;
      background-color: rgba(255, 255, 255, 0.9);
      height: 62px;
      width: 100%;
      z-index: 999;

      .title {
        display: block;
        position: relative;
        left: 10px;
        background-color: #d6e4ff;

        a {
          display: block;
          margin: 22px 0;
          color: #999999;
          cursor: pointer;
          text-decoration: none;

          &:hover {
            color: #da6b64;
          }
        }
      }
    }
  }
</style>

さらにentry直下にindex.jsというファイルを生成し、内容を以下にします。

index.js
import Vue from 'vue/dist/vue.esm.js'
import App from './App.vue'

new Vue({
    el: '#app',
    template: '<App/>',
    components: { App }
});

アプリケーションのデフォルトのアクセス先を変更するためrouters/router.goを以下に書き換えましょう。

router.go
package routers

import (
    "github.com/astaxie/beego"
)

func init() {
    beego.DelStaticPath("/static")
    beego.SetStaticPath("//", "static/js/dist")
}

ここまでできたらまずは以下のコマンドを実行してみましょう。

$ npm run webpack

先ほどpackage.jsonのscriptsで定義したwebpackを実行するコマンドのエイリアスです。
上記コマンドを実行して http://localhost:10080/ にアクセスして以下のような画面になっていれば成功です。
うまく行ってない場合はdocker-composeの再起動なども試してみましょう。

localhost_10080_.png

ここまでできたら次に以下のコマンドを試してみましょう。

$ npm run dev

先ほどpackage.jsonのscriptsで定義したwebpack-dev-serverを実行するコマンドのエイリアスです。

先ほどのwebpack.config.jsのdevServerのportを8000番にしたことにより http://localhost:8000 にアクセスできるようになります。
http://localhost:10080/ の時と同じ画面が見れていたら成功です。

今度はApp.vueの内容を適当に書き換えてみましょう。
npm run devを起動している最中はホットリロードが効いているはずなので、変更が即時で反映されていればwebpack.config.jsで設定した内容が反映されていることになります。

ここまでで構成は以下のようになります。

.
├── README.md
├── beego-app
├── conf
│   └── app.conf
├── controllers
│   └── default.go
├── docker
│   └── Dockerfile
├── docker-compose.yml
├── main.go
├── models
├── package-lock.json
├── package.json
├── routers
│   └── router.go
├── static
│   ├── css
│   ├── img
│   ├── index.html
│   └── js
│       ├── dist
│       │   ├── index.html
│       │   └── main.js
│       ├── entry
│       │   ├── App.vue
│       │   └── index.js
│       └── reload.min.js
├── tests
│   └── default_test.go
├── touch
├── views
│   └── index.tpl
└── webpack.config.js

次は?

デバッガツールdelveの導入、テストコード実行方法、MySQLコンテナを立ち上げてマイグレーションする方法などを追記します。そのうち。

参考
Getting started
https://beego.me/quickstart
Beego を触ってみる (環境構築)
https://qiita.com/macococo/items/e5ace2550418ccced9ac
Setting GOPATH
https://github.com/golang/go/wiki/SettingGOPATH
Compose ファイル・リファレンス
http://docs.docker.jp/compose/compose-file.html
npm入門
https://qiita.com/maitake9116/items/7825d90c09f3e2f87dea
webpack documentation
https://webpack.js.org/concepts/

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

JSON unmarshalやORMがreturnで結果を返すのではなく変数のポインタを使う理由

はじめに

GoでJSONをデコードするとき、こんな感じに変数のポインタを渡しますよね。

var result SomeStruct
err := json.Unmarshal(b, &result) // ポインタを渡して結果を詰め込んでもらう

rubyやpythonなら、JSONデコードってこんな感じですよね

# ruby
result = JSON.parse(some_json) # 入口からJSONが入って出口からパース結果が出てくる
# python 
result = json.load(some_json) # 入口からJSONが入って出口からパース結果が出てくる

さて、なぜでしょうか。

TL;DR

動的言語なら関数がどんな型を返しても良い訳です。
resultの型が何であっても、問題なく取り扱うことができます。
が、静的言語であるGoはそのようにはいきません。
同じようにresultを受け取る方式では、関数を利用する側は
取り出したい型がわかってるのに、取り出し後に毎回型アサーションをしなければなりません。

コレでピンときた人はブラウザバックです。

以上、では味気ないので、ツンと来なかった人は以下進んで下さい。

値に変更を加える、2種類の処理方法

Go言語では(他の言語でもですが…)関数やメソッドの引数に変数を渡すことも、
変数のポインタを渡すこともできます。
そして、変数を書き換える関数の実装には以下のように2通りの方法があります。

package main

import "fmt"

// A: 変数を受け取り、returnで新しい変数を返す
func plusOne(n int) int {
    return n + 1
}

// B: 変数のポインタを受け取り、直接書き換える
func plusOnePt(n *int) {
    *n++
}

func main() {
    // Aの使用側
    n1 := 10
    n1 = plusOne(n1)
    fmt.Println(n1) // 11

    // Bの使用側
    n2 := 10
    plusOnePt(&n2)
    fmt.Println(n2) // 11
}

変数のポインタを受け取って、新しい変数のポインタを作って返却する方式もありますが、
とりあえず「とある変数を元のスコープとは別の場所で直接書き換えるかそうでないか」
という比較がしたいので、2パターンのみ記載してます。

また、簡単のため以降の章でもreturnで新しい値を返す方式をA方式、
ポインタの指す値を直接書き換える方式をB方式と表現します。

どちらを使うべきか

直感で何となく分かるかと思いますが、基本的にはAを使うべきです。
関数に突っ込んだ値が使用側のコードとは違う場所でいつの間にか書き換わる
というのは直感的ではないですし、出現頻度的にもAの方式のほうが「なんとなく自然」
であることはそれなりの量のコードを書いてこられた方は実感できるのではないでしょうか。

パターンBが必要になる場面

さて、タイトルの回収です。

「素直に、入口に値を入れたら出口から出てきてくれればいいじゃん」
と思う人も多いかと思いますが、そうは問屋が卸さないパターンがあります。

  • interface{}を返す実装を避けたいとき★本題
  • 愚直な実装ではメモリを大量消費してしまうとき
  • ガベージコレクションを減らしたいとき
  • etc...

まさかり飛んできそうなので一応リストにはしましたが、
メモリ管理については今回の本題ではないのでそちらの話は省きます。

interface{} を返す実装を避けるとき

返却値が interface{} になってしまう場合。
言い換えると、何を返却すればいいのか関数の実装段階で不定ななもの、ですね。

例として encoding/json のMarshalとUnmarshal見比べてみましょう。

// (前準備: 以下のような構造体があったとします。)
type Message struct {
    Name string
    Body string
    Time int64
}

まずはMarshalです。
関数シグネチャは func Marshal(v interface{}) ([]byte, error)
使い方は以下、returnされた値を受け取る方式(A方式)ですね。

m := Message{"Alice", "Hello", 1294706395881547000}
b, err := json.Marshal(m)

次はUnmarshalです。
関数シグネチャは func Unmarshal(data []byte, v interface{}) error
使い方は以下、ポインタを渡して値を詰め込んでもらう方式(B方式)です。

m = Message{
    Name: "Alice",
    Body: "Hello",
    Time: 1294706395881547000,
}

b := []byte(`{"Name":"Bob","Food":"Pickle"}`)
var m Message
err := json.Unmarshal(b, &m)

この2つ、同じJSONを扱う関数なのになぜ処理の仕方が違うのか?
結論から言うと、「返却したい値」が不定の場合にパターンBを使用します。

  • Marshalは常に []byte を返す(関数実装時に決定している)
  • UnmarshalはJSONの値を詰め込んだ任意のstruct (関数実装時には不定)

Marshal, Unmarshal共に、組み込み型に加えユーザーが好き勝手作った構造体を扱わなければなりません。
つまり、これらの関数が作られた段階では引数・返却値として扱うべき構造体がそもそも存在しないのです。
この未知の構造体を、Marshalは「入力」として、Unmarshalは「出力」として受け付ける
という違いがあります。

仮にUnmarshalをMarshal同様にA方式で実装すると、関数シグネチャはこの様になります。
func Unmarshal(data []byte) (v interface{}, error)

さて、もしこのような実装の場合、関数利用者はどのようにJSONのUnmarshalを行うでしょうか。

m = Message{
    Name: "Alice",
    Body: "Hello",
    Time: 1294706395881547000,
}

b := []byte(`{"Name":"Bob","Food":"Pickle"}`)
maybeMessage, err := json.Unmarshal(b)
// 型アサーションする
if m, ok := maybeMessage.(Message); !ok {
    ... 
}
...

こうなりますね。関数利用者は自分がUnmarshalしたい型はMessage型だと知っているのに、
わざわざ型アサーションでjsonのunmarshal結果がMessage型であることを示さなければなりません。
Go言語ではinterface Xを満たす型であればどのような型でもinterface Xとして扱える反面、
一度interface Xとして振る舞わせてしまったら再び元の型として扱いたくても
それを暗黙的に行うことはできません。

このように、使用者側が受け取りたい型を知っているにも関わらず型アサーションの負担を強いるような場面
では、B方式を取る選択をする事が多いです。その他の例では、コードを端折ってしまいましたが僕の好きなORMである
Gormでも、やはりDBから実際に値をロードするべき入れ物が不定なため、パターンBの実装になっています。

終わりに

コードを書いていて関数やメソッドが interface{} を返すような実装になりそうなら、
パターンBの実装を検討してみてはいかがでしょうか。ただ、ポインタによる書き換えは
いわゆる「驚き最小の原則」には従っていないと個人的には思っており、利用側が
使い方を理解できるカタチで関数コメントなりドキュメントなりに記しておく必要もあります。
最終的には「どちらの実装になっていればより利用者が楽できるだろうか」が
判断基準になってくるのかな、と思います。

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

【Go】メソッドの定義方法サンプルメモ

基本の形

func (<レシーバー>) <関数名>([引数]) [戻り値の型] {
    [関数の本体]
}

サンプル

sample.go
package main

import (
    "fmt"
)

type myInt int

// 引数無し
func (i myInt) plusOne() myInt {
    return i + 1
}

// 引数あり
func (i myInt) plus(j myInt) myInt {
    return i + j
}

// レシーバーの変数名省略(呼び出し元の変数にはアクセスしない)
func (myInt) printHoge() {
    fmt.Println("Hoge")
}

func main() {
    var i myInt = 1
    result := i.plusOne()
    fmt.Println(result) // 2

    var j myInt = 4
    result2 := i.plus(j)
    fmt.Println(result2) // 5

    i.printHoge() // Hoge
}

参考

Go言語 - メソッド - 覚えたら書く https://blog.y-yuki.net/entry/2017/05/05/000000

改訂2版 基礎からわかる Go言語
古川 昇
シーアンドアール研究所 (2015-07-17)
売り上げランキング: 87,346
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【Go】関数の定義方法サンプルメモ

基本の形

func <関数名>([引数]) [戻り値の型] {
    [関数の本体]
}

サンプル

sample.go
package main

import (
    "fmt"
)

// 基本の形
func plus(a int, b int) int {
    return a + b
}

// 戻り値無し
func printPlus(a int, b int) {
    fmt.Println(a + b)
}

// 戻り値&引数無し
func printDummy() {
    fmt.Println("printDummy")
}

// 戻り値複数
// 元の値と、足し算した値を返す
func plus2(a int, b int)(int, int, int){
    return a, b, a + b
}

// 可変長引数
// アンダースコア変数
// https://qiita.com/penguin_dream/items/c1df36040b3fc6d42945
func printVariable(strs ...string) {
    for _, str := range strs {
        fmt.Println(str)
    }
}

// 名前付きの戻り値
func plusMinus(a int, b int)(add int, minus int){
    add = a + b
    minus = a - b
    return
}

func main() {
    result := plus(1, 2)
    fmt.Println(result) // 3

    printPlus(2, 3) // 5

    printDummy() // printDummy

    a, b , c := plus2(1, 2)
    fmt.Println(a) // 1
    fmt.Println(b) // 2
    fmt.Println(c) // 3

    printVariable("1", "2", "3")

    add, minus := plusMinus(2, 1)
    fmt.Println(add) // 3
    fmt.Println(minus) // 1
}

参考

Go言語 - 関数 - 覚えたら書く https://blog.y-yuki.net/entry/2017/05/03/000000

改訂2版 基礎からわかる Go言語
古川 昇
シーアンドアール研究所 (2015-07-17)
売り上げランキング: 87,346
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む