20210129のGoに関する記事は3件です。

goでmongodbからdocumentの取得

はじめに

WordPressでブログを運用しているのですが、文章を全てMarkdownで保持しておきたいと思い自前でブログシステムを書いています1。1年近く前にWeb屋をリストラされてしまい今は違う仕事をしているので、せっかくなので知らない技術でやってみようとGo(echo)とMongoDBを使って作っています。そこでの経験を書いていければと思います。Goについてはまともに始めて3ヶ月位で、体系的に、というよりは作りながら書籍/ドキュメントを読んでいるので誤りがあれば訂正いただけると幸いです。

開発環境

  • Windows10
  • Visual Studio Code
  • go version go1.15.5 windows/amd64
  • MongoDB server version: 4.4.3
  • go.mongodb.org/mongo-driver v1.4.5

開発環境はWindowsですが、CentOS7上で動作は確認しています。

MongoDB

このようなCollectionになっています。
1611919233416.png

database/User/indexの作成
use doblog  
db.createUser( { user:"USER", pwd:"PASSWORD", roles:[{ "role" : "dbOwner", "db" : "doblog" }] } );  
db.entries.createIndex({entryCode: 1},{unique: true});
db.entries.createIndex({entryId: 1},{unique: true});  
db.entries.createIndex({isPublished: 1, publishDate: -1});
db.entries.createIndex({category: 1, isPublished: 1, publishDate: -1});

unique:trueでユニークフィールドに。他はfind用。indexが効いているかはexplainでチェック。

