20220223のGoに関する記事は5件です。

【Go】gormでテーブル名と構造体の名前が違う時のtips

Go難しいです。gormは「なんでやねん」と思うことが多いです。 元Railsエンジニアには早すぎた技術なのかもしれません? Many To Many周りの公式の記事が少しわかりにくくて、tipsを書いておきます Many To Many (many2many) gormではPreload()する際に、構造体のフィールドのタグ(reflect.StructTag)を仕込むことで、中間テーブルを簡単にORM側に理解させることができます。 普通の書き方 gorm内部的には、Fieldという構造体でTagSettings map[string]stringで値を持っていて、全部大文字に変換してるのでmap[MANY2MANY:many2many:user_languages]という感じで保存してあります // usersテーブル type User struct { ID uint Languages []*Language `gorm:"many2many:user_languages;"` } // languagesテーブル type Language struct { ID uint Name string } // 中間テーブルの構造体を定義せずに、Eager Loadingが実行できる DB.Model(&User{}).Preload("Languages") テーブル名と構造体名が違う書き方 こういう書き方がGo的に良いか悪いかどうかは置いといて? Languageの構造体にフィールドを追加したくなくて、Languageと同じフィールドを持つLanguageWithOwnを作成した時の、many2manyの書き方で foreignKey, joinForeignKey, References, joinReferencesの4つを設定する必要があります 前述の通り、大文字小文字は内部で吸収されているので関係ないです。 // usersテーブル type User struct { ID uint Languages []*OwnLanguage `gorm:"many2many:user_languages;foreignKey:id;joinForeignKey:user_id;References:id;joinReferences:language_id;"` } // foreignKey:id // joinForeignKey:user_id // References:id // joinReferences:language_id // languagesテーブル type LanguageWithOwn struct { ID uint Name string Own bool // 母国語かどうか } // 構造体とテーブル名を一致させる func (l *LanguageWithOwn) TableName() string { return "languages" } // Eager Loadingが実行され、User.Languagesに値が入る DB.Model(&User{}).Preload("Languages") あんまGoのライブラリのソース読まないけど、jsのライブラリとテイストは似てるなと思った(所感)
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

[Golang] [標準logライブラリ] GCPの構造化ログにX-Cloud-Trace-ContextのtraceIdをセットする

やったこと 前回の記事で下記の課題があった。それを改善したのが本記事の内容。 サービス運用では同時に複数のアクセスが考えられるため、traceId等でログを追跡できるようにする必要があります。アクセスがたまにしかないようなフェーズではいいかもしれないけど、X-Cloud-Trace-Contextを使用してリクエストとログを一意にできるようにすることは必須だと思うので今後やる。 はてなブログに掲載しています。
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Go の Gorm 使う

