- 投稿日:2022-02-14T20:49:05+09:00
RaspberryPi4BにGoを入れる。
概要 ちょっとしたツールを使おうと思ったらGoで書かれてたからGoを入れようとしてハマった話。 手順 好きなところにwget https://golang.org/dl/go1.17.7.linux-armv6l.tar.gzでダウンロード。 sudo tar -C /usr/local -xzf go1.10.1.linux-armv6l.tar.gzで展開。今回は/usr/localを指定。 ~/.bashrcにexport PATH=$PATH:/usr/local/go/binを追記してパスを通す。 source ~/.bashrcで適用。 ところが… go versionが通らずno such file or directoryが返る…。 あちこち調べるもののピンとこない。 なんとなくuname -mを打ってみるとaarch64と返ってきた。ここで気がついた…参考ページが結構古くてターゲットもRaspberryPiとしか書いていないことに。 試しにarmv6l向けではなくarm64向けをインストールしてみたら… 通った…。 armややこしいな。
- 投稿日:2022-02-14T20:22:05+09:00
# 【Go】一人コードリーディング会を始めます。(3回目)
はじめに 元ネタはtenntennさんの伝説カンファレンスの一部になります。 https://www.youtube.com/watch?v=X0tzCL5gqr8 コードリーディング会とは何人かで同じコードを各々で読んで、発見を共有して理解を深めましょう!というのが趣旨と理解しています。しかし知人で一緒にGoを学んでくれる人も見つけられず、悲しくも一人でコードリーディング会を開催することになりました。 やってみたら読めない箇所が多すぎました。1文ずつ嚙み砕いてGo言語の理解を深めていく作業になりました。その過程の不明点と解釈を記事にして公表していこうと思います。僕と同じ程度の理解度(初学者)の方が、不明点を理解するきっかけになればうれしいです。 ※1日の学びを1記事にするので情報量は少なめです。徐々に情報量を増やして有益な記事を書けるよう頑張ります。 ※個人の解釈を投稿する記事なので誤った情報が多々あると思います。申し訳ありませんがご了承ください。誤りについてはご指摘いただけるとうれしいです。 本記事は 3回目となり、 fmt パッケージを読み進めます。 ※前回まではerrors パッケージを読み進めていましたが、少し脱線します。 メモ 関数Printf は別の関数 Fprintf を呼んでいるだけ 以下の通りです。 // Printf formats according to a format specifier and writes to standard output. // It returns the number of bytes written and any write error encountered. func Printf(format string, a ...interface{}) (n int, err error) { return Fprintf(os.Stdout, format, a...) } 何気なく使っていた fmt.Printf ですが別の関数を呼んでいるだけなんですね。。。。 Printf は Fprintf をラッパーしているという理解です。 しかも Printf の引数に注目すると、第一引数にstring型、第二引数以降はインタフェースで受け取っていることがわかります。 Printfを利用するときは当たり前に文字列や数値を好き勝手渡していましたが、インタフェースで受け取っているから実行可能だとわかりました。 os.Stdout はosパッケージの関数Stdout ではない Fprintf の引数で渡している os.Stdout について、「標準出力に文字列を返すおまじない。多分関数とかでしょ。」と認識していたので、osパッケージに以下のような関数があると思ってました。 //※以下のような関数はありません!! func Stdout(str string) string { しかし実際は以下のように Stdot は変数でした。 // Note that the Go runtime writes to standard error for panics and crashes; // closing Stderr may cause those messages to go elsewhere, perhaps // to a file opened later. var ( Stdin = NewFile(uintptr(syscall.Stdin), "/dev/stdin") Stdout = NewFile(uintptr(syscall.Stdout), "/dev/stdout") Stderr = NewFile(uintptr(syscall.Stderr), "/dev/stderr") ) 以下のように書かれています。 Stdin, Stdout, and Stderr are open Files pointing to the standard input, standard output, and standard error file descriptors. (Stdin, Stdout, Stderr はオープン 標準入力、標準出力、標準エラーファイル記述子を指すファイルです。) そうらしいです。標準出力のファイルを渡しているのか?となってくると沼な気がするのでこの辺で辞めますが、 os.Stdout は関数ではなく、関数 NewFile で作成された標準出力をするために作成されたファイルだという点は理解できました。では引き続き Fprintf を確認していきます。 newPrinter は構造体 pp のポインタを返している Fprintf の処理を開始してすぐ newPrinter を呼んでいたのでそちらを確認しに行きます。 // Fprintf formats according to a format specifier and writes to w. // It returns the number of bytes written and any write error encountered. func Fprintf(w io.Writer, format string, a ...interface{}) (n int, err error) { p := newPrinter() p.doPrintf(format, a) n, err = w.Write(p.buf) p.free() return } ここで newPrinter が *pp 型の何かを返しているとわかりました。次は *pp が何者か突き止めます。 // newPrinter allocates a new pp struct or grabs a cached one. func newPrinter() *pp { p := ppFree.Get().(*pp) p.panicking = false p.erroring = false p.wrapErrs = false p.fmt.init(&p.buf) return p } pp は構造体でした。 // pp is used to store a printer's state and is reused with sync.Pool to avoid allocations. type pp struct { コメント部分を和訳します。 pp はプリンタの状態を保存するために使用され、割り当てを避けるために sync.Pool で再利用されます。 正直よくわかりません。ですが Fprintf の2行目が pp 型のポインタレシーバを持つ doPrintf メソッドを実行していることがわかりました。(数日前はこの処理まで追えなかったはずなので、少しは成長したかも。「プログラミング言語Go」すごい。)詳しい処理は追いませんが、レシーバを利用して渡した引数(構造体のフィールド)を更新してるはず。。 で最後に free メソッドを呼んでreturnしていますね。 // free saves used pp structs in ppFree; avoids an allocation per invocation. func (p *pp) free() { // Proper usage of a sync.Pool requires each entry to have approximately // the same memory cost. To obtain this property when the stored type // contains a variably-sized buffer, we add a hard limit on the maximum buffer // to place back in the pool. // // See https://golang.org/issue/23199 if cap(p.buf) > 64<<10 { return } free saves used pp structs in ppFree; avoids an allocation per invocation. (free は、ppFree に使用された pp 構造体を保存します;呼び出しごとのアロケーションを回避します。) どこかのコメントでキャッシュされた pp がある場合はそれを使う意図が書いてあったコメント見た。これですね。 // newPrinter allocates a new pp struct or grabs a cached one. func newPrinter() *pp { p := ppFree.Get().(*pp) ppFree.Get().(*pp) これなんだ? .(*pp) の箇所は型アサーションと呼ばれるインタフェース関連のやつです。まだ勉強できてないので本日のコードリーディングでは飛ばしますが、課題として残しておきます。 var ppFree = sync.Pool{ New: func() interface{} { return new(pp) }, } ↑これも次回の課題ってことで。ぜんぜんわからん。 最後に 少しだけ読めた気がしました。明日は休日(振休)なのでインターフェースの理解を深めたいと思います。 余談 明日は1Go日(15日)ですね。
- 投稿日:2022-02-14T20:21:18+09:00
【Go】テンプレートエンジンを使ってコンテンツを表示してみた
はじめに Goの標準で用意されているテンプレートエンジンを使って,HTMLページを表示させてみた. 環境 macOS BigSur Go 1.17.5 darwin/arm64 テンプレートエンジンとは 本題に入る前にテンプレートエンジンについて説明する. テンプレートエンジンとは「テンプレート」と呼ばれる雛形に「データ」を組み込んで最終的なHTMLを生成するもの.Web用のテンプレートエンジンは多くの本格的なフレームワークに含まれている.Goも例外ではなく,数種のテンプレートエンジンが組み込まれている.通常,ハンドラがテンプレートエンジンを呼び出してデータをテンプレートに組み込み,出来上がったHTMLをクライアントに返す.簡単なイメージ図はこんな感じ. テンプレートエンジンには標準規格がないが,大まかに2つの種類がある. ロジックなしのテンプレートエンジン ロジック埋め込みテンプレートエンジン ロジックなしのテンプレートエンジンは全くロジックの処理は行わず,文字列の置換だけを行う.表現とロジックを明確に分離することで,ロジックはハンドラによってのみ処理される.一方ロジック埋め込みテンプレートエンジンは,プログラミング言語のコードをテンプレートに埋め込み,実行時にテンプレートエンジンによって処理を行う. Goのテンプレートエンジン Go言語のテンプレートエンジンは,ロジックなし型とロジック埋め込み型のハイブリッドである.下図はWebアプリにおけるGo言語のテンプレートエンジンの動作を表している. マルチプレクサはリクエストを受け付け、URLに対応するハンドラへ転送する(他の言語ではルーティングと呼んだりする).ハンドラがテンプレートエンジンを呼び出して,使用するテンプレートを動的なデータと一緒に渡す.テンプレートエンジンはHTMLを生成して,ResponseWriterに生成したHTMLを書き込み,HTTPレスポンスにそれを追加する. Go言語には標準ライブラリとして,text/template と html/template が用意されている. それでは実践 1. Hello World まずは一番シンプルな,"Hello World"をテンプレートエンジンを使って表示させるものから始める.以下のようなテンプレートファイルtemplate.htmlを用意する.body内の{{ . }}に注目.このドット「.」はアクションと呼び,テンプレートエンジンがテンプレートを実行したときに,この部分にデータを埋め込む. template.html <!DOCTYPE html> <html> <head> <meta http-equiv="Content-Type" content="text/html; charset=utf-8"> <title>Hello Worldを表示させる</title> </head> <body> <h1>{{ . }}</h1> </body> </html> 次にハンドラ関数からテンプレートエンジンを呼び出す処理を記述する.server.go内にhandlerというハンドラ関数が定義されており,これがテンプレートエンジンを呼び出している.まず,ParseFiles関数でテンプレートファイルを解析し,Executeメソッドを呼び出してデータ(今回は"Hello World!")をテンプレートのアクションに埋め込んでいる. server.go package main import ( "html/template" "net/http" ) func handler(w http.ResponseWriter, r *http.Request) { t, _ := template.ParseFiles("template.html") t.Execute(w, "Hello World!") } func main() { server := http.Server{ Addr: "127.0.0.1:8080", } http.HandleFunc("/", handler) server.ListenAndServe() } 最後にserver.goを実行して,http://localhost:8080/ に接続すると... "Hello World!"が表示されていることが確認できる. 2. 条件アクション 次に重要なアクションの一つである条件アクションを実装してみる.条件アクションとは,テンプレートに渡されたデータをif elseなどを用いて条件分岐させるアクションである.以下のようなtemplate.htmlを用意する. template.html <!DOCTYPE html> <html> <head> <meta http-equiv="Content-Type" content="text/html; charset=utf-8"> <title>条件アクション</title> </head> <body> {{ if .key1 }} <h1>False</h1> {{ else if .key2 }} <h1>True</h1> {{ else }} <h1>False</h1> {{ end }} </body> </html> ここで,マップが map[string]string{"key1":"val1", "key2":"val2"} のように定義されているとき,テンプレート内では <p>{{ .key1 }}</p> のように指定することで,対応するバリューを表示させることができる. 実行結果 <p> val1 </p> ハンドラ関数を次のように定義する.今回はExcuteメソッドでboolをセットしたマップをテンプレートのアクションに埋め込んでいる. server.go package main import ( "html/template" "net/http" ) func handler(w http.ResponseWriter, r *http.Request) { t, _ := template.ParseFiles("template.html") t.Execute(w, map[string]bool{ "key1": false, "key2": true, }) } func main() { server := http.Server{ Addr: "127.0.0.1:8080", } http.HandleFunc("/", handler) server.ListenAndServe() } 最後にserver.goを実行して,http://localhost:8080/ に接続すると... 3. イテレータアクション イテレータアクションとは,配列やスライス,マップの要素ごとに反復処理を行うアクションである.次のように記述する. {{ range array }} ドットには要素が入る {{ . }} {{ end }} 次のようなテンプレートファイルとハンドラ関数を用意する. template.html <!DOCTYPE html> <html> <head> <meta http-equiv="Content-Type" content="text/html; charset=utf-8"> <title>イテレータアクション</title> </head> <body> <ul> {{ range . }} <li>{{ . }}</li> {{ end }} </ul> </body> </html> server.go package main import ( "html/template" "net/http" ) func handler(w http.ResponseWriter, r *http.Request) { t, _ := template.ParseFiles("template.html") daysOfWeek := []string{"月", "火", "水", "木", "金", "土", "日"} t.Execute(w, daysOfWeek) } func main() { server := http.Server{ Addr: "127.0.0.1:8080", } http.HandleFunc("/", handler) server.ListenAndServe() } ここでは曜日名を格納した文字列のスライスをテンプレートエンジンに渡し,実行時に {{ range . }} にあるドッドに渡され,スライスの各要素が順に処理される.最後にserver.goを実行して,http://localhost:8080/ に接続すると... 4. 代入アクション 代入アクションは,そのアクションで囲まれたセクション内で指定の値をドットに代入できるものである.次のように記述する. {{ with hoge }} ドットに {{ . }} が設定される {{ end }} //実行結果 ドットに hoge が設定される 以下のようなテンプレートファイルとハンドラ関数を用意する. template.html <html> <head> <meta http-equiv="Content-Type" content="text/html; charset=utf-8"> <title>代入アクション</title> </head> <body> <div>ドットの値は{{ . }}です</div> <div> {{ with "go"}} ドットは今、{{ . }}に設定されています {{ end }} </div> <div>ドットの値は{{ . }}に戻りました</div> </body> </html> server.go package main import ( "html/template" "net/http" ) func handler(w http.ResponseWriter, r *http.Request) { t, _ := template.ParseFiles("template.html") t.Execute(w, "hello") } func main() { server := http.Server{ Addr: "127.0.0.1:8080", } http.HandleFunc("/", handler) server.ListenAndServe() } 最後にserver.goを実行して,http://localhost:8080/ に接続すると... 5. インクルードアクション インクルードアクションとは,テンプレートの中に別のテンプレートを差し込むアクションのこと.これによってテンプレートを入れ子にすることができる.インクルードアクションは{{ template "name" . }}のように記述する.templateの引数は,1つ目はインクルードするテンプレート名,2つ目はテンプレートに渡すデータを指定する.次のようなテンプレートファイルtemplate1.htmlとtemplate2.htmlを用意する.template1.htmlはtemplate2.htmlをインクルードする. template1.html <!DOCTYPE html> <html lang="en"> <head> <meta charset="utf-8"> <meta http-equiv="X-UA-Compatible" content="IE=9"> <title>インクルードアクション</title> </head> <body> <div> ここは、template1.html(インクルードの前)</div> <div>template1.html内でのドットの値 - [{{ . }}]</div> <hr/> {{ template "template2.html" . }} <hr/> <div> ここは、template1.html(インクルードの後)</div> </body> </html> template2.html <div style="background-color: blue;"> ここはtemplate2.htmlです<br/> template2.html内でのドットの値 - [{{ . }}] </div> テンプレートを作成するときに名前を設定していないため,テンプレート名にファイル名が使用されていることがわかる.テンプレート名を定義するには次のように記述する. {{ define "main" }} <div style="background-color: blue;"> ここはtemplate2.htmlです<br/> template2.html内でのドットの値 - [{{ . }}] </div> {{ end }} こうすることでテンプレート名はmainと定義された. そして次のようなハンドラ関数を用意して実行すると server.go package main import ( "html/template" "net/http" ) func handler(w http.ResponseWriter, r *http.Request) { t, _ := template.ParseFiles("t1.html", "t2.html") t.Execute(w, "Hello World!") } func main() { server := http.Server{ Addr: "127.0.0.1:8080", } http.HandleFunc("/", handler) server.ListenAndServe() } 参考にした記事 Goプログラミング実践入門 HTMLテンプレート
- 投稿日:2022-02-14T18:50:26+09:00
Go言語_Slice
Go言語 Slice The Go Blog 『Arrays, slices (and strings): The mechanics of 'append'』から Arrays have their place—they are a good representation of a transformation matrix for instance—but their most common purpose in Go is to hold storage for a slice. Go言語における配列の最も共通する目的はスライスのストレージを保持するため A slice is a data structure describing a contiguous section of an array stored separately from the slice variable itself. A slice is not an array. A slice describes a piece of an array. スライスはスライス自体の変数とは別に保存されている配列の連続するするデータ構造の事です。スライスは配列ではありません、スライスは配列の一部を表現しています。
- 投稿日:2022-02-14T17:56:44+09:00
順列計算
プログラミングスキルチェックの問題を解く際に順列計算を行う場面が多くなった。 以下の記事を参考にさせていただきgolangで実装してみました。 順列の全探索をするプログラム(競技プログラミング向け) @suzuki-navi 様ありがとうございます。 備忘録目的です。 type Cat struct { id int eat_time int fuman int } (中略) //呼び出し側 cat_len:=len(cats) pat:=1 for { sb:="" i:=0 for i=0;i<cat_len;i++ { sb+=fmt.Sprintf(" %d", cats[i]); } fmt.Printf("pattern %d\n",pat); fmt.Println(sb); if !nextPermutation(cats) { break } pat++ } (中略) //順列を求める関数 //ここではStruct(Cat型)のSliceをidをキーに順列を求めています。 func nextPermutation(arr []Cat) bool { len:=len(arr) left:=len-2 for left >= 0 && arr[left].id >= arr[left+1].id { left-- } if left < 0 { return false } right:=len-1 for arr[left].id >= arr[right].id { right-- } t:=arr[left] arr[left]=arr[right] arr[right]=t left++ right=len-1 for left < right { t:=arr[left] arr[left]=arr[right] arr[right]=t left++ right-- } return true; }
- 投稿日:2022-02-14T14:37:29+09:00
GolangにORM導入(ent migration編)
記事作成にあたり GolangでORMを導入し、マイグレーション/CRUD処理を実施。GolangのORMはGORMやPrismaが有名だが、entを採用。entの説明や導入方法を記載。 アジェンダ entとは 環境情報 インストール スキーマ定義 マイグレーション実行 1. entとは Facebook社(Meta)提供のGolangのORM。公式リファレンスや機能も充実。 2. 環境情報 PC:Mac(CPUはintel製) Go:1.17.6 ←Goのver大事。versionは1.16以降をインストールしてください。 開発エディタ:Visual Studio Code 3. インストール ディレクトリ作成 ~/go/src $ mkdir ent_sample ~/go/src $ cd ent_sample ※GOPATH配下にディレクトリは作成ください。 go.modの作成 ~/go/src/ent_sample $ go mod init go: creating new go.mod: module ent_sample entのインストール ~/go/src/ent_sample $ go get -d entgo.io/ent/cmd/ent 4. スキーマ定義 テンプレート作成(Userテーブル) ~/go/src/ent_sample $ ent init User ※生成スキーマはアッパーキャメルでないとエラー出ます。 テンプレート作成後のディレクトリ構成 ~/go/src/ent_sample $ tree . ├── ent │ ├── generate.go │ └── schema │ └── user.go ├── go.mod └── go.sum 2 directories, 4 files →generate.goと /schema/user.goが作成されている。中身を見ていきましょう。 generate.go package ent //go:generate go run -mod=mod entgo.io/ent/cmd/ent generate ./schema /schema/user.go package schema import "entgo.io/ent" // User holds the schema definition for the User entity. type User struct { ent.Schema } // Fields of the User. func (User) Fields() []ent.Field { return nil } // Edges of the User. func (User) Edges() []ent.Edge { return nil } usersテーブルのスキーマ設定のため、Fields関数を変更。 /schema/user.go package schema import "entgo.io/ent" // User holds the schema definition for the User entity. type User struct { ent.Schema } // Fields of the User. ←Fields関数を修正。 func (User) Fields() []ent.Field { return []ent.Field{ field.Int("age"). Positive(), field.String("name"). Default("unknown"), } } // Edges of the User. func (User) Edges() []ent.Edge { return nil } ファイル生成 ~/go/src/ent_sample $ go generate ./ent ファイル生成後のディレクトリ構成 ~/go/src/ent_sample $ tree . ├── ent │ ├── client.go │ ├── config.go │ ├── context.go │ ├── ent.go │ ├── enttest │ │ └── enttest.go │ ├── generate.go │ ├── hook │ │ └── hook.go │ ├── migrate │ │ ├── migrate.go │ │ └── schema.go │ ├── mutation.go │ ├── predicate │ │ └── predicate.go │ ├── runtime │ │ └── runtime.go │ ├── runtime.go │ ├── schema │ │ └── user.go │ ├── tx.go │ ├── user │ │ ├── user.go │ │ └── where.go │ ├── user.go │ ├── user_create.go │ ├── user_delete.go │ ├── user_query.go │ └── user_update.go ├── go.mod └── go.sum 8 directories, 24 files →ファイルが多数作成されていることが確認されますね。 5. マイグレーション実行 dockerでPostgreSQLを準備。 docker-compose.yml version: '3.1' services: postgres: container_name: postgres image: postgres:13 ports: - 5432:5432 volumes: - ./.data/postgres:/var/lib/postgresql/data environment: POSTGRES_USER: postgres POSTGRES_PASSWORD: postgres PostgreSQLインスタンス生成 ~/go/src/ent-test $ docker-compose up -d Creating network "ent-test_default" with the default driver Creating postgres ... done 追加ライブラリインストールの上、main.goを作成 ライブラリの追加インストール(postgressのドライバー) go get github.com/lib/pq main.go package main import ( "context" "ent_sample/ent" "ent_sample/ent/migrate" "log" _ "github.com/lib/pq" //postgresは必須。https://github.com/bmizerany/pq.go/issues/15 ) func main() { client, err := ent.Open("postgres", "postgres://postgres:postgres@localhost:5432/enttest?sslmode=disable") // sslmodeが必要。https://stackoverflow.com/questions/21959148/ssl-is-not-enabled-on-the-server if err != nil { log.Fatalf("failed connecting to postgres: %v", err) } defer client.Close() ctx := context.Background() // マイグレーションの実行 err = client.Schema.Create( ctx, migrate.WithDropIndex(true), migrate.WithDropColumn(true), ) if err != nil { log.Fatalf("failed creating schema resources: %v", err) } } DBスキーマ作成 ~/go/src/ent_sample $ psql -h localhost -p 5432 -U postgres Password for user postgres: psql (14.1, server 13.4 (Debian 13.4-1.pgdg100+1)) Type "help" for help. postgres=# \l List of databases Name | Owner | Encoding | Collate | Ctype | Access privileges -----------+----------+----------+------------+------------+----------------------- postgres | postgres | UTF8 | en_US.utf8 | en_US.utf8 | template0 | postgres | UTF8 | en_US.utf8 | en_US.utf8 | =c/postgres + | | | | | postgres=CTc/postgres template1 | postgres | UTF8 | en_US.utf8 | en_US.utf8 | =c/postgres + | | | | | postgres=CTc/postgres (3 rows) postgres=# create database ent_sample; CREATE DATABASE postgres=# \l List of databases Name | Owner | Encoding | Collate | Ctype | Access privileges ------------+----------+----------+------------+------------+----------------------- ent_sample | postgres | UTF8 | en_US.utf8 | en_US.utf8 | postgres | postgres | UTF8 | en_US.utf8 | en_US.utf8 | template0 | postgres | UTF8 | en_US.utf8 | en_US.utf8 | =c/postgres + | | | | | postgres=CTc/postgres template1 | postgres | UTF8 | en_US.utf8 | en_US.utf8 | =c/postgres + | | | | | postgres=CTc/postgres (4 rows) main.goの実行 ~/go/src/ent_sample $ go run main.go ~/go/src/ent_sample $ 環境確認 ~/go/src/ent_sample $ psql -h localhost -p 5432 -U postgres Password for user postgres: psql (14.1, server 13.4 (Debian 13.4-1.pgdg100+1)) Type "help" for help. postgres=# \c ent_sample psql (14.1, server 13.4 (Debian 13.4-1.pgdg100+1)) You are now connected to database "ent_sample" as user "postgres". ent_sample=# \dt List of relations Schema | Name | Type | Owner --------+-------+-------+---------- public | users | table | postgres (1 row) ent_sample=# \d users Table "public.users" Column | Type | Collation | Nullable | Default --------+-------------------+-----------+----------+---------------------------------- id | bigint | | not null | generated by default as identity age | bigint | | not null | name | character varying | | not null | 'unknown'::character varying Indexes: "users_pkey" PRIMARY KEY, btree (id) 参考文献
- 投稿日:2022-02-14T11:31:23+09:00
Golangで実装したAPIをCloud Runにデプロイする
本記事の内容 Go言語で実装したAPIをCloud Runにデプロイする gcloud cliのインストール artifact registryへのdocker imageのpush はてなブログにて掲載しています。
- 投稿日:2022-02-14T07:55:49+09:00
【Go】GitHub上の「特定の1ファイル」を取得する
Go言語でAPIを使う練習のため、タイトルにある「GitHubのリポジトリにある特定の1ファイルの内容を取得する」プログラムを書いてみました。 取得するファイル(GitHubリポジトリ側) 今回はチュートリアル用のリポジトリから、以下のようなREADME.mdのコンテンツを取ってきてみたいと思います。 This is a script for API-tutorial - feature-A - fix-B - feature-C プログラム全文 package main import ( "encoding/base64" "encoding/json" "fmt" "io/ioutil" "net/http" ) func main() { blobUrl := "https://api.github.com/repos/fugithora812/git-tutorial/contents/README.md" req, err := http.NewRequest("GET", blobUrl, nil) if err != nil { fmt.Println("failed: NewRequest") } req.Header.Set("Accept", "application/vnd.github.v3+json") client := new(http.Client) resp, err := client.Do(req) if err != nil { fmt.Println("failed: DoReq") } defer resp.Body.Close() contents, err := ioutil.ReadAll(resp.Body) if err != nil { fmt.Println("failed: ReadAll") } var blob interface{} err = json.Unmarshal(contents, &blob) if err != nil { fmt.Println("failed: Unmarshal") } src := blob.(map[string]interface{})["content"] dec, err := base64.StdEncoding.DecodeString(src.(string)) fmt.Println(string(dec)) } 実行結果 $ go run main.go This is a script for API-tutorial - feature-A - fix-B - feature-C 解説 解説はこちらをクリックして読めます。(私の個人ブログに遷移します)
- 投稿日:2022-02-14T01:50:04+09:00
[Golang] reflectパッケージの使い方を調べたら書く
はじめに 最近reflectパッケージを使う機会があったので、その時どう書けばいいか調べたことをメモ書き。 気が向いたら更新してく。 面倒なのでimportとかは端折る。 reflect パッケージ なにものなのかは godoc を見る。 DeepL翻訳様様で翻訳した内容↓ reflectパッケージは、プログラムが任意の型を持つオブジェクトを操作することを可能にする、ランタイムリフレクションを実装しています。典型的な使い方は,静的な型interface{}を持つ値を受け取り, TypeOfを呼んで動的な型情報を取り出し,それをTypeで返すというものです. ValueOfを呼び出すと、実行時データを表すValueが返される。ZeroはTypeを取り、その型に対するゼロ値を表すValueを返す。 godocにも書かれているが https://go.dev/blog/laws-of-reflection も見てみると良さそう。 いろいろ使い方 Type/Valueを取得 interface{}を扱う場合の基本はここから始まる。 func a(v interface{}) { rt := reflect.TypeOf(v) // return reflect.Type rv := reflect.ValueOf(v) // return reflect.Value // do something... } 型を判定して分岐処理をする reflectパッケージで定数化されている型はGitHubの reflect/type.go を参照。 func a(rt reflect.Type) { switch rt.Kind() { case reflect.String: // string型の場合の処理 case reflect.Struct: // 構造体の場合の処理 case reflect.Ptr: // ポインタの場合の処理 default: // いずれの型でもない } } reflect.Valueでも同様にKind()を呼び出すことで型判定が可能。 (値がポインタでなければ基本的には問題なと思う。たぶん。) func a(rv reflect.Kind) { switch rv.Kind() { case reflect.String: // string型の場合の処理 default: // いずれの型でもない } } 型を判定して分岐処理をする(ポインタを考慮) reflect.TypeのElem()を実行すれば良い。 ※Array, Chan, Map, Ptr, Slice以外の型に対してElem()を呼び出すとpanicが発生する。 func a(rt reflect.Type) { switch rt.Kind() { case reflect.String: fmt.Println("string") case reflect.Ptr: fmt.Println("pointer") a(rt.Elem()) default: fmt.Printf("other(%s)", rt.Kind()) } } reflect.Valueでも同様の処理を書くことは可能だがnil値の考慮が必要になる。 これはNGな例 func main() { var s *string a(reflect.ValueOf(s)) } func a(rv reflect.Value) { switch rv.Kind() { case reflect.String: fmt.Println("string") case reflect.Ptr: fmt.Println("pointer") a(rv.Elem()) default: fmt.Printf("other(%s)", rv.Kind()) } } このコードを実行すると出力は以下のようになる。 nil値のポインタ型に対してElem()を実行した場合、Kind()メソッドが返す値はreflect.Invalidになるので、おそらく書いた本人が意図した挙動にはならない。 $ go run main.go pointer!! other(invalid) reflect.Ptrの場合にIsNil()を呼び出すことでnil値か判定してあげれば同じようなことができないわけではない。 func a(rv reflect.Value) { switch rv.Kind() { case reflect.String: fmt.Println("string") case reflect.Ptr: fmt.Println("pointer") if rv.IsNil() { fmt.Println("nil!!") return } a(rv.Elem()) default: fmt.Printf("other(%s)", rv.Kind()) } } Array, Sliceの型を取得 例えばSliceに設定可能な型を取得したい場合、Elem().Kind()で取得できる。 var s []string rt := reflect.TypeOf(s) fmt.Println(rt.Elem().Kind()) // string rv := reflect.ValueOf(s) fmt.Println(rv.Type().Elem().Kind()) // string reflect.Valueから値を取り出す プリミティブ型 取得したい型に対応するメソッドが用意されているので実行すれば良い。 ※String()以外のメソッドは実際の値の型と異なるメソッドを呼び出すとpanicが発生する。 func a(rv reflect.Value) { switch rv.Kind() { case reflect.Bool: fmt.Println(rv.Bool()) case reflect.Int, reflect.Int64, reflect.Int32, reflect.Int16, reflect.Int8: fmt.Println(rv.Int()) case reflect.Uint, reflect.Uint64, reflect.Uint32, reflect.Uint16, reflect.Uint8: fmt.Println(rv.Uint()) case reflect.Float32, reflect.Float64: fmt.Println(rv.Float()) case reflect.String: fmt.Println(rv.String()) default: fmt.Printf("other(%s)", rv.Kind()) } } String()はpanicを発生させることはないが、値がstring以外の型の場合は <int Value> のような文字列が返ってくる。 設定された値を文字列として出力したい場合にString()を使用すると意図しない結果になるので注意が必要。 それぞれの型でstringに変換するような処理を実装してあげる。 func a(rv reflect.Value) { switch rv.Kind() { case reflect.Bool: if rv.Bool() { fmt.Println("true") } else { fmt.Println("false") } case reflect.Int, reflect.Int64, reflect.Int32, reflect.Int16, reflect.Int8: fmt.Println(strconv.FormatInt(rv.Int(), 10)) case reflect.Uint, reflect.Uint64, reflect.Uint32, reflect.Uint16, reflect.Uint8: fmt.Println(strconv.FormatUint(rv.Uint(), 10)) case reflect.Float32: fmt.Println(strconv.FormatFloat(rv.Float(), 'f', -1, 32)) case reflect.Float64: fmt.Println(strconv.FormatFloat(rv.Float(), 'f', -1, 64)) case reflect.String: fmt.Println(rv.String()) default: fmt.Printf("other(%s)", rv.Kind()) } } ただ文字列になればいいだけなら別にこれでも良いが。 func a(rv reflect.Value) { fmt.Printf("%v", rv.Interface()) } Array、Slice SliceとArrayの使い方は基本的に同じ。 Len()を呼び出すと長さが取得できるのでforで回しながらIndex()を呼び出して値を取得していく。 Index()はreflect.Valueを返すので、型によって処理分岐したければKind()を使う。 ちなみにCap()を呼び出すとcapacityを取得できる。 ※Len()とCap()はArray, Chan, Slice型以外の場合panicが発生する。 ※Index()はArray, Slice, String型以外の場合panicが発生する。 func a(rv reflect.Value) { fmt.Println(rv.Len()) fmt.Println(rv.Cap()) for i := 0; i < rv.Len(); i++ { fmt.Println(rv.Index(i).String()) } } Map MapRange()を呼び出して、要素がなくなるまでforで回していく。 Mapのキーと値はそれぞれreflect.MapIterのKey(), Value()で取得可能。 KeyとValuえはともにreflect.Valueを返すので、型によって処理分岐したければKind()を使う。 ※MapRange()はMap型以外の場合panicが発生する。 func a(rv reflect.Value) { iter := rv.MapRange() // return *reflect.MapIter for iter.Next() { fmt.Printf("key: %v, value: %v", iter.Key(), iter.Value()) } } 構造体 NumField()で構造体が持つフィールド数を取得し、forで回していく。 フィールドの情報を取得するにはType()で一度reflect.Typeを取得してから、Field()を実行する必要がある。 フィールドの値を取得するにはFieldByName()を使用する(戻り値はreflect.Value) ※NumField(), Field() FieldByName()はStruct型以外の場合panicが発生する。 func a(rv reflect.Value) { for i := 0; i < rv.NumField(); i++ { f := rv.Type().Field(i) fv := rv.FieldByName(f.Name) fmt.Printf("field name: %s, value: %v", f.Name, fv) } } その他構造体のフィールドから情報を取るメソッド タグを取得 TagフィールドのGet()を実行する。 タグは文字列で帰ってくるので、あとは自分が使いたいようにパースしたりして使う。 encoding/json の実装を参考にしてみると良いかも。 func main() { a(reflect.ValueOf(struct { a string `json:"abcde"` }{a: "aaaa"})) // tag: "abcde" a(reflect.ValueOf(struct{ a string }{a: "aaaa"})) // tag: "" } func a(rv reflect.Value) { for i := 0; i < rv.NumField(); i++ { f := rv.Type().Field(i) fmt.Printf("field name: %s, tag: %v\n", f.Name, f.Tag.Get("json")) } } フィールドがPublicかPrivateか判定 IsExported()を使う encoding/json でも使っている。 func main() { a(reflect.ValueOf(struct{ A string }{A: "aaaa"})) // exported: true a(reflect.ValueOf(struct{ a string }{a: "aaaa"})) // exported: false } func a(rv reflect.Value) { for i := 0; i < rv.NumField(); i++ { f := rv.Type().Field(i) fmt.Printf("field name: %s, exported: %v", f.Name, f.IsExported()) } } interface{} interface{}を返すやつ (TODO: 使ってないので雑) func a(rv reflect.Value) { fmt.Println(rv.Interface()) } reflect.Valueに値を設定する reflectを使用して元の値を書き換えるためには https://go.dev/blog/laws-of-reflection の第3法則 で説明されている内容が大事なポイント。 To modify a reflection object, the value must be settable. 上記のように書かれています。 DeepL翻訳様様で翻訳した内容↓ Reflectionオブジェクトを変更するには、その値が設定可能でなければなりません 詳細な内容は https://go.dev/blog/laws-of-reflection を見たほうが良い。 一応雑に書くとこんな感じ 以下のコードはxのコピーがreflect.ValueOfに渡るため reflect.ValueOfの引数として生成されるインターフェース値はxそのものではない。 var x float64 = 3.4 v := reflect.ValueOf(x) そのため以下のコードがもし成功したとしても、元の変数xの値は変わることはない。 そのような挙動は混乱を招き、役に立たないため違反であるため、panicが発生する。 v.SetFloat(7.1) 値が設定可能であるかを判定するには CanSet() メソッドを使用すれば良い。 var x float64 = 3.4 v := reflect.ValueOf(x) fmt.Println(v.CanSet()) // false 値が設定可能であるとするためには、ValueOf()に変数xのポインタを渡す必要がある。 var x float64 = 3.4 v := reflect.ValueOf(&x) // --> set pointer fmt.Println(v.CanSet()) // false さらにポインタの参照先のValueを取得するためにElem()を使用することで、値が設定可能になります。 var x float64 = 3.4 v := reflect.ValueOf(&x) fmt.Println(v.Elem().CanSet()) // true Set 型を意識せずに値を設定したいだけならSet()を呼び出せば良い。 func a(rv reflect.Value, v interface{}) { src := reflect.ValueOf(v) rv.Elem().Set(src) } SetXXX Set()ではなく、型に対応したメソッドを使うという方法もある。 設定したい型に対応するメソッドが用意されているので実行すれば良い。 func a(dist reflect.Value, v interface{}) { rv := reflect.ValueOf(v) switch rv.Kind() { case reflect.Bool: dist.SetBool(rv.Bool()) case reflect.Int, reflect.Int64, reflect.Int32, reflect.Int16, reflect.Int8: dist.SetInt(rv.Int()) case reflect.Uint, reflect.Uint64, reflect.Uint32, reflect.Uint16, reflect.Uint8: dist.SetUint(rv.Uint()) case reflect.Float32, reflect.Float64: dist.SetFloat(rv.Float()) case reflect.String: dist.SetString(rv.String()) default: fmt.Printf("other(%s)", rv.Kind()) } } Arrayの各indexに値を設定 Index().Set()を使う。 当然だがlen(dist) < len(src)の場合はpanic(reflect: array index out of range)が発生するので ちゃんとやるなら双方のLen()のチェックを行うべきである。 func main() { var s [3]string rv := reflect.ValueOf(&s) fmt.Println(s) // ["", "", ""] a(rv, []string{"a", "b"}) fmt.Println(s) // ["a", "b", ""] } func a(rv reflect.Value, src []string) { dist := rv.Elem() for i, s := range src { dist.Index(i).Set(reflect.ValueOf(s)) } } Sliceに値を追加 reflect.AppendSlice()を使用し、戻り値をSet()に渡す。 reflect.AppendSlice()はSlice同士の結合を行うため、第2引数にはSliceをreflect.ValueOf()した結果を渡す必要がある。 func main() { var s []string rv := reflect.ValueOf(&s) fmt.Println(s) // [] a(rv, []string{"a", "b", "c"}) fmt.Println(s) // ["a", "b", "c"] a(rv, []string{"1", "2", "3"}) fmt.Println(s) // ["a", "b", "c", "1", "2", "3"] } func a(rv reflect.Value, src []string) { dist := rv.Elem() dist.Set(reflect.AppendSlice(dist, reflect.ValueOf(src))) } Mapのキーを指定して値を設定 SetMapIndex()にキーと値のValueを設定する。 func main() { s := map[string]string{} rv := reflect.ValueOf(&s) fmt.Println(s) // map[] a(rv, map[string]string{"a": "hoge", "b": "fuga"}) fmt.Println(s) // map[a:hoge b:fuga] a(rv, map[string]string{"b": "mage", "c": "age"}) fmt.Println(s) // map[a:hoge b:mage c:age] } func a(rv reflect.Value, src map[string]string) { dist := rv.Elem() for key, val := range src { dist.SetMapIndex(reflect.ValueOf(key), reflect.ValueOf(val)) } } 値を初期化する Arrayを作る reflect.ArrayOf()を使う。 例えば[5]stringを作りたい場合は以下のようになる。 reflect.New()はreflect.Ptr型のreflect.Valueを返すので、Elem()を使用しする必要がある。 rv := reflect.New(reflect.ArrayOf(5, reflect.TypeOf(""))) fmt.Printf("%#v\n", rv) // &[5]string{"", "", "", "", ""} fmt.Printf("%#v\n", rv.Elem()) // [5]string{"", "", "", "", ""} Sliceを作る reflect.MakeSlice()を使う。 make()と同様にlen, capを指定して使う。 s := []string{} rv := reflect.MakeSlice(reflect.TypeOf(s), 5, 10) fmt.Printf("%#v\n", rv) // []string{"", "", "", "", ""} fmt.Printf("len: %d\n", rv.Len()) // len: 5 fmt.Printf("cap: %d\n", rv.Cap()) // cap: 10 } Mapを作る reflect.MakeMapを使う。 s := map[string]string{} rv := reflect.MakeMap(reflect.TypeOf(s)) fmt.Printf("%#v\n", rv) // map[string]string{} 構造体を作る reflect.New()に構造体のTypeを渡す。 s := struct { A string B int }{} rv := reflect.New(reflect.TypeOf(s)) fmt.Printf("%#v\n", rv) // &struct { A string; B int }{A:"", B:0} fmt.Printf("%#v\n", rv.Elem()) // struct { A string; B int }{A:"", B:0} }