- 投稿日:2021-01-29T21:16:31+09:00
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
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/mongocode
main.gopackage 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) }オレオレブログパッケージからコードを抜き出していますが、挙動については確認してから掲載しています。
プラグインを使ってMarkdownでpost_contentを書いていたのですが、wp_postを覗いたところ妙なタグで埋め尽くされていたり、魔窟のようになっていたのでこれを機に作ろうかなと。いろいろなプラグインを試したのが悪手だったのかも。 ↩
- 投稿日:2021-01-29T14:42:51+09:00
Go言語を試す
環境設定
Go言語仕様
https://golang.org/ref/specgoのインストール
https://golang.org/go getでパッケージを取れるか
https://pkg.go.dev/
go get xxxxGoland
有料のIDEVSCode
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_typesi++ 対応
文字列
インデックス指定でアスキーコード取得、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言語みたいだと感じた
参考
- 投稿日:2021-01-29T13:17:56+09:00
【自己学習用】はじめての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を選定していく。
その中で疑問点は明らかにしていきたい。