はじめに Goを勉強し始めたということで、O/Rマッパーも体験しておきます。 今回は、Go で API 作るの続きとして、Gormを入れて試してみたいと思います。 早速作ってみる 今回もこれをベースに組み込んでいきます。 Go で API 作るの続きなので、API部分はginを使います。 ※ginの使い方は、上記Qiita記事で紹介しているので割愛します。 Gorm を取得する Dockerで開発しているので、docker composeコマンドとなります。 DBはPostgreSQLを使ってみます。 $ docker compose run --rm back go get -u gorm.io/gorm $ docker compose run --rm back go get -u gorm.io/driver/postgres DB と接続 取得したgormとpostgresをimport cmd/main.go import ( + "gorm.io/driver/postgres" + "gorm.io/gorm" ) DBと接続するメソッドを作成します。 cmd/main.go func gormConnect() *gorm.DB { dsn := os.Getenv("DATABASE_URL") db, _ := gorm.Open(postgres.Open(dsn), &gorm.Config{}) db.AutoMigrate(&Todo{}) return db } Herokuにデプロイも試したかったので、dsnは、DATABASE_URLの環境変数から取るようにしました。 DATABASE_URL=host=postgres user=postgres password=password dbname=go-todo-api-db port=5432 sslmode=disable TimeZone=Asia/Tokyo todosテーブルは、AutoMigrateを使って作られるようにしました。 db.AutoMigrate(&Todo{}) todosテーブルはこんな感じで定義しています。 cmd/main.go type Todo struct { gorm.Model Title string } リスト取得(GET: /api/todos)を作ってみる 上記のようにルートを用意し、getTodosメソッドが呼ばれるようにします。 Railsで言う、Todo.allのように、DBから取得します。 cmd/main.go func getTodos(c *gin.Context) { var todos [] Todo db := gormConnect() db.Find(&todos) c.JSON(http.StatusOK, gin.H { "todos": todos }) } Findメソッドを使います。 データが空なので、何も帰ってこないと分かりづらいです。 試しに2つデータを登録して、GETで/api/todosを叩いてみました。 todosテーブルを作成する際に使っているTodoの型には、Titleカラムしか指定していないのに、ID、CreatedAt、UpdatedAt、DeletedAtカラムが追加されています。 これは、gorm.Modelに定義されているそうです。 デフォルトでIDカラムは主キーで、自動でインクリメントもしてくれました。 ちなみにIDカラム以外の独自に定義したカラムを主キー且つ、自動インクリメントしたい場合は、以下のようにすると出来ました。 Id uint `gorm:"primaryKey;autoIncrement` Title string リスト追加(POST: /api/todos)を作ってみる 上記のようにルートを用意し、addTodoメソッドが呼ばれるようにします。 cmd/main.go func addTodo(c *gin.Context) { type InputTodo struct { Title string } var inputTodo InputTodo c.BindJSON(&inputTodo) newTodo := Todo { Title: inputTodo.Title } db := gormConnect() db.Create(&newTodo) c.JSON(http.StatusOK, gin.H { "todo": newTodo }) } Createメソッドを使います。 リクエストを投げるだけでは分かりづらいので、追加したTodoをレスポンスとして返すようにしてみました。 POSTで/api/todosを叩いてみます。 IDも自動インクリメントされているし、渡したTitleが入ったデータが作られていそうです。 リスト更新(PATCH: /api/todos/:ID)を作ってみる 上記のようにルートを用意し、updateTodoメソッドが呼ばれるようにします。 ※パラメタの受け取り方をIdからIDに変更しました。(r.PATCH("/api/todos/:ID", updateTodo)) cmd/main.go func updateTodo(c *gin.Context) { type InputTodo struct { Title string } Id := c.Param("ID") id, _ := strconv.Atoi(Id) db := gormConnect() var todo Todo db.First(&todo, id) var inputTodo InputTodo c.BindJSON(&inputTodo) db.Model(&todo).Update("Title", inputTodo.Title) c.JSON(http.StatusOK, gin.H { "todo": todo }) } Updateメソッドを使います。 FirstやFindで取得した後、カラム毎に値を代入して、Saveもできるそうです。 PATCHで/api/todos/1を叩いてみます。 渡したTitleでIDが1のレコードが更新されていそうです。 リスト削除(DELETE: /api/todos/:ID)を作ってみる 上記のようにルートを用意し、deleteTodoメソッドが呼ばれるようにします。 ※パラメタの受け取り方をIdからIDに変更しました。(r.DELETE("/api/todos/:ID", deleteTodo) cmd/main.go func deleteTodo(c *gin.Context) { Id := c.Param("ID") id, _ := strconv.Atoi(Id) db := gormConnect() var todo Todo db.Delete(&todo, id) } Deleteメソッドを使います。 DELETEで/api/todos/1を叩いてみます。 削除されたことが分かりづらいので、GETで/api/todosも叩いてみます。 IDが1のレコードが削除されていそうです。 おまけ(Cross-Domain問題) 今回紹介した方法で、リクエストを送っているツールは、Insomniaを使っています。 仮に、フロントエンドアプリからリクエストを送るとしたら、Cross-Domain問題が発生すると思います。 その場合は、corsの設定を行います。 $ docker compose run --rm back go get -u github.com/gin-contrib/cors import ( + "github.com/gin-contrib/cors" ) r := gin.Default() +r.Use(cors.New(cors.Config{ + AllowOrigins: []string{os.Getenv("CORS_ORIGIN")}, + AllowMethods: []string{"PUT", "PATCH", "DELETE"}, +})) GETとPOSTまでは問題なかったですが更新のPATCHあたりからこの問題に直面しました。 適宜、使用するHTTPリクエストメソッドを追加してください。 おまけ(HerokuのGoのバージョン認識) 今回使用しているGoのバージョンは、1.17.1です。 筆者は、DockerをそのままHerokuにデプロイしていますが、ライブラリインストールでエラーが発生しました。 ../codon/tmp/cache/go-path/pkg/mod/gorm.io/gorm@v1.23.1/logger/sql.go:105:45: rv.IsZero undefined (type reflect.Value has no field or method IsZero) note: module requires Go 1.14 Goのバージョンを1.14以降にしたら良さそうですが、1.17.1の想定です。 go.modにも1.17と記載されています。 HerokuのBuild Logを遡ってみると、1.12.17を使っていることが判明 -----> !! The go.mod file for this project does not specify a Go version !! !! Defaulting to go1.12.17 !! !! For more details see: https://devcenter.heroku.com/articles/go-apps-with-modules#build-configuration !! -----> Using go1.12.17 Herokuの環境変数に、GOVERSION=1.17.1を追加すると解決しました。 おわりに 今回のソースはこちらのGitHubに上げてます。 参考文献 GORMガイド Go言語でORM触ってみた - Qiita
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

goでutf8メールを送信する

Goでメールを送信することは簡単です。net/smtpのSendMail()で送信できます。しかし、これは平文の送信に限った話です。 マルチバイト文字である日本語を平文のまま送信してしまうと、メーラーによっては文字化けが発生します。よって、utf8エンコーディングしてから送信する必要が出てきます。 メールを送信するには、お決まりのインターネットメッセージフォーマットを守る必要があります。そこで決まっているのが、 RFC2822 という文書です。ここで特に気にしないといけないのが、一行あたりの文字数です。翻訳してみると、 There are two limits that this standard places on the number of characters in a line. Each line of characters MUST be no more than 998 characters, and SHOULD be no more than 78 characters, excluding the CRLF. この標準では、1行の文字数に2つの制限があります。 文字の各行は998文字以下である必要があり、CRLFを除いて78文字以下である必要があります。 Google翻訳が意味不明ですが、 CRLF を除いて、各行は 998 文字を超えてはならならず(MUST)、78 文字を超えるべきではない(SHOULD)。 という意味になります。要するに「一行の文字数はは78を超えるな」ということです。 上記規約を守るように、ヘッダと本文を適度に改行したソースを下記に示しています。 go で utf8メールを送信をめちゃくちゃ参考にしています。 上記記事は最終更新からだいぶ時間が経っていたので、検証を含め見やすくリファクタリングしています。 そして、タイトルと本文が文字化けせず送信できることも確認しています。 // メール一行で超えるべきではない文字数 // https://datatracker.ietf.org/doc/html/rfc2822#section-2.1.1 const ( charactorLimitForOneLineOfEmail = 78 smtpServer = "smtp.example.com:25" ) func main() { to := "to@example.com" from := "from@example.com" subject := "タイトル" body := "本文" sendEmail(to, from, subject, body) } // 適切な長さにカットしCRLFを挿入 func cutAndAddCrlf(msg string) string { buffer := bytes.Buffer{} for k, c := range strings.Split(msg, "") { buffer.WriteString(c) if (k+1)%charactorLimitForOneLineOfEmail == 0 { buffer.WriteString("\r\n") } } return buffer.String() } // UTF8文字列を指定文字数で分割 func utf8Split(utf8string string, length int) []string { result := []string{} buffer := bytes.Buffer{} for k, c := range strings.Split(utf8string, "") { buffer.WriteString(c) if (k+1)%length == 0 { result = append(result, buffer.String()) buffer.Reset() } } if buffer.Len() > 0 { result = append(result, buffer.String()) } return result } // タイトルをMIMEエンコード func encodeSubject(subject string) string { buffer := bytes.Buffer{} buffer.WriteString("Subject:") limit := charactorLimitForOneLineOfEmail / 6 // Unicodeでは一文字が最大6バイトになるため for _, line := range utf8Split(subject, limit) { buffer.WriteString(" =?utf-8?B?") buffer.WriteString(base64.StdEncoding.EncodeToString([]byte(line))) buffer.WriteString("?=\r\n") } return buffer.String() } // ヘッダを作る func makeHeader(from, to, subject string) bytes.Buffer { header := bytes.Buffer{} header.WriteString(fmt.Sprintf("From: %s\r\n", from)) header.WriteString(fmt.Sprintf("To: %s\r\n", to)) header.WriteString(encodeSubject(subject)) header.WriteString("MIME-Version: 1.0\r\n") header.WriteString("Content-Type: text/plain; charset=\"utf-8\"\r\n") header.WriteString("Content-Transfer-Encoding: base64\r\n") return header } // 設定したメールアドレスは To にセットされる func SendEmail(to string, from string, subject string, body string) error { header := makeHeader(from, to, subject) encodedBody := base64.StdEncoding.EncodeToString([]byte(body)) message := header message.WriteString(cutAndAddCrlf(encodedBody)) auth := getAuth(config.GetConf().Email) err := smtp.SendMail(smtpServer, auth, from, []string{to}, message.Bytes()) if err != nil { logger.Errorln(err) return err } return nil }
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

ポインタについて

最近Goを触り始めました。右も左も分からない状況ですが、今日学んだことをアウトプットしていきます。 ポインタ めちゃ大事そうだったのでまとめようとおもいます。まず、これを理解するためにはアドレスを理解しなければなりません。 コンピュータにはメモリという作業場があって、変数はこのメモリに格納されているんですね。その格納されている場所をアドレスというんです。Goではアドレスをポインタと呼んでいます。 つまり、アドレス=ポインタ 実際にポインタを取得してみましょう test.go package main import "fmt" func main() { name := "what is golang?" fmt.Println(&name) } => 0xc000042230 Printlnの後に、&変数名と書くことでポインタを取得できます。 それでは今度はポインタを変数に代入してみましょう。 test.go package main import "fmt" func main() { name := "what is golang?" var testname *string = &name fmt.Println(testname) } => 0xc000042230 ポインタ型変数を定義するには、データ型に「*」をつけて、&変数名を右辺に書けばオッケです。 ポインタに入れたポインタの実際の値を出力してみましょう。 package main import "fmt" func main() { name := "what is golang?" var testname *string = &name fmt.Println(testname) fmt.Println(*testname) } => 0xc000042230 what is golang? 以上です。何か間違いがございましたら、ご教示いただけますと幸いです。 【参考資料】
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む