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

Firestoreをgoの構造体で扱う

概要 GoでFirestoreを扱う際に、構造体を利用したかったのだがその取扱いについて詳細な記載がなかったのでいろいろと試してみました。 ただし今回の記事は、調査するための基盤づくりとしての要素が強いので調査内容自体は簡易的なものです。自身のユースケースに合わせて今後も調査できればと思います。 ちなみに今回は、構造体内でポインタを使った際の動作について検証しました。 (ある程度Firestoreの操作を知っている人向けになってしまうことをお許しください) 始めるにあたって PlayGroundで動作可能な範疇を超えていたのでGithubでレポジトリを作成しました。簡単な使い方を紹介していたりしていますが、調査が目的なため殴り書きです。悪しからず。 調査内容 概要でも示した通り、Firestoreでドキュメントを作成する際や取得する際にどのような構造体であれば、なんのデータが得られるのかを確認してみました。 具体的には以下のようなケースです。(今回の検証に利用した構造体です) type City struct { Name string `firestore:"name"` Tags []string `firestore:"tags"` Pop int64 `firestore:"population"` } type PC struct { Name *string `firestore:"name"` Tags []string `firestore:"tags"` Pop *int64 `firestore:"population"` } PCはPointerCityの略称です。 同じフィールド名に対して、ポインタで定義しているかそうでないかが異なっています。 こうした場合、特にポインタで定義した場合どのようになるか以下の観点で気になりました。 今回の検証対象 新規書込時にnilであれば書き込み対象にならないのか 読み込み時に存在しないフィールドを取得する際の挙動はどのようになるのか ([]stringで構造体として受け取れるのか否か) 検証していないこと 更新の際nilなフィールドは更新されないのか、はたまた削除されるのか 調査 今回は四つの流れで検証しました。 Do0 すべての項目が埋められたCity構造体でDocをCreate City構造体でDocの内容をGet PC構造体でDocの内容をGet Do1 一部の項目(Nameのみ)が埋められたCity構造体でDocをCreate City構造体でDocの内容をGet PC構造体でDocの内容をGet Do2 すべての項目が埋められたPC構造体でDocをCreate City構造体でDocの内容をGet PC構造体でDocの内容をGet Do3 すべての項目が埋められたPC構造体でDocをCreate City構造体でDocの内容をGet PC構造体でDocの内容をGet 基本的には作成、読み込みの手順を踏んでおり、四つの手法では作成の部分のみ異なっています。 関連するソースコードは以下のようになります。Githubからご確認ください。 cmd/structs/main.go internal/structs/action.go internal/structs/dos.go internal/structs/structs.go 結果 それぞれの手順で実行した結果を以下に示します。 一番上にFireStore内での構成。 その下にCityで取得した際の結果とPCで取得した際の結果を示しています。 Do0 Name: City Full * users/ * userid0/ * tags: ([]interface {} -> [hello world haha]) * population: (int64 -> 56) * createdAt: (time.Time -> 2021-07-30 11:57:13.986231 +0000 UTC) * name: (string -> sasakuna) * updatedAt: (time.Time -> 2021-07-30 11:57:13.986231 +0000 UTC) (end) Name: Data To City City: &{sasakuna [hello world haha] 56} Name: Data To PC City: interface{}( + &structs.PC{Name: &"sasakuna", Tags: []string{"hello", "world", "haha"}, Pop: &56}, ) Do1 Name: City Partial * users/ * userid0/ * population: (int64 -> 0) * tags: (Nil -> <nil>) * createdAt: (time.Time -> 2021-07-30 11:57:14.002157 +0000 UTC) * name: (string -> sasakuna) * updatedAt: (time.Time -> 2021-07-30 11:57:14.002157 +0000 UTC) (end) Name: Data To City City: &{sasakuna [] 0} Name: Data To PC City: interface{}( + &structs.PC{Name: &"sasakuna", Pop: &0}, ) Do2 Name: PC Full * users/ * userid0/ * name: (string -> sasakuna) * updatedAt: (time.Time -> 2021-07-30 11:57:14.016314 +0000 UTC) * tags: ([]interface {} -> [hello world haha]) * population: (int64 -> 56) * createdAt: (time.Time -> 2021-07-30 11:57:14.016314 +0000 UTC) (end) Name: Data To City City: &{sasakuna [hello world haha] 56} Name: Data To PC City: interface{}( + &structs.PC{Name: &"sasakuna", Tags: []string{"hello", "world", "haha"}, Pop: &56}, ) Do3 Name: PC Partial * users/ * userid0/ * createdAt: (time.Time -> 2021-07-30 11:57:14.030879 +0000 UTC) * name: (string -> sasakuna) * updatedAt: (time.Time -> 2021-07-30 11:57:14.030879 +0000 UTC) * tags: (Nil -> <nil>) * population: (Nil -> <nil>) (end) Name: Data To City City: &{sasakuna [] 0} Name: Data To PC City: interface{}( + &structs.PC{Name: &"sasakuna"}, ) まとめ 今回の結果から以下のようなことがわかりました。 値を割当てなかった際の挙動 タイプ Create Get 値 デフォルト値が保管される デフォルト値が代入される ポインタ Nilとして保管される nilのまま フィールド値そのものが存在しないようにふるまうわけではなさそう。 感想 よくよく考えたら、ソースコードみれば分かったのでは....? 今後も手っ取り早く動作を確認するのに使わせてもらいます。 というか今回の最大の成果は、Firestore内のデータを可視化したことでは? あとがき 書いていて、タグ情報ってほかに追加できないのかなって思っていたら、omitemptyという項目がありました。 これを追加すると0値の時にそのフィールドをエンコード対象としないことができるらしく、恐らく多くの人はこちらを使っていることと思います。(これの少し下あたり) ただ、0を代入することができないという側面もあるので、完全に無駄というわけではなさそう。 ということで、今回の成果は簡単にFirestoreの挙動調査ができるレポジトリをGithubにあげたという点に落ち着きました。
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Go言語 基礎文法