(例
db.entries.find({category: "php", isPublished: 1}).sort({publishDate: -1}).limit(1).explain();

参考にしました。 https://qiita.com/koshilife/items/79a8d59fa0973c16de57

WordPressからのデータ移行はphpで書いたのですが省略してます。

mongo-driver

go get -u go.mongodb.org/mongo-driver/mongo

code

main.go
package main

import (
    "context"
    "fmt"

    "go.mongodb.org/mongo-driver/bson"
    "go.mongodb.org/mongo-driver/mongo"
    "go.mongodb.org/mongo-driver/mongo/options"
)

// Entry for get data from mongodb
type Entry struct {
    EntryID     int32    `bson:"entryId"`
    EntryCode   string   `bson:"entryCode"`
    PublishDate string   `bson:"publishDate"`
    Title       string   `bson:"title"`
    Content     string   `bson:"content"`
    Category    []string `bson:"category"`
    IsPublished int32    `bson:"isPublished"`
    AuthorID    int32    `bson:"authorId"`
    CreatedAt   string   `bson:"createdAt"`
    UpdatedAt   string   `bson:"updatedAt"`
}

func main() {
    //
    // mongodb init
    //
    ctx := context.Background()
    credential := options.Credential{
        AuthSource: "doblog",
        Username:   "USER",
        Password:   "PASSWORD",
    }
    client, err := mongo.Connect(ctx, options.Client().ApplyURI("mongodb://127.0.0.1:27017").SetAuth(credential))
    defer client.Disconnect(ctx)
    //
    // get entries (publishDateの降順でソート)
    //
    var entryList []Entry
    entries := client.Database("doblog").Collection("entries")
    // publishDateの降順
    findOption := options.Find().SetSort(bson.D{{Key: "publishDate", Value: -1}})
    // PaginatorなどでLimit, Offsetを使いたい場合はこう
    // findOption := options.Find().SetSort(bson.D{{Key: "publishDate", Value: -1}}).SetSkip(10).SetLimit(5)

    // SQLでの select * from entries where isPublished = 1 と同様
    cur, err := entries.Find(ctx, bson.D{{Key: "isPublished", Value: 1}}, findOption)
    if err != nil {
        fmt.Println(err)

    }
    defer cur.Close(ctx)
    // findはスライス等で返ってこない。 *Cursol型で返ってくるので下記のようにループ回して取得
    for cur.Next(ctx) {
        var result Entry
        err := cur.Decode(&result)
        if err != nil {
            fmt.Println(err)
        }
        entryList = append(entryList, result)
    }
    fmt.Println(entryList)

    //
    // get entry (entryCodeを指定して一件取得)
    //
    var entry Entry
    err = entries.FindOne(ctx, bson.D{{Key: "entryCode", Value: "net-core-3-0-excel-epplus"}, {Key: "isPublished", Value: 1}}).Decode(&entry)
    if err != nil {
        fmt.Println(err)
    }
    fmt.Println(entry)
}

オレオレブログパッケージからコードを抜き出していますが、挙動については確認してから掲載しています。


  1. プラグインを使ってMarkdownでpost_contentを書いていたのですが、wp_postを覗いたところ妙なタグで埋め尽くされていたり、魔窟のようになっていたのでこれを機に作ろうかなと。いろいろなプラグインを試したのが悪手だったのかも。 

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

Go言語を試す

環境設定

  • Go言語仕様
    https://golang.org/ref/spec

  • goのインストール
    https://golang.org/

  • go getでパッケージを取れるか
    https://pkg.go.dev/
    go get xxxx

  • Goland
    有料のIDE

  • VSCode
    cmd + j でVSCodeのパネル開く
    go run xxxx.go で実行
    go build xxxx.go でバイナリファイルが残る

  • delve デバッガ
    go get github.com/go-delve/delve/cmd/dlv
    launch.jsonを作成 -> vscodeのrunタブにボタンが有る

基本

  • main.goから始まる
  • init() 関数は強制的に最初に実行される
  • 使わない変数を書くとコンパイルできない
  • 先頭は大文字でパブリック、小文字だとプライベートになる
  • fmt.Println()で出力

import

標準パッケージ一覧
https://golang.org/pkg/
パッケージはimportして使う
パッケージとはコードの集まり。ライブラリもほぼ同じ意味。クラスはない。
ディレクトリ名がpackage名となる。その中にgoファイルを配置。

app
L main.go
L package1
  L file1.go
  L file2.go
L package2

変数

var i int = 3
float64, string, bool
var を () でくくればvarを毎回先頭に付ける必要はない
初期値を代入しなかった場合デフォルト値となる。0、空文字、falseなど
fmt.Printf("%T\n", xi)

short variable declaration

xi := 1
xs := "ccc"
代入により自動で型が決まる
関数内でしか使えない

const

書き換えられない
型がない(untype)(実行するまで解釈されない)
通常はグローバルに書く
通常は先頭は大文字

数値

言語仕様
https://golang.org/ref/spec#Numeric_types

i++ 対応

文字列

インデックス指定でアスキーコード取得、stringで文字に変換
string("Hello"[0])
文字の置き換え
var s string = "Namaste"
s = strings.Replace(s, "N", "Z", -1)
strings.Contains(s, "mas")
バッククオートでヒアドキュメント ``

論理値

変わったところはない
&& || !

型変換

int float64
文字 -> 数字はpythonみたいにstringではできない
i, _ := strconv.Atoiで変換

戻り値をアンダースコア(_)で受け取れば、未使用でもコンパイルできる

配列

型部分で宣言したサイズは変更できません
var b [2]int = [2]int{30, 40}

スライス

サイズ変更できる配列
var b []int = []int{30, 40}
b = append(b, 50) で追加

var n []int 初期化しないとnilになる
n := make([]int, 0) だと中身は空だがメモリ上に存在する

map

m := map[string]int{"apple": 100, "banana": 300}
新しいキーに代入すると追加される

var m map[string]int 初期化しないとnil mapになり、追加できない

m := make(map[string]int) はnilじゃない
a, ok := m["grape"]
keyが存在しないと0を返し、2つ目の返り値にfalseが返る

バイト

b := []byte{72,73}
stringで文字列にキャストできる
c := []byte("HI")
文字列を渡すとアスキーコードに変換される

アスキーコード変換表
https://www.ascii-code.com/

関数

引数の型が同じ場合はまとめられる
返り値が複数の場合は()でくくる

func add2(x, y int) (int, int) {
    return x + y, x - y
}

名前付きの戻り値 Named Return Values

func calc(price, item int) (result int) {
  result = price * item
}

返り値として宣言した名前を変数として扱えるので、returnを書く必要なし
返り値が多くなってわかりにくい時など

Inner Function

関数内で変数に関数を代入して使う

f := func()

名前無し関数も作れる

func(x int) {
}()

クロージャー closure

関数を返す関数

func increment() func() int {

関数を戻す前に投入したパラメータを、戻した関数の処理に引き継ぐ事ができる

func circle(pi float64) func(radius float64) float64 {
    return func(radius float64) float64 {
        return pi * radius * radius
    }
}

可変長引数 Variadic Function Parameter

任意の個数の引数を受け取れる

func foo(params ...int) {

関数の中ではスライスで受け取れる

スライスを可変長引数の関数に渡すには

// スライスが展開される
foo(s...) 

コードスタイル

イコールの位置をそろえるのがgoの慣習
列挙する場合は演算子は詰めて書く

var a    = 100
var bbbb = 200
var cc   = 300

fmt.Println(1+1, 2*2)

ステートメント

if

()はなくて良い
;で一文でかける

// if / else if / else
if result == "ok" {

for

continue
break
//初期化とインクリメントを省略できる ;も省略できる
for sum < 10 { 
} 

for { } だけだと無限ループできる

range

リストやマップをかんたんに表示できる

for i, v := range list {

インデックスを使わない場合は _ で置き換える
マップのバリューのみ使わない場合は書かない
マップのキーのみ使わない場合は _ で置き換える

switch

switchの判定のみ使うのなら、;の前で取得して一文でかける
変数を書かなくても分岐に使える

switch {}

defer

前につけることで関数が終わったあとに実行する指示
close処理忘れ防止など

func defaTest() {
    defer fmt.Println("defer") // 関数が終わったら実行されます
    fmt.Println("end")   
}

deferはスタッキングされるので、複数あると最後から表示される

log

go logging package

log.Println()
log.Fatalln() // Fatalはそこでプログラムが終了する
log.SetOutput //設定

エラー

goは戻り値のerrを使って処理する(トライキャッチじゃなく)
:= はどれか一つを初期化してたらエラーにならないが一つもなければアウト

count, err := file.Read(data) 

panic、recover

deferでrecover()してpanic()が起きても正常終了させる
recoverはpanicの前に書く
基本panicをあえて書くのではなく、エラーハンドリングをするべき

ポインタ

ポインタ

var p *int = &n //intのポインタ型、アドレスを格納
p //アドレス
*p //ポインタが指す値

new make

指定の型のメモリ確保する

var p *int = new(int)

*p は0になる
スライスやマップはmakeでメモリ確保(ポインタを返さない)

struct 構造体

先頭は大文字でパブリック、小文字だとプライベートになる

type Vertex struct {
    X int
    Y int
    S string
}
v := Vertex{X: 1, Y: 2}

初期化しなければデフォルトが入る
名前を指定しなければ順番どおりはいる

アドレスを取る場合、newよりも&を先頭につける事が多い
(マップやスライスなどはmakeを使うほうが多い)

structの場合は、参照渡しでも*つけずに値にアクセスできる!!

struct

メソッド

定義済みstructに関連付けてドットで呼び出せる関数(値レシーバ)

func (v Vertex) Area() int {

structを参照渡しして、メソッド内で中身を書き換えるもの(ポインタレシーバ)

func (v *Vertex) Scale(i int) {

コンストラクタ

structをプライベートにすると他のパッケージからアクセスできないので
Newメソッドで初期化する

func New(x, y int) *Vertex {
    return &Vertex{x, y}
}

packageName.Newすることでstructを返す

embeded

structの中にstructを定義すると、クラス継承みたいな感じで拡張できる

type Vertex3D struct {
    Vertex //名前を書くだけ
    z int
}

func New(x, y, z int) *Vertex3D {
    return &Vertex3D{Vertex{x, y}, z}
}

non-struct

structではないカスタム型もメソッドをつけられる

type MyInt int
func (i MyInt) Double() int {

interface

メソッド名だけ書かれている型
structがメソッドを実装してないとエラーになる

type Human interface {
    Say()
}

使うにはstructを作るときに変数の後ろにつける

var mike Human = Person{"Mike"}

引数をinterfaceで宣言して、メソッドを実装済みのstructのみ受け付けるようにできる(ダックタイピング)

タイプアサーション

引数の型を interface{} にすれば何でも渡せる
その場合、i.(int) などで型を変換することをタイプアサーションという
インターフェースでない通常の型の変換(タイプコンバージョン、キャスト)とは違う

switch type文で複数の型に対応

Stringer

string()メソッドを実装するとそれが標準出力になる

func (p Person2) String() string {

カスタムエラー

自分なりのエラーを出したい場合はString()同様、Error()を実装すればよい

func (e *UserNotFound) Error() string {

Error()はポインタ型推奨

エラーを比較するときはポインタで比較しないと同じエラーになっちゃう

まとめ

C言語みたいだと感じた

参考

現役シリコンバレーエンジニアが教えるGo入門 + 応用でビットコインのシストレFintechアプリの開発

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

【自己学習用】はじめてのGo4

encoding/jsonパッケージ

JSONを扱うためにはencoding/jsonパッケージを用いる。
構造体にデータを代入し、ポインタをjson.Marshal()に渡すだけで,デフォルトのフォーマットでJSON文字列の[]byteを生成できる。

type Person struct {
    ID      int
    Name    string
    Email   string
    Age     int
    Address string
    memo    string
}

func main() {
    person := &Person{
        ID:      1,
        Name:    "Gopher",
        Email:   "gopher@example.org",
        Age:     5,
        Address: "",
        memo:    "golang lover",
    }
    b, err := json.Marshal(person)
    if err != nil {
        log.Fatal(err)
    }
    fmt.Println(string(b)) // 文字列に変換
}

/* 出力結果
{
  "ID": 1,
  "Name": "Gopher",
  "Email": "gopher@go.org",
  "Age": 5,
  "Address": ""
}
*/

小文字で始まるプライベートなメンバは出力結果に含まれない。
パブリックなメンバを出力しないようにしたり、結果のメンバ名を変えたい場合は構造体にタグを記述する。
プライベートなメンバを出力するようにするにはメンバ名を変更するしかない模様。

/*
`json:"name"` // nameというキーで格納する
`json:"-"` // JSONに格納しない
`json:",omitempty"` // 値が空なら無視
`json:",string"` // 値をJSONとして格納
*/

type Person struct {
    ID int `json:"id"`
    Name string `json:"name"`
    Email string `json:"-"`
    Age int `json:"age"`
    Address string `json:"address,omitempty"`
    memo string
}

JSONから構造体への変換

逆にJSONの文字列からデータをマップした構造体を生成するには,json.Unmarshal()を使用する。

func main() {
    var person Person
    b := []byte(`{"id":1,"name":"Gopher","age":5}`)
    err := json.Unmarshal(b, &person)
    if err != nil {
        log.Fatal(err)
    }

    fmt.Println(person) // {1 Gopher 5 }
}

ファイルの書き込み

他の言語と大して変わらないので省略!

net/httpパッケージ

簡単なhttpサーバの実装。

package main

import (
    "fmt"
    "net/http"

)

func IndexHandler(w http.ResponseWriter,
    r *http.Request) {

    fmt.Fprint(w, "hello world")
}

func main() {
    http.HandleFunc("/", IndexHandler)
    http.ListenAndServe(":3000", nil)
}

http.HandleFunc()でルーティングの設定。
ルートパス(/)に対するリクエスト(IndexHandler)を設定。
http.ListenAndServe()でポート指定してサーバを起動。
http://localhost:3000/
をブラウザから実行すると"hello world"が表示される。

POST

package main

import (
    "encoding/json"
    "fmt"
    "log"
    "net/http"
    "os"
)

type Person struct {
    ID int `json:"id"`
    Name string `json:"name"`
}

func IndexHandler(w http.ResponseWriter,
    r *http.Request) {

    fmt.Fprint(w, "hello world")
}

func PersonHandler(w http.ResponseWriter,
    r *http.Request) {
    defer r.Body.Close() // 処理の最後にBodyを閉じる

    if r.Method == "POST" {
        // リクエストボディをJSONに変換
        var person Person
        decoder := json.NewDecoder(r.Body)
        err := decoder.Decode(&person)
        if err != nil { // エラー処理
            log.Fatal(err)
        }

        // ファイル名を {id}.txtとする
        filename := fmt.Sprintf("%d.txt", person.ID)
        file, err := os.Create(filename) // ファイルを生成
        if err != nil {
            log.Fatal(err)
        }
        defer file.Close()

        // ファイルにNameを書き込む
        _, err = file.WriteString(person.Name)
        if err != nil {
            log.Fatal(err)
        }

        // レスポンスとしてステータスコード201を送信
        w.WriteHeader(http.StatusCreated)
    }
}

func main() {
    http.HandleFunc("/", IndexHandler)
    http.HandleFunc("/persons", PersonHandler) //curl http://localhost:3000/persons -d '{"id":1,"name":"gopher"}'
    http.ListenAndServe(":3000", nil)
}

html/templateパッケージ

テンプレートの作成

テンプレートを作成し、その中にパラメータを埋め込んで使用する。
構造体の値を埋め込むなら.〇〇。

<!DOCTYPE html>
<title>person</title>
<h1>{{ .ID }} : {{ .Name }}</h1>

使用するときはコンパイルしてから。
テンプレートのコンパイルはParseFiles()を用いる。

テンプレートへの値の埋め込み

コンパイルしたテンプレートに実際に値を埋め込むには,Execute()を用いる。
第二引数に渡した構造体がテンプレートの{{ .〇〇 }}にセットされる。

package main

import (
    "encoding/json"
    "fmt"
    "html/template"
    "io/ioutil"
    "log"
    "net/http"
    "os"
    "strconv"
)

type Person struct {
    ID   int    `json:"id"`
    Name string `json:"name"`
}

func IndexHandler(w http.ResponseWriter,
    r *http.Request) {

    fmt.Fprint(w, "hello world")
}

// テンプレートのコンパイル
var t = template.Must(template.ParseFiles("index.html"))

func PersonHandler(w http.ResponseWriter,
    r *http.Request) {
    defer r.Body.Close() // 処理の最後にBodyを閉じる

    if r.Method == "POST" {
        // リクエストボディをJSONに変換
        var person Person
        decoder := json.NewDecoder(r.Body)
        err := decoder.Decode(&person)
        if err != nil { // エラー処理
            log.Fatal(err)
        }

        // ファイル名を{id}.txtとする
        filename := fmt.Sprintf("%d.txt", person.ID)
        file, err := os.Create(filename) // ファイルを生成
        if err != nil {
            log.Fatal(err)
        }
        defer file.Close()

        // ファイルにNameを書き込む
        _, err = file.WriteString(person.Name)
        if err != nil {
            log.Fatal(err)
        }

        // レスポンスとしてステータスコード201を送信
        w.WriteHeader(http.StatusCreated)
    } else if r.Method == "GET" {
        // パラメータを取得
        id, err := strconv.Atoi(r.URL.Query().Get("id"))
        if err != nil {
            log.Fatal(err)
        }

        filename := fmt.Sprintf("%d.txt", id)
        b, err := ioutil.ReadFile(filename)
        if err != nil {
            log.Fatal(err)
        }

        // personを生成
        person := Person{
            ID:   id,
            Name: string(b),
        }

        // レスポンスにエンコーディングしたHTMLを書き込む
        t.Execute(w, person)
    }
}

func main() {
    http.HandleFunc("/", IndexHandler)
    http.HandleFunc("/persons", PersonHandler)
    //POST curl http://localhost:3000/persons -d '{"id":1,"name":"gopher"}'
    //GET  http://localhost:3000/persons?id=1
    http.ListenAndServe(":3000", nil)
}

ここまででGoの言語学習は終了。
Goの印象としては
分かりにくいところはなくjava等が理解できれば問題なく理解可能
例外処理がなくてpanicになっている箇所くらい?
ラムダ式は使えない・・・?調べてもちょっとわからなかった
html/templateパッケージは実際どの程度使用されているのか疑問
vueやreactとの相性は?という点で
次回からGoのWebFrameWorkを選定していく。
その中で疑問点は明らかにしていきたい。

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