目次 Go言語とは? 基本文法 演算子とデータ型 制御文 関数 メソッドとインタフェース 並行処理 1.Go言語とは? Googleが2009年に公開した静的型付け言語 文法がシンプル 並行処理が得意 実行速度が早い 公式サービス Go Playground ブラウザ上でGo言語のソースコードを実行する環境を提供している また、ソースコードのシェアやフォーマットが可能。 A Tour of Go チュートリアル形式でGo言語の一通りの文法を学ぶことができる コンパイル・実行方法 # コンパイル go build hello.go # exeファイルが作成されるので、あとはそれを叩けばよい # ビルドと実行を同時に実施する go run hello.go 2.基本文法 基本ルール 文末にセミコロンを付ける必要がない(付けても良い) セミコロンで文を区切れば1行に複数の処理を記載できる ダブルクォートで括ると通常の文字列、バッククォートで括る とraw 文字列 処理のブロックは波括弧({})で括る クラスのメンバアクセスはドットで行う 大文字と小文字は区別される 識別子に使えるのは文字、数値、アンダースコアで、数値以外から始める(2バイト文字も使用可) 関数定義は「func 関数名(引数...)」、戻り値の指定は「return 式」 プログラムはmainパッケージのmain関数から開始される ``` 変数 //「var 変数名 型名」で変数宣言できる var a int //初期値の指定なし var b int = 1 //初期値を指定 var c = 0 //型を省略 //複数の変数宣言1 var( d int e int = 1 f = 0 ) //複数の変数宣言2 var a1,a2 int var b1,b2 int = 0,1 var c1,c2 =0,1 変数(スコープによる違い) ローカルスコープ...関数内で宣言 グローバルスコープ...ソースのトップレベルで宣言 //ローカルスコープの変数に限り、varを省略して //変数名 := 式の形式で宣言できる //この際、変数の型は指定できない func main(){ a := 1 b,c := 2,3 var d int = 4 //varも使用可能 } 定数 constで宣言することが可能 func main(){ const c1 string = "hoge" const ( c2 = 1 c3 = c1 ) fmt.Println(c1,c2,c3) //hoge 1 hoge //iotaを使う定数 //iotaを使うと連続したものを生成することができる const ( c4 = iota c5 = iota c6 ) const ( c7 = iota c8 = 0 c9 = iota c10 ) fmt.Println(c4, c5, c6) //0 1 2 fmt.Println(c7, c8, c9, c10) //0 0 2 3 } コメント //単行コメント //コメント //複数行コメント /* コメント1 コメント2 コメント3 */ パッケージ パッケージのルール プログラムのグループ分けする機能で、 このパッケージのグループを「名前空間」と呼ぶ パッケージは一つ以上のソースファイルで構成される 同じパッケージに属するソースファイルは全て同一のディレクトリに格納 する 一つのディレクトリに複数のパッケージを配置することはでき ない mainパッケージを除き、パッケージ名と格納ディレクトリ名 は同じにする(これは必須ではなく慣例) パッケージのインポート //他のパッケージのインポート import "p1" //複数のインポート import ( "p2" "p3" "p4" ) パッケージのエクスポート 関数の最初の文字を大文字にするか、小文字にするかで挙動が変わる 小文字の場合、異なるパッケージに属するソースファイルからは参照できない 大文字の場合、異なるパッケージ外からも使用できる package p1 //この関数はp1パッケージ以外からは使用できない func fnc1(){ //略 } //この関数はp1パッケージ以外から使用可能 func Fnc2(){ //略 } 3.演算子とデータ型 演算子 演算子は大きく、「演算」「比較」「論理」「単項」に分類される 三項演算子(条件演算子)はない 算術演算子 演算子 説明 + 加算 - 減算 * 乗算 / 除算 % 余剰 & ビットAND | ビットOR ^ ビットXOR &^ ビットAND NOT << 左シフト >> 右シフト 比較演算子 演算子 説明 == 等しい != 等しくない < 未満 <= 以下 > より大きい >= 以上 論理演算子 演算子 説明 && AND || OR 単項演算子 演算子 説明 ++ インクリメント -- デクリメント - マイナス ^ ビットNOT ! 論理NOT データ型 「基本形」と「複合型」に分けられる 基本型 単体で意味を成すデータ型 * 数値型 * 文字列型 * 真偽値型 複合型 他の型と組み合わせて使用するデータ型 型宣言 型に別名をつけて新しい型を作成すること 「type 新しい型名 元の型」で宣言できる //int型をベースにした型宣言 type MyInt int //複数の宣言も可能 type ( MyInt 2 int MyInt 3 int ) //宣言した型の利用 var n1 MyInt = 1 var n2 MyInt = 2 var n int = 10 func main() { fmt.Println(n1 + n2) // 3 //fmt.Println(n1 + n) 型が異なるため演算できない } 数値型 以下の3種類がある。 整数型 浮動小数点型 複素数型 整数型 型 バイト数 説明 int 4 or 8※ 符号付きの整数 int8 1 符号付きの整数(8ビット) int16 2 符号付きの整数(16ビット) int32 4 符号付きの整数(32ビット) int64 8 符号付きの整数(64ビット) uint 4 or 8※ 符号なしの整数 uint8 1 符号なしの整数(8ビット) uint16 2 符号なしの整数(16ビット) uint32 4 符号なしの整数(32ビット) uint64 8 符号なしの整数(64ビット) byte 1 uint8と同じ rune 4 int32と同じ uintptr ※ ポインタの値を符号なしの整数で格納する ※CPUアーキテクチャに依存 ゼロ値は「0」 浮動小数点型 型 バイト数 説明 float32 4 32ビットの浮動小数点 float64 8 64ビットの浮動小数点 ゼロ値は「0.0」 複素数型 型 バイト数 説明 complex64 8 float32型の実数+虚数 complex128 16 float64型の実数+虚数 ゼロ値は「0.0」 文字列型 string型 UTF-8のバイト列を使用する ゼロ値は空文字("") 真偽値型 bool型 値はtrue, falseのどちらか ゼロ値はfalse 構造体型 複数のデータをひとまとめに扱うために使用する structを使って宣言する //構造体型の宣言 type MyStruct struct { a string b,c int } func main() { var st MyStruct st.a = "hoge" st.b = 1 st.c = 2 fmt.Println(st.a, st.b + st.c) //hoge 3 } //構造体の埋め込み type MyStruct2 struct { MyStruct d int } func main() { var st2 MyStruct2 st.a = "hoge" //埋め込んだ構造体のメンバアクセス st.d = 10 fmt.Println(st.a, st.d) } //構造体メンバのタグ付け //reflectパッケージの機能を使って参照できる import ( "fmt" "reflect" ) type MyStruct struct { a string `tag1:"value1" tag2:"value2"` b int `tag3:"value3"` } func main() { var st MyStruct field1 := reflect.TypeOf(st).Field(0) field2 := reflect.TypeOf(st).Field(1) fmt.Println(field1.Tag.Get("tag1")) //value1 fmt.Println(field2.Tag.Get("tag3")) //value3 } ポインタ型 ポインタを扱うための型 変数宣言の際に型名の前に「*」をつける 変数に「&」をつけると変数のアドレスを参照することができる アドレスから変数の値を参照するには「*」を変数につける func main(){ //int型のポインタ変数 var p *int //int型の変数 n := 10 //変数nのアドレスを取得 p = &n fmt.Println(p) //0x116d006c fmt.Println(*p) //10 } また、new関数を使って、動的にメモリを確保することが可能。 func main(){ //型を指定してメモリを割り当てる var p *int = new(int) fmt.Println(p) //0x116da0bc fmt.Println(*p) //0 } 配列型 「変数名 [データ長]型名」で定義する 初期値を指定する場合は、配列リテラルを使用する(配列リテラルは{}の中に、,で区切った値を記述する) func main() { //配列の定義 var a [3]int b := [2]string{"hoge", "fuga"} //初期値を指定すると「...」表記で長さを省略できる c := [...]int{1,2} //インデックスを指定して値を割り当てることも可能 d := [3]int{ 1:1, 2:10 } fmt.Println(a[0]) //0 fmt.Println(b[1]) //fuga fmt.Println(d[0], d[1], d[2]) //0 1 10 //データ長はlen関数で取得可能 fmt.Println(len(a)) //3 fmt.Println(len(c)) //2 } スライス型 可変長な配列 func main(){ //スライスの定義 var a []int //nilが設定される b := []string{"hoge", "fuga"} c := []int {1, 2} fmt.Println(a) //[] fmt.Println(b[0]) //hoge fmt.Println(c[1]) //2 } マップ型 連想配列やハッシュ、辞書に相当する型 「map[キーの型名]要素の型名」で定義する //マップの定義 m := map[string]int{"a":1, "b":2} //要素への代入 m["b"] = 10 //要素の参照 fmt.Println(m["a"], m["b"]) //1 10 そのほかの型 * チャネル型 * 関数型 * インタフェース型 が存在。(後述記載) リテラル 以下のリテラルが使用可能 リテラル名 記述例 備考 整数リテラル 1111,01234,0xabcd 10進数、8進数、16進数 浮動小数点リテラル 1.1,1.2e+3 符指数表記可 虚数リテラル 1.1i ルーンリテラル 'あ' Unicodeコードポイントの数値を表す 文字列リテラル "あいうえお" エスケープシーケンス利用可 row文字列リテラル かきくけこさしすせそ 複数行の文字列を表記可 構造体リテラル struct{a int}{a: 0} 配列リテラル [2]int{0,1,2} スライスリテラル []int{0,1,2} マップリテラル map[string]int{"a":0} 関数リテラル func(a int) int {return 1} nil 指し示す先が無いことを表すための特別な値 型キャスト 「変換後の型(式)」の形式で行う var a int = 1 var b int16 = 2 var c int = a + int(b) 4.制御文 条件分岐 if文 if a > 1 { b = "hoge" } else if a == 1 { b = "fuga" } else { b = "boo" } //if文のスコープとなる変数が宣言可能 if s := "abc"; a > 0 { fmt.Println(s) //abc } switch文 他言語と違って、break文が必要ない 他のケースに移らせるためには「fallthrough」を使う switch i := 0; a { case 1: fmt.Println(i + 1) //1 fallthrough case 2: fmt.Println(i + 1) //2 case 3, 4: fmt.Println(i + 1) //実行されない default: fmt.Println(i) //実行されない } このような書き方もできる switch { case a == 0: fmt.Println(0) case a == 1: fmt.Println(1) case a == 1: fmt.Println(2) default: fmt.Println(999) } 繰り返し文 for文 //基本的なfor文 for i:=0;i<3;i++ { fmt.Println(i) //0,1,2 } //いわゆるforeach文 for i, t := range []int{5,6,7}{ fmt.Printf("i=%d,t=%d¥n",i,t) //i=0,t=5 i=1,t=6 i=2,t=7 } //いわゆるwhile文 w := 0 for w < 2 { fmt.Println(w) w++ } //無限ループ for { //略 } //ループを抜けるには、break //スキップするには、continue 5.関数 Go言語の関数の特徴 複数の戻り値を返却できる 戻り値に名前をつけて扱える //ノーマルな関数 func f1(a int, b int) int { return a + b } //可変長引数 func f2(s string, params ...string){ fmt.Printf("%s:", s) for _, p:= range params { fmt.Printf("%s", p) } } //複数の戻り値 func f3() (int, string) { return 100, "hoge" } //戻り値に名前を付けて扱う func f4() (i int, t int){ i = 5 t = 2 return } 関数型 関数を変数に代入するためのデータ型 //関数型の変数を定義 var fnc func(int, int) int //関数の代入 fnc = add fmt.Println(fnc(2,1)) //3 func add(a int, int) int { return a + b } クロージャ ローカル変数を参照している関数内関数 var fnc = f() fmt.Println(fnc()) //1 fmt.Println(fnc()) //2 fmt.Println(fnc()) //3 //ローカル変数を返却する関数 func f() func()int{ l := 0 //ローカル変数を参照する関数を返却する return func() int { l++ return l } } defer文 それを呼び出した関数が終了する際に実行すべき処理を記述できる リソースの解放など、確実に行いたい処理を記述するのに適している func main(){ f() //processing...と出力された後にfinish!と出力される } func f(){ defer fmt.Println("finish!") fmt.Println("processing...") } パニック エラー処理の仕組み 処理を停止して関数の呼び出し元に戻る仕組み 発生すると順番に関数の読み出し元に戻り、main関数まで戻るとスタックトレースを出力してプログラムを終了させる panic(エラー値)でパニックを起こすことができる func main(){ fmt.Println("start") sub() fmt.Println("end") //この行は実行されない } func sub(){ fmt.Println("sub start") panic("パニック!") fmt.Println("sub end") //この行は実行されない } 6.メソッドとインタフェース メソッド 特定の型に関連付けられた関数 (メソッドと関連づいている型のことを「レシーバ」と呼ぶ) //型宣言 type Calc struct{ a,b int } type MyInt int //メソッド(Calc構造体に紐づく関数) func (p Calc) Add() int { return p.a + p.b } //メソッド(MyInt型に紐づく関数) func (m MyInt) Add(n int) MyInt { return m + MyInt(n) } func main() { p := Calc{3,2} var m MyInt = 1 //メソッドの呼び出し fmt.Println(p.Add()) //5 fmt.Println(m.Add(2)) //3 } インタフェース メソッドの集まりを定義するためのもの 「type インタフェース名 interface {...}」の形式で宣言する //インタフェースの宣言 type Human interface { hello() walk() } //構造体とメソッドの宣言 type Masaru struct {   name string   age int } func(m Masaru) hello(){   fmt.Printf("%s%d歳です\n", m.name, m.age) } func(m Masaru) walk(){   fmt.Println("トコトコ...") } func(m Masaru) shout(){   fmt.Println("ワーーー!!") } func main(){ //構造体の値をインタフェースに設定する m := Masaru{"マサル", 20} var h Human = m //インタフェースから構造体のメソッドを呼び出す h.hello() //マサル20歳です h.walk() //トコトコ... h.shout() //アクセスできない } エラーインタフェース エラー処理のための仕組み Errorメソッドでエラー内容を確認することができる type error interface { Error() string } import( "fmt" "os" ) func main(){ //os.Open関数はファイルを開くための関数 //存在しないファイルを開こうとしてエラーを発生させる _, e := os.Open("./hoge.txt") if(e != nil){ fmt.Println(e.Error()) } //略 } 自作関数のエラーハンドリング import ( "errors" "fmt" ) func main() { e := test() fmt.Println(e.Error()) //errors.Newでエラーを生成する } func test() error { return errors.New("errors.Newでエラーを生成する") } 7.並行処理 ゴルーチン ゴルーチンとは処理の単位のこと 複数のゴルーチンを並行で動作させることで並行処理が実現できる go文で関数を呼ぶことで生成できる main関数は他のゴルーチンの終了を待たないため、自身(main関数のゴルーチン)が終了した時点でプログラムは終了する //ゴルーチンの生成 import ( "fmt" "time" ) func main() { fmt.Println("main実行") fmt.Println(&n) //ゴルーチンの生成 go sub() //3ミリ秒スリープする time.Sleep(time.Millisecond * 3) } func sub() { fmt.Println("sub実行") fmt.Println(&n) //10ミリ秒スリープする time.Sleep(time.Millisecond * 10) fmt.Println("sub実行2") //先にmainゴルーチンが終了するためこの行は実行されない } チャネル Go言語で値を送受信するための通信経路 ゴルーチン間でなんらかの値をやりとりするために使用する チャネルはmake関数で生成できる 用途に応じて次の3パターンの定義を行う chan 要素の型(送受信用のチャネル) chan <- 要素の型(送信用のチャネル) <-chan 要素の型(受信専用のチャネル) func main() { //チャネルの作成 ch := make(chan int) //ゴルーチンの生成 go send(ch) //チャネルによる値の受信 c := <-ch fmt.Printf("[値を受信:%d]¥n", c) } func send(ch chan int) { fmt.Println("[値を送信:1]") //チャネルによる値の送信 ch <- 1 }
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Go言語豆知識(一)

豆知識 Go言語には、スライスという概念があります。スライスは値型ではなく、参照型のデータです。スライスは値型のArrayを参照しています。 スライスを拡張するにはappendメッソドを使います。もしスライスを拡張するときに、元に参照するArrayの長さを超えた場合は、新しい参照元Arrayを再作りすることになります。 コード slice1 := []int{1,2,3,4} slice2 := slice1[:3] fmt.Println("最初の段階、slice1,slice2はおなじ参照元Arrayを参照しています") fmt.Println(slice1) fmt.Println(slice2) fmt.Println(&slice1[0]) fmt.Println(&slice2[0]) fmt.Println() fmt.Println("slice2の長さはまた参照元Arrayの長さを超えていない") slice2 = append(slice2, 15) fmt.Println(slice1) fmt.Println(slice2) fmt.Println(&slice1[0]) fmt.Println(&slice2[0]) fmt.Println() fmt.Print("slice2は元のArrayの長さを超えました。") slice2 = append(slice2, 16) fmt.Println(slice1) fmt.Println(slice2) fmt.Println(&slice1[0]) fmt.Println(&slice2[0]) 最初の段階、slice1,slice2はおなじ参照元Arrayを参照しています [1 2 3 4] [1 2 3] 0xc0000b4000 <-参照元のアドレス(Array) 0xc0000b4000 <-参照元のアドレス(Array) slice2の長さはまた参照元Arrayの長さを超えていない [1 2 3 15] [1 2 3 15] 0xc0000b4000 <-参照元のアドレス(Array) 0xc0000b4000 <-参照元のアドレス(Array) slice2は元のArrayの長さを超えました。[1 2 3 15] [1 2 3 15 16] 0xc0000b4000 <-参照元のアドレスA(Array) 0xc0000b8000 <-参照元のアドレス(新しいArray) 出力のように、最初slice1とslice2はおなじ参照元長さが4のArrayに参照しています。 slice1はArrayの0-3番目まで切り出しています。 slice2はArrayの0-2番目まで切り出しています。 1回目のappend作業をしてら、slice2の長さは3から4までに伸ばしました。そのときに参照元Arrayの長さは4です。slice2の拡張によって、参照元Arrayの4番目値が修正されました。 2回目のappend作業をしてら、slice2の長さは4から5までに伸ばしました。そのときに参照元Arrayの長さは4です。 もともとの参照元Array(長さ4)はslice2(長さ5)を納めないから、slice2の参照を収めるために、新しい参照元を作ることになります。 そのときに、slice1の参照元とslice2の参照元はそれぞれ異なる参照元になります。 0xc0000b4000 <-参照元のアドレスA(Array) 0xc0000b8000 <-参照元のアドレス(新しいArray) 説明 まとめ スライスは参照しているArrayの長さを超えたら、新しい参照元を作成することになります。
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む