20210724のGoに関する記事は10件です。

Amazon ComprehendをAWS SDK for Goを使って呼び出す

はじめに AWS SDK for Goがありますが、現在(2021年7月24日)のところ、AWS SDK for Go v2のAmazon Comporehendのコードサンプルがなかったので、とりあえず自分で作ってみることにしました。 個人的にはPythonを使用することが多いのですが、Go言語も業務で使用することがあるので、Go言語でもチャレンジしてみました。 ※他の言語や他のサービスのコードサンプルは以下から参照できます。 Developer Guide/Code Examples awsdocs/aws-doc-sdk-examples Amazon Comprehendを使用するための事前準備 Go言語のプログラムをLinuxで実行してAmazon Comprehendのエンティティ認識のバッチを呼び出して結果をダウンロードして表示させることを考えます。オンラインのAPI呼び出しではなく、バッチ処理にしたのはAPI呼び出しだと文字数に(少し)きつい制限があるためです。エンティティ認識の処理だと、UTF-8のエンコーディングで5,000バイトが処理の上限です。 Amazon Comprehendを使用するには事前準備として権限の付与とロールの定義が必要です。 コマンド(Goのソースコードをコンパイルしたもの)を実行するユーザ(aws configureで設定したアクセスキーやシークレットキーを持つユーザ)にAmazon Comprehendの呼び出しを許可する コマンドを実行するユーザにAWS のサービスにロールを渡すアクセス権限を付与する。(こちらを参考にしてください。) Amazon Comprehendに渡すロールを定義する。(こちらを参考にしてください。) 処理の流れ 処理対象ファイルをS3にアップロードする。 Amazon Comprehendを呼び出す(エンティティ認識のバッチ処理) Amazon Comprehendのジョブの完了を一定間隔で確認する。 処理結果のtar.gzファイルをダウンロードする。 tar.gzファイルに含まれる結果のJSONからScoreの高い順にエンティティのキーワードと種別、スコアの値を表示する(デフォルトでは20個) ソースコード(Go言語) ソースコードは以下のとおりです。(GitHubにも置いてあります。) 43行目の「roleArn string = "arn:aws:iam::123456789012:role/comprehend-access-role"」のところは、事前準備で定義した「Amazon Comprehendに渡すロール」のARNを指定します。 main.go package main import ( "archive/tar" "bufio" "compress/gzip" "context" "encoding/json" "flag" "fmt" "io" "net/url" "os" "sort" "time" "github.com/aws/aws-sdk-go-v2/config" "github.com/aws/aws-sdk-go-v2/service/comprehend" comprehend_types "github.com/aws/aws-sdk-go-v2/service/comprehend/types" "github.com/aws/aws-sdk-go-v2/service/s3" ) type Entity struct { BeginOffset int `json:"BeginOffset"` EndOffset int `json:"EndOffset"` Score float64 `json:"Score"` Text string `json:"Text"` Type string `json:"Type"` } type JsonOutput struct { File string `json:"File"` Entities []Entity `json:"Entities"` } const ( resultFileName string = "output" bufferSize int = 1024 * 1024 interval int = 10 timeoutCount int = 100 jobName string = "sample-entities-detection-job" // ここのロールは置き換えること roleArn string = "arn:aws:iam::123456789012:role/comprehend-access-role" ) func main() { pBucketName := flag.String("bucket", "testcomprehend-tn", "Bucket to put a content on.") pPrefixName := flag.String("prefix", "comprehend/", "Prefix to store a content file") pContentFileName := flag.String("file", "content.txt", "The content file") pLimitNumber := flag.Int("limit", 20, "The limit to display keywords") flag.Parse() fmt.Println("START!!!!") fmt.Println("Bucket:", *pBucketName) fmt.Println("Prefix:", *pPrefixName) fmt.Println("Limit:", *pLimitNumber) fmt.Println("File:", *pContentFileName) cfg, err := config.LoadDefaultConfig(context.TODO()) if err != nil { panic("configuration error, " + err.Error()) } s3Client := s3.NewFromConfig(cfg) comprehendClient := comprehend.NewFromConfig(cfg) // 処理対象ファイルをS3にアップロードする file, err := os.Open(*pContentFileName) if err != nil { fmt.Println("Unable to open file " + *pContentFileName) return } defer file.Close() objectName := *pPrefixName + "input/" + *pContentFileName fmt.Println("Object Name: " + objectName) input := &s3.PutObjectInput{ Bucket: pBucketName, Key: &objectName, Body: file, } _, err = s3Client.PutObject(context.TODO(), input) if err != nil { fmt.Println("Got error uploading file:") fmt.Println(err) return } // Amazon Comprehend 呼び出し inputS3Uri := "s3://" + *pBucketName + "/" + objectName fmt.Println("InputS3URI: " + inputS3Uri) inputConfig := &comprehend_types.InputDataConfig{ S3Uri: &inputS3Uri, InputFormat: comprehend_types.InputFormatOneDocPerFile, } outputS3Uri := "s3://" + *pBucketName + "/" + *pPrefixName + "output/" fmt.Println("OutputS3URI: " + outputS3Uri) outputConfig := &comprehend_types.OutputDataConfig{ S3Uri: &outputS3Uri, } roleArnForInput := roleArn jobNameForInput := jobName jobInput := &comprehend.StartEntitiesDetectionJobInput{ DataAccessRoleArn: &roleArnForInput, InputDataConfig: inputConfig, LanguageCode: comprehend_types.LanguageCodeJa, OutputDataConfig: outputConfig, JobName: &jobNameForInput, } out, err := comprehendClient.StartEntitiesDetectionJob(context.TODO(), jobInput) if err != nil { fmt.Println("Starting an entities detection job Error:") fmt.Println(err) return } jobId := *out.JobId fmt.Println("Job ID: " + jobId) // Amazon Comprehend 完了確認 describeJobInput := &comprehend.DescribeEntitiesDetectionJobInput{ JobId: &jobId, } var outDesc *comprehend.DescribeEntitiesDetectionJobOutput for i := 0; i < timeoutCount; i++ { fmt.Println("In Progress...") time.Sleep(time.Duration(interval) * time.Second) outDesc, err = comprehendClient.DescribeEntitiesDetectionJob(context.TODO(), describeJobInput) if err != nil { fmt.Println("Getting a status of the entities detection job Error:") fmt.Println(err) return } if outDesc.EntitiesDetectionJobProperties.JobStatus == comprehend_types.JobStatusCompleted { fmt.Println("Job Completed.") break } } if outDesc.EntitiesDetectionJobProperties.JobStatus != comprehend_types.JobStatusCompleted { fmt.Println("Job Timeout.") return } // Amazon Comprehend 結果ダウンロード downloadS3Uri := *outDesc.EntitiesDetectionJobProperties.OutputDataConfig.S3Uri fmt.Println("Output S3 URI: " + downloadS3Uri) parsedUri, err := url.Parse(downloadS3Uri) if err != nil { fmt.Println("URI Parse Error:") fmt.Println(err) return } downloadObjectName := parsedUri.Path[1:] fmt.Println("Download Object Name: " + downloadObjectName) getObjectInput := &s3.GetObjectInput{ Bucket: pBucketName, Key: &downloadObjectName, } outGetObject, err := s3Client.GetObject(context.TODO(), getObjectInput) if err != nil { fmt.Println("Getting S3 object Error:") fmt.Println(err) return } defer outGetObject.Body.Close() // tar.gzファイルから結果のJSONを取り出す gzipReader, err := gzip.NewReader(outGetObject.Body) if err != nil { fmt.Println("Reading a gzip file Error:") fmt.Println(err) return } defer gzipReader.Close() tarfileReader := tar.NewReader(gzipReader) var jsonOutput JsonOutput for { tarfileHeader, err := tarfileReader.Next() if err == io.EOF { break } if err != nil { fmt.Println("Reading a tar file Error:") fmt.Println(err) return } if tarfileHeader.Name == resultFileName { jsonReader := bufio.NewReaderSize(tarfileReader, bufferSize) for { // TODO: isPrefixを見て、行がバッファに対して長すぎる場合の処理を行なう line, _, err := jsonReader.ReadLine() if err == io.EOF { break } if err != nil { fmt.Println("Reading a line Error:") fmt.Println(err) return } json.Unmarshal([]byte(line), &jsonOutput) fmt.Println() fmt.Println("File: " + jsonOutput.File) // Score順にソートする entities := jsonOutput.Entities sort.Slice(entities, func(i, j int) bool { return entities[i].Score > entities[j].Score }) // Amazon Comprehend 結果表示 for i := 0; i < *pLimitNumber; i++ { fmt.Printf("%s (%s): %f\n", entities[i].Text, entities[i].Type, entities[i].Score) } } } } fmt.Println() fmt.Println() fmt.Println("END!!!!") } go buildでビルドできます。また、以下のコマンドでオプション(引数)を確認できます。-fileの指定が解析したいファイルになります。デフォルトはcontent.txtという名前です。UTF-8で文章を格納してください。 ./mycomprehend -h Usage of ./mycomprehend: -bucket string Bucket to put a content on. (default "testcomprehend-tn") -file string The content file (default "content.txt") -limit int The limit to display keywords (default 20) -prefix string Prefix to store a content file (default "comprehend/") 実行結果 content.txtについてエンティティ認識の処理をした結果は以下のようになりました。 (前略) File: content.txt 2020年12月 (DATE): 0.999014 3時間 (QUANTITY): 0.997277 Linux Academy (ORGANIZATION): 0.995822 3年 (QUANTITY): 0.995796 180分 (QUANTITY): 0.995555 2回 (QUANTITY): 0.995218 33,000円 (QUANTITY): 0.993613 https://qiita.com/takanattie/items/7dd188ce14a2a5b9ef14 (OTHER): 0.990118 1度 (QUANTITY): 0.988592 3ヶ月半 (QUANTITY): 0.988081 AWS (ORGANIZATION): 0.987514 AWS (ORGANIZATION): 0.980833 U.S. (ORGANIZATION): 0.976798 AWS (ORGANIZATION): 0.976790 AWS (ORGANIZATION): 0.975587 3年間 (QUANTITY): 0.975035 2周 (QUANTITY): 0.972928 Linux Academy (ORGANIZATION): 0.966450 AWS (ORGANIZATION): 0.966190 Qiita Advent Calendar 2020 (TITLE): 0.962282 認識されたエンティティのタイプ(ORGANIZATIONなど)の情報もあるので、そのタイプでフィルタリングすることもできると思います。 補足 上記のコードは、サンプルとなるように書いたので不充分な点があります。特に以下の点は改善の余地があると考えています。 コマンドの戻り値(Exit Code)を設定する S3にAmazon Comprehendの結果が格納されるので、それをトリガとしてLambdaを起動して結果を取り出す、などの処理を行なうようにする。(イベントドリブンにする) また、今回はエンティティ認識だけでしたが、キーフレーズ抽出なども同じように実行できるはずです(コードの修正は必要になりますが)。APIの定義はソースコードとしてGitHubにもありますし、APIのドキュメントもありますので、そこを読み解けばコードをかけるはずですが、やはり一連の流れを示したサンプルコードが欲しいところです。 以上。
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Amazon ComprehendをAWS SDK for Go v2を使って呼び出す

はじめに AWS SDK for Goがありますが、現在(2021年7月24日)のところ、AWS SDK for Go v2のAmazon Comporehendのコードサンプルがなかったので、とりあえず自分で作ってみることにしました。 個人的にはPythonを使用することが多いのですが、Go言語も業務で使用することがあるので、Go言語でもチャレンジしてみました。 ※他の言語や他のサービスのコードサンプルは以下から参照できます。 Developer Guide/Code Examples awsdocs/aws-doc-sdk-examples Amazon Comprehendを使用するための事前準備 Go言語のプログラムをLinuxで実行してAmazon Comprehendのエンティティ認識のバッチを呼び出して結果をダウンロードして表示させることを考えます。オンラインのAPI呼び出しではなく、バッチ処理にしたのはAPI呼び出しだと文字数に(少し)きつい制限があるためです。エンティティ認識の処理だと、UTF-8のエンコーディングで5,000バイトが処理の上限です。 Amazon Comprehendを使用するには事前準備として権限の付与とロールの定義が必要です。 コマンド(Goのソースコードをコンパイルしたもの)を実行するユーザ(aws configureで設定したアクセスキーやシークレットキーを持つユーザ)にAmazon Comprehendの呼び出しを許可する コマンドを実行するユーザにAWS のサービスにロールを渡すアクセス権限を付与する。(こちらを参考にしてください。) Amazon Comprehendに渡すロールを定義する。(こちらを参考にしてください。) 処理の流れ 処理対象ファイルをS3にアップロードする。 Amazon Comprehendを呼び出す(エンティティ認識のバッチ処理) Amazon Comprehendのジョブの完了を一定間隔で確認する。 処理結果のtar.gzファイルをダウンロードする。 tar.gzファイルに含まれる結果のJSONからScoreの高い順にエンティティのキーワードと種別、スコアの値を表示する(デフォルトでは20個) ソースコード(Go言語) ソースコードは以下のとおりです。(GitHubにも置いてあります。) 43行目の「roleArn string = "arn:aws:iam::123456789012:role/comprehend-access-role"」のところは、事前準備で定義した「Amazon Comprehendに渡すロール」のARNを指定します。 main.go package main import ( "archive/tar" "bufio" "compress/gzip" "context" "encoding/json" "flag" "fmt" "io" "net/url" "os" "sort" "time" "github.com/aws/aws-sdk-go-v2/config" "github.com/aws/aws-sdk-go-v2/service/comprehend" comprehend_types "github.com/aws/aws-sdk-go-v2/service/comprehend/types" "github.com/aws/aws-sdk-go-v2/service/s3" ) type Entity struct { BeginOffset int `json:"BeginOffset"` EndOffset int `json:"EndOffset"` Score float64 `json:"Score"` Text string `json:"Text"` Type string `json:"Type"` } type JsonOutput struct { File string `json:"File"` Entities []Entity `json:"Entities"` } const ( resultFileName string = "output" bufferSize int = 1024 * 1024 interval int = 10 timeoutCount int = 100 jobName string = "sample-entities-detection-job" // ここのロールは置き換えること roleArn string = "arn:aws:iam::123456789012:role/comprehend-access-role" ) func main() { pBucketName := flag.String("bucket", "testcomprehend-tn", "Bucket to put a content on.") pPrefixName := flag.String("prefix", "comprehend/", "Prefix to store a content file") pContentFileName := flag.String("file", "content.txt", "The content file") pLimitNumber := flag.Int("limit", 20, "The limit to display keywords") flag.Parse() fmt.Println("START!!!!") fmt.Println("Bucket:", *pBucketName) fmt.Println("Prefix:", *pPrefixName) fmt.Println("Limit:", *pLimitNumber) fmt.Println("File:", *pContentFileName) cfg, err := config.LoadDefaultConfig(context.TODO()) if err != nil { panic("configuration error, " + err.Error()) } s3Client := s3.NewFromConfig(cfg) comprehendClient := comprehend.NewFromConfig(cfg) // 処理対象ファイルをS3にアップロードする file, err := os.Open(*pContentFileName) if err != nil { fmt.Println("Unable to open file " + *pContentFileName) return } defer file.Close() objectName := *pPrefixName + "input/" + *pContentFileName fmt.Println("Object Name: " + objectName) input := &s3.PutObjectInput{ Bucket: pBucketName, Key: &objectName, Body: file, } _, err = s3Client.PutObject(context.TODO(), input) if err != nil { fmt.Println("Got error uploading file:") fmt.Println(err) return } // Amazon Comprehend 呼び出し inputS3Uri := "s3://" + *pBucketName + "/" + objectName fmt.Println("InputS3URI: " + inputS3Uri) inputConfig := &comprehend_types.InputDataConfig{ S3Uri: &inputS3Uri, InputFormat: comprehend_types.InputFormatOneDocPerFile, } outputS3Uri := "s3://" + *pBucketName + "/" + *pPrefixName + "output/" fmt.Println("OutputS3URI: " + outputS3Uri) outputConfig := &comprehend_types.OutputDataConfig{ S3Uri: &outputS3Uri, } roleArnForInput := roleArn jobNameForInput := jobName jobInput := &comprehend.StartEntitiesDetectionJobInput{ DataAccessRoleArn: &roleArnForInput, InputDataConfig: inputConfig, LanguageCode: comprehend_types.LanguageCodeJa, OutputDataConfig: outputConfig, JobName: &jobNameForInput, } out, err := comprehendClient.StartEntitiesDetectionJob(context.TODO(), jobInput) if err != nil { fmt.Println("Starting an entities detection job Error:") fmt.Println(err) return } jobId := *out.JobId fmt.Println("Job ID: " + jobId) // Amazon Comprehend 完了確認 describeJobInput := &comprehend.DescribeEntitiesDetectionJobInput{ JobId: &jobId, } var outDesc *comprehend.DescribeEntitiesDetectionJobOutput for i := 0; i < timeoutCount; i++ { fmt.Println("In Progress...") time.Sleep(time.Duration(interval) * time.Second) outDesc, err = comprehendClient.DescribeEntitiesDetectionJob(context.TODO(), describeJobInput) if err != nil { fmt.Println("Getting a status of the entities detection job Error:") fmt.Println(err) return } if outDesc.EntitiesDetectionJobProperties.JobStatus == comprehend_types.JobStatusCompleted { fmt.Println("Job Completed.") break } } if outDesc.EntitiesDetectionJobProperties.JobStatus != comprehend_types.JobStatusCompleted { fmt.Println("Job Timeout.") return } // Amazon Comprehend 結果ダウンロード downloadS3Uri := *outDesc.EntitiesDetectionJobProperties.OutputDataConfig.S3Uri fmt.Println("Output S3 URI: " + downloadS3Uri) parsedUri, err := url.Parse(downloadS3Uri) if err != nil { fmt.Println("URI Parse Error:") fmt.Println(err) return } downloadObjectName := parsedUri.Path[1:] fmt.Println("Download Object Name: " + downloadObjectName) getObjectInput := &s3.GetObjectInput{ Bucket: pBucketName, Key: &downloadObjectName, } outGetObject, err := s3Client.GetObject(context.TODO(), getObjectInput) if err != nil { fmt.Println("Getting S3 object Error:") fmt.Println(err) return } defer outGetObject.Body.Close() // tar.gzファイルから結果のJSONを取り出す gzipReader, err := gzip.NewReader(outGetObject.Body) if err != nil { fmt.Println("Reading a gzip file Error:") fmt.Println(err) return } defer gzipReader.Close() tarfileReader := tar.NewReader(gzipReader) var jsonOutput JsonOutput for { tarfileHeader, err := tarfileReader.Next() if err == io.EOF { break } if err != nil { fmt.Println("Reading a tar file Error:") fmt.Println(err) return } if tarfileHeader.Name == resultFileName { jsonReader := bufio.NewReaderSize(tarfileReader, bufferSize) for { // TODO: isPrefixを見て、行がバッファに対して長すぎる場合の処理を行なう line, _, err := jsonReader.ReadLine() if err == io.EOF { break } if err != nil { fmt.Println("Reading a line Error:") fmt.Println(err) return } json.Unmarshal([]byte(line), &jsonOutput) fmt.Println() fmt.Println("File: " + jsonOutput.File) // Score順にソートする entities := jsonOutput.Entities sort.Slice(entities, func(i, j int) bool { return entities[i].Score > entities[j].Score }) // Amazon Comprehend 結果表示 for i := 0; i < *pLimitNumber; i++ { fmt.Printf("%s (%s): %f\n", entities[i].Text, entities[i].Type, entities[i].Score) } } } } fmt.Println() fmt.Println() fmt.Println("END!!!!") } go buildでビルドできます。また、以下のコマンドでオプション(引数)を確認できます。-fileの指定が解析したいファイルになります。デフォルトはcontent.txtという名前です。UTF-8で文章を格納してください。 ./mycomprehend -h Usage of ./mycomprehend: -bucket string Bucket to put a content on. (default "testcomprehend-tn") -file string The content file (default "content.txt") -limit int The limit to display keywords (default 20) -prefix string Prefix to store a content file (default "comprehend/") 実行結果 content.txtについてエンティティ認識の処理をした結果は以下のようになりました。 (前略) File: content.txt 2020年12月 (DATE): 0.999014 3時間 (QUANTITY): 0.997277 Linux Academy (ORGANIZATION): 0.995822 3年 (QUANTITY): 0.995796 180分 (QUANTITY): 0.995555 2回 (QUANTITY): 0.995218 33,000円 (QUANTITY): 0.993613 https://qiita.com/takanattie/items/7dd188ce14a2a5b9ef14 (OTHER): 0.990118 1度 (QUANTITY): 0.988592 3ヶ月半 (QUANTITY): 0.988081 AWS (ORGANIZATION): 0.987514 AWS (ORGANIZATION): 0.980833 U.S. (ORGANIZATION): 0.976798 AWS (ORGANIZATION): 0.976790 AWS (ORGANIZATION): 0.975587 3年間 (QUANTITY): 0.975035 2周 (QUANTITY): 0.972928 Linux Academy (ORGANIZATION): 0.966450 AWS (ORGANIZATION): 0.966190 Qiita Advent Calendar 2020 (TITLE): 0.962282 認識されたエンティティのタイプ(ORGANIZATIONなど)の情報もあるので、そのタイプでフィルタリングすることもできると思います。 補足 上記のコードは、サンプルとなるように書いたので不充分な点があります。特に以下の点は改善の余地があると考えています。 コマンドの戻り値(Exit Code)を設定する S3にAmazon Comprehendの結果が格納されるので、それをトリガとしてLambdaを起動して結果を取り出す、などの処理を行なうようにする。(イベントドリブンにする) また、今回はエンティティ認識だけでしたが、キーフレーズ抽出なども同じように実行できるはずです(コードの修正は必要になりますが)。APIの定義はソースコードとしてGitHubにもありますし、APIのドキュメントもありますので、そこを読み解けばコードをかけるはずですが、やはり一連の流れを示したサンプルコードが欲しいところです。 以上。
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

GolangでFirebaseのFirestore Databaseを使う

はじめに FirebaseのFirestore Databaseに、GolangのSDKを用いて接続したい。 公式Docを参考にFirestoreへの接続を試みたところ、以下のように「ファイルが見つからない」と怒られたので、その解決策を備忘録として残す。 Init Firestore failed. err=cannot read credentials file: open ./firebase-adminsdk-key.json: no such file or directory" ちなみに、解決方法をググると、GOPATHまわりを良い感じに変更する方法がたくさん出てくるが、よくわからなかったので、本記事では別の方法を採用している。 (一応、記事後半に採用理由も記載済み) 失敗パターン プログラム自体は概ねこれで問題ない // このpackageはあくまで筆者の環境 package infrastructure import ( "context" "github.com/labstack/gommon/log" firebase "firebase.google.com/go" "google.golang.org/api/option" ) func NewFirestoreHandler() error { ctx := context.Background() // 今回の問題点はここ sa := option.WithCredentialsFile("./firebase-adminsdk-key.json") // ProjectIDには自分のFirebaseプロジェクトIDを入れる conf := &firebase.Config{ ProjectID: "hogehoge", } app, err := firebase.NewApp(ctx, conf, sa) if err != nil { log.Errorf("Firebase NewApp failed. err=%+v", err) return err } client, err := app.Firestore(ctx) if err != nil { log.Errorf("Init Firestore failed. err=%+v", err) return err } defer client.Close() // ここから動作確認用のレコードINSERT処理なので、必須ではない _, _, err = client.Collection("users").Add(ctx, map[string]interface{}{ "first": "Ada", "last": "Lovelace", "born": 1815, }) if err != nil { log.Errorf("Add failed. err=%+v", err) return err } // ここまで return nil } これで実行すると、下記の通り「ファイルが見つからない」と怒られる Init Firestore failed. err=cannot read credentials file: open ./firebase-adminsdk-key.json: no such file or directory" 成功パターン ファイルが読み込めないならば読み込まなければ良いのでは? ということで、JSONをベタ書きする案でいけた // JSONファイル読み込み案を不採用 // sa := option.WithCredentialsFile("./firebase-adminsdk-key.json") // JSONをベタ書きする案を採用 tmpJson := []byte(`{"type": "hogehoge", "project_id": ...}`) sa := option.WithCredentialsJSON(tmpJson) 念のため、JSONベタ書き案を採用する場合は、しっかりと中身を環境変数で管理するなどしてGit管理下に含めないようお気をつけを… おわりに JSONベタ書きする案ならば、SDKの秘密鍵を環境変数などで管理することができるので、JSONファイルを読み込む方法よりも何かと便利かと。 例:JSONファイル使う方法の場合だと、認証情報の入ったJSONファイルはGit管理できないのでHerokuにデプロイするときに少し手を加える必要が出てくるが、今回の方法ならば環境変数に設定すれば終わり 参考 Cloud Firestore を使ってみる  |  Firebase 公式Doc go - Build golang including firebase credential file .json - Stack Overflow 解決方法を知るにあたり、この記事が大いに参考になった 【第3回】Go言語(Golang)入門~Firestoreデータ操作編~ – 株式会社ライトコード 画像も載っていて、Firestoreへの接続方法の基本的なやり方はここも参考になった(ファイル読み込む方法を採用している記事)
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

[Go Snippet] マルチプレクサをざっくり

自作で ServeHTTP を実装 http.Handler は、ServeHTTP を実装すれば OK。 (詳しくは「Golang での Web アプリ開発で、理解を早める 5 ステップ」をチェック) maing.go package main import ( "fmt" "math/rand" "net/http" ) type MyServeMux struct { } // ServeHTTP を implement func (p *MyServeMux) ServeHTTP(w http.ResponseWriter, r *http.Request) { if r.URL.Path == "/" { fmt.Fprint(w, "hello, world!\n") return } if r.URL.Path == "/randomInt" { fmt.Fprintf(w, "Random number is: %d", rand.Int()) return } // TOP 以外のパスがきたら Not Found を返す http.NotFound(w, r) } func main() { mux := &MyServeMux{} http.ListenAndServe(":8000", mux) } NewServeMux を使う 自作のマルチプレクサだと、if 文で URL パスをチェックしないといけない。 これだと手間なので、NewServeMus を使うことでもう少し簡単に書ける main.go package main import ( "fmt" "math/rand" "net/http" ) func main() { myMux := http.NewServeMux() // TOP ページ myMux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { fmt.Fprint(w, "hello, world!\n") }) // random int ページ myMux.HandleFunc("/randomInt", func(w http.ResponseWriter, r *http.Request) { fmt.Fprintln(w, rand.Int()) }) http.ListenAndServe(":8000", myMux) }
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

[Go Snippet] シンプル HTTP サーバー

基本 main.go package main import ( "fmt" "log" "net/http" ) func hello(w http.ResponseWriter, r *http.Request) { // io.WriteString(w, "hello, world!\n") or fmt.Fprint(w, "hello, world!\n") } func main() { http.HandleFunc("/hello", hello) log.Fatal(http.ListenAndServe(":8000", nil)) } go run main.go の後、コンソールから curl http://localhost:8000/hello を実行すると、hello, world! が返ってくる ただ、POST で送信しても同様の結果になる (curl -X POST http://localhost:8000/hello) リクエストメソッドで出力変える GET と POST でレスポンスを変えるには、r.Method をチェックする必要がある package main import ( "fmt" "log" "net/http" ) func hello(w http.ResponseWriter, r *http.Request) { if r.Method == http.MethodGet { fmt.Fprint(w, "hello from GET\n") } else { fmt.Fprint(w, "hello from POST\n") } } func main() { http.HandleFunc("/hello", hello) log.Fatal(http.ListenAndServe(":8000", nil)) } これで GET と POST のリクエストに応じてレスポンスを変えられる fmt.Fprint or io.WriteString どっちを使う? 上記のサンプルでは、fmt.Fprint も io.WriteString いずれでも問題なく動作するけど、結局何が違うの? ということで、調べてみました。 関数 シグネチャ io.WriteString func WriteString(w Writer, s string) (n int, err error) fmt.Fprint func Fprint(w io.Writer, a ...interface{}) (n int, err error) 結論から言うと、文字列 を出力する場合は io.WriteString で、それ以外は、fmt.Fprint で OK。 fmt.Fprint の方が、複数の引数を渡せたり、型に縛られずに渡すことができる汎用性がある。
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

[Snippet] シンプル HTTP サーバー

基本 main.go package main import ( "fmt" "log" "net/http" ) func hello(w http.ResponseWriter, r *http.Request) { // io.WriteString(w, "hello, world!\n") or fmt.Fprint(w, "hello, world!\n") } func main() { http.HandleFunc("/hello", hello) log.Fatal(http.ListenAndServe(":8000", nil)) } go run main.go の後、コンソールから curl http://localhost:8000/hello を実行すると、hello, world! が返ってくる ただ、POST で送信しても同様の結果になる (curl -X POST http://localhost:8000/hello) リクエストメソッドで出力変える GET と POST でレスポンスを変えるには、r.Method をチェックする必要がある package main import ( "fmt" "log" "net/http" ) func hello(w http.ResponseWriter, r *http.Request) { if r.Method == http.MethodGet { fmt.Fprint(w, "hello from GET\n") } else { fmt.Fprint(w, "hello from POST\n") } } func main() { http.HandleFunc("/hello", hello) log.Fatal(http.ListenAndServe(":8000", nil)) } これで GET と POST のリクエストに応じてレスポンスを変えられる fmt.Fprint or io.WriteString どっちを使う? 上記のサンプルでは、fmt.Fprint も io.WriteString いずれでも問題なく動作するけど、結局何が違うの? ということで、調べてみました。 関数 シグネチャ io.WriteString func WriteString(w Writer, s string) (n int, err error) fmt.Fprint func Fprint(w io.Writer, a ...interface{}) (n int, err error) 結論から言うと、文字列 を出力する場合は io.WriteString で、それ以外は、fmt.Fprint で OK。 fmt.Fprint の方が、複数の引数を渡せたり、型に縛られずに渡すことができる汎用性がある。
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Go のテストでプロジェクトの特定箇所のディレクトリをフルパス指定する

Go のプロジェクトで、デバッグと、コマンド実行時のテストの挙動の違いが発生して、原因が、カレントディレクトリの違いということに気づいた。VSCode のデバッガでデバッグを実行するときは、プロジェクトルートをワーキングディレクトリにしているが、通常実行すると、現在テストのソースがあるディレクトリがある場所がカレントになる。 この場合、どうやって解決したらいいだろうか? という小ネタを調べたのでメモしておく ファイルの現在位置を取得する runtime.Caller は、関数実行のファイルやライン行数を取得するための関数。この関数を利用して現在のファイルパスを取得する。 _, testSourceFilePath, _, _ := runtime.Caller(0) 相対パスを取得する Win/Mac/Linux のファイルのパスの違いが気になるので、ライブラリで処理する。結局こんなメソッドを書いた。すると、プロジェクトルートからの相対パスを渡すと、そのパスの絶対パスを返すようになる。 func GetTargetPath(t *testing.T, relativePathFromProjectRoot string) string { _, testSourceFile, _, _ := runtime.Caller(0) currentDir := filepath.Dir(testSourceFile) projectRootDir := filepath.Join(currentDir, "..", "..", "..") return filepath.Join(projectRootDir, relativePathFromProjectRoot) } 大変単純であったが、しっかりデバッグも、コマンドラインも動くようになった。ただ、VSCode のデバッガ側の設定をもっとうまくする方法も勉強すべきであるが、ファイルパスを元にファイルの絶対パスを求める方法がわかったのでメモしておく
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Ebitenで作ったHello WorldをiOS端末で表示させる その2

はじめに 前回(Ebitenで作ったHello WorldをiOS端末で表示させる その1)の続きです。 今回は、前回作ったMobile.frameworkを読み込んで実機で動かすところを解説します。 Xcodeの作業メインになるのでかなり画像多めです。 作業内容 1. 新規プロジェクトの作成 2. 不要なファイルの削除 3. Mobile.frameworkのインポート 同じファイルを入れたり消したりしてますが、この方法なら間違いなく動く状態でMobile.frameworkをインポートできるようです。 4. 必要なファイルの生成と調整 MobileEbitenViewControllerWithErrorHandling.mの作成 中に記述する内容は公式のサンプルをコピペ MobileEbitenViewControllerWithErrorHandling.hの作成 中に記述する内容は公式のサンプルをコピペ storyBoardのclassを変更 AppDelegate.mの中身を変更 変更する内容は公式のサンプルをコピペ 5. 実機を繋いで実行! 6. なんかエラーが出てアプリ動かない! こんなエラーが出た時は、こちらの記事を参考にすると動くようになります 7. 実機で動いた! こんな画面が表示されるはず! IMG_8411.PNG 仕上げ このままだと、コード修正があったときに毎回Mobile.frameworkを移動させないといけないので、出力元にxcodeのプロジェクトを持ってきちゃいましょう コードの変更があったとき 再度Mobile.frameworkを書き出してXcodeでビルド&ランするだけ! 試しにHello worldの文字を入れ替えてみよう! 終わりに React Nativeは触ったことありますが、Xcodeで色々やるのは初めてだったので結構苦しみました。。。この記事を見て「iOSビルドまでのやり方が細かく載ってるならebiten触ってみようかな」という人が増えることを祈ってます?
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Ebitenで作ったHello WorldをiOS端末で表示させる その1

はじめに Ebitenは素晴らしいことにmobile対応もしているため、作ったゲームをiOS/andoridで動かすことができます。 とはいえ、コマンド一発で全てが完了するわけではなく、ebiten側の準備、iOS用ファイルの書き出し、Xcodeの準備をする必要があります。 まずはいつものHello worldをiOSの実機で表示させるためのサンプルコードと手順を解説する第1弾としてebiten側の準備とiOS用ファイルの書き出しを説明していきます。 参考にしたもの やっぱり公式のgo-inovationに倣って作業するのが一番です ディレクトリ構造 . ├── hello │ └── hello.go  // hello worldを表示させるコードが書かれている │ ├── mobile │ └── mobile.go  // mobile用にhello.goを呼び出す │ └── tools.go  // ビルドツールのimport用? │ └── go.mod └── go.sum └── main.go  //PC用にhello.goを呼び出す 今回の作業の前に 公式のツアーのHello worldを見ると、main.goの中にHello worldを表示させるコードが書いてあります。 ただし、今回はmobile用に書き出すためにinit関数の中でmobile.SetGameを実行する必要があります。 そこで、PCで作業する入り口をmain.goに、mobile用に書き出す入り口をmobile.goに、共有で使うコードをhello.goにします。 PCで作業をするときはgo run main.go、mobileに書き出すときはmobile.goを使うという形で共通のhello.goが表示されるようにします。 それぞれのコード hello/hello.go package hello import ( "fmt" "github.com/hajimehoshi/ebiten/v2" "github.com/hajimehoshi/ebiten/v2/ebitenutil" ) type Game struct{} func (g *Game) Update() error { return nil } func (g *Game) Draw(screen *ebiten.Image) { // Hello, WorldとTPSを画面に表示させる ebitenutil.DebugPrint(screen, "Hello, World!\n") ebitenutil.DebugPrint(screen, fmt.Sprintf("\nTPS: %0.2f", ebiten.CurrentTPS())) } func (g *Game) Layout(outsideWidth, outsideHeight int) (screenWidth, screenHeight int) { return 320, 240 } // main.goやmobile.goで、ここで作ったGame構造体を使えるようにする重要な部分 func NewGame() (*Game, error){ game := &Game{} return game, nil } mobile/mobile.go package mobile import ( "github.com/hajimehoshi/ebiten/v2/mobile" // hello-worldを表示させるファイルをインポート "github.com/krile136/hello-world/hello" ) func init() { // helloパッケージで宣言されたGameをgameへ格納 game, err := hello.NewGame() if err != nil { panic(err) } // helloパッケージで宣言されているGameをmobile用にセットする mobile.SetGame(game) } // Dummy is a dummy exported function. // // gomobile doesn't compile a package that doesn't include any exported function. // Dummy forces gomobile to compile this package. func Dummy() {} mobile/tools.go // +build tools import ( _ "github.com/hajimehoshi/ebiten/v2/cmd/ebitenmobile" ) main.go package main import ( "log" "github.com/hajimehoshi/ebiten/v2" // hello-worldを表示させるファイルをインポート "github.com/username/projectname/hello" ) func main() { // helloパッケージで宣言されたGameをgameへ格納 game, err := hello.NewGame() if err != nil { panic(err) } ebiten.SetWindowSize(640, 480) ebiten.SetWindowTitle("Hello, World!") // helloパッケージで宣言されているGameを実行する if err := ebiten.RunGame(game); err != nil { log.Fatal(err) } } PCでの動作確認 main.goがあるディレクトリで、go run main.goを実行 iOS用ファイルの書き出し main.goがあるディレクトリで $ ebitenmobile bind -target ios -o ./mobile/ios/Mobile.framework ./mobile これは、./mobileフォルダにあるmobile.goを参照して、./mobile/ios/ に Mobile.frameworkを書き出すコマンドになります。 私の環境だとebitenmobileコマンドががうまく認識されなかったため $ go run github.com/hajimehoshi/ebiten/v2/cmd/ebitenmobile bind -target ios -o ./mobile/ios/Mobile.framework ./mobile を実行しました。 書き出し後 ./mobile/ios/にMobile.frameworkがあればOKです。 これを読み込んで実機で動かすステップは、Ebitenで作ったHello WorldをiOS端末で表示させる その2 で解説します。
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

GoでCLIのTODOリストを作成した時の振り返り

の転載です。 目次 開発管理や開発ルール GitHub GitHub Flow GitHub Projects 英語 CI/CD GitHub Actions 設計 ディレクトリ構造 クリーンアーキテクチャ テスト コーディング Makefile Value Object GoDoc Table Driven Test ライブラリ選定 CLIフレームワーク cobra vs urfave/cli writerライブラリ golangci-lint ポインタ vs 値 スケジューリング launchd cron 通知方法 ossascript slack まとめ CLIでのTODO管理ツールをGoで実装しました。 その際に得た知見ややっていて良かったこと、やって失敗だったと思うことなどを振り返っていこうと思います。 開発管理や開発ルール GitHub デファクトスタンダードなので特に説明することもないかもしれませんが、GitHubを用いてコード管理を行いました。 コード管理だけではなく、後述するようにCI/CDやプロジェクト管理もGitHubで完結させました。 (最近知ったのですがGitLabってオンプレ以外に普通のWebアプリケーションも提供されているんですね。 特に比較とかはしていないです。) GitHub Flow ブランチ戦略というものがあります。 よく知られているものとしてGit FlowとGitHub Flowがあります。 私はGitHub Flowしか利用したことがないので、Git Flowの詳細は他サイトに譲ります(参考)。 GitHub Flowを要約すると変更をmasterに反映するときはPR経由で行えよ、ということになります。 GitHub Flowに準じた開発を行うと決めたので、個人開発ではありますが、masterに直接マージするようなことはせずにPRを毎回作成して開発を行いました。 良かった点は masterは常にテストが通った状態になる 管理しているチケットと紐付けができる 行った変更を自分で追いやすくなる 作業の中断ステータスがわかりやすい 悪かった点は 単純に面倒臭い 個人リポジトリだとmasterへのpush禁止をGitHubがしてくれないので2, 3回 masterにpushしてしまった 次回個人開発を行うことがあれば、最初はmaster pushで整えて test CI/CD パイプライン が整ったタイミングでPRを出すようにしようかと思います。 ただ、後で反省するように上記二つともプロジェクトの最初期に整えるべきものだと考えているので、実質最初からPRを出すつもりです。 また、masterへのpush防止策としてはgit hookに何かしら突っ込んでおく必要があるかなと思っています。 GitHub Projects プロジェクト管理は GitHub Projectsを利用しました。 https://github.com/dondakeshimo/todo-cli/projects/1 https://github.com/dondakeshimo/todo-cli/projects/2 欲しい機能は最初からほとんど決まっていたので、メモ書き以下の要件を最初に記載していたようです。 IssueとPRは紐付けができるので機能の作成さえしておけば、Kanbanでの移動はそこまで必要ないです。 調査系のタスクが入った時にIssueがあると自分の進捗がわかりやすいのとドキュメントが勝手に出来上がっていくのでとても良かったです。 メリットはやはりコード管理ツールと統合されていることに尽きると思います。今後もGitHubで何かしらのプロジェクトを行うときは重宝すると思います。 英語 コードのコメントや使い方などは全て英語にしようと決めていました。 英語の勉強をしたかったという部分と、日本語が入ったコードはダサいと思ったのと、ゆくゆくは外国の方にもcommitしてもらえるようなプロダクトにしたいという野望があったからです。 2人の知人にcontributerをしていただいたのですが、その際にPRを英語で出していただいてハッとさせられました。 海外の方にcommitしてもらうためにはIssueやPRも全て英語で行う必要があったのでは??と。 ちょっとそこまで英語にリソース割けないなと思い妥協しています。個人リポジトリですしね... CI/CD GitHub Actions CI/CDツールとしてはGitHub Actionsを使用しました。 ツールで行いたかったことは下記になります。 PRに対して コードフォーマットのチェック test master commitに対して バイナリのビルド Releasesの作成 コードフォーマットのチェックやtestは比較的簡単に設定できました (設定ファイル)。 Releasesの作成についてはそれなりに苦闘したので、別記事にまとめています。 設計 ディレクトリ構造 ディレクトリ構造について初期は golang-standards/project-layout を模倣して設計していました。 ところが、 this is not a stndard Go project layout というIssueがGo開発者から立てられ、これは標準ではないし、Goの思想としては標準レイアウトのようなものは存在しない、なんでも好きなように作れば良いのだよということが公言されていました。 特に問題となっていたのは pkg ディレクトリのようで、慣例として作られていた vendor との差分がよくわからないなどの意見が見られました。思考停止で利用していましたが、このIssueを受けて internal ディレクトリは pkg ディレクトリにまとめました。 結果的には以下のような構成にしました。 . ├── Makefile ├── README.md ├── cmd │   └── todo │   └── main.go ├── go.mod ├── go.sum ├── pkg │   ├── commands │   ├── domain │   │   ├── notifier │   │   ├── scheduler │   │   └── task │   ├── gateways │   │   └── json │   └── usecases ├── scripts │   └── uninstall.sh └── test    └── scenario    └── crud_test.go pkg の中のディレクトリはそのままパッケージ名となります。 こちらは次節にて詳しく述べる予定ですが、クリーンアーキテクチャのレイヤ名を随所に使用しています。 クリーンアーキテクチャ にてまとめているクリーンアーキテクチャを念頭に設計しています。というよりは開発していくうちにクリーンアーキテクチャっぽく修正していきました。 CLIフレームワークを変更したタイミングがあったのですが、CLIフレームワークとアプリケーションルールがそのタイミングでは絡み付いており、変更が非常に重たいタスクになってしまいました。これを嫌って、先にusecase層とcontroller層(commandsディレクトリ)を分割しました。これによってフレームワークに依存しているのはcontroller層のみとなり変更を容易に行うことができました。(分割は簡単ではなかったです。) このことから、これからはフレームワークやインプットアウトプットが少しでも変わる可能性がある場合は常にクリーンアーキテクチャを最初から意識して設計していこうと心に誓いました。 逆にクリーンアーキテクチャを意識したアーキテクチャになっていた部分で良かったのは、後述するスケジューリングや通知の詳細実装を追加していくのが非常に容易だったことです。最初にインタフェースを決めておく難易度はありましたが、今回の場合は最初から要件がある程度固まっていたのでそこまで悩まずに済みました。 テスト 今回の場合二つのテストが必要と考えていました。 domain/taskの単体テスト e2eテスト 逆にそれ以外の部分は手を抜いています。 ただし、結構アプリケーションロジックにバグが紛れたりするので、usecase層もテストするべきだったなと思っています。 これは今後追加するかもしれません。 最初にテストスコープを決めていて良かったのはdomain層に入れるべきものがはっきりとしたことです。 このロジックにはテスト必要そうだぞというものは大抵domain層にいるべきものなので、ビジネスロジックがusecaseに紛れ込むのを防ぐことができました。 テストの実装時期ですが、プロトタイプのタイミングから単体テストは実装しておくべきだと感じました。 後からやるのは辛いというのが主な理由ですが、先述の通りテストを念頭においた実装をすることでかなりすっきりとした設計になりがちですなので、気づいたらスパゲッティを錬成していたということを防ぐためにも最初からテストを書くべきです。 逆にe2eテストについてはある程度までは放置しておいて良いと感じました。 テスト項目だけ決めておいて毎回手作業で確認していくくらいで良いかなと。 結構テストの実装自体が手間ですし、その手間をかけて自動化するならまずは満足できるレベルのプロダクトを作るのが先だろというのが今の思いです。 コーディング Makefile Makefileは最初に用意しておくと良いと思います。GoのMakefileは毎回ほとんど同じものになると思うので、ここに自分が使っているものでテンプレとなりそうな部分を貼っておきます。 GOBUILD=go build GOCLEAN=go clean GOTEST=go test GOGET=go get GOFMT=gofmt GOGEN=go generate GOIMPORTS=goimports GOLINT=golangci-lint BINARY_NAME=todo CMD_PKG=./cmd/todo SCENARIO_DIR=./test/scenario all: help .PHONY: init init: ## initilize developer environment # mockを利用する場合 go install github.com/golang/mock/mockgen@latest .PHONY: get get: ## go get dependencies $(GOGET) -u -v -t -d ./... .PHONY: build build: ## build go binary $(GOBUILD) -o $(BINARY_NAME) -v $(CMD_PKG) .PHONY: mockgen mockgen: ## generate mock $(GOGEN) ./... .PHONY: test test: build ## go test $(GOTEST) -v ./... .PHONY: scenario-test scenario-test: build ## run scenario test $(GOTEST) -v $(SCENARIO_DIR) -tags scenario .PHONY: clean clean: ## remove go binary $(GOCLEAN) rm -f $(BINARY_NAME) .PHONY: fmt fmt: ## format go files $(GOFMT) -l -w -s . $(GOIMPORTS) -w . .PHONY: lint # need docker to run this command # this command just run golangci-lint # so, if you hate docker, you can run equivalent this installing golangci-lint locally lint: ## check lint, format docker run --rm -v $(shell pwd):/app -w /app golangci/golangci-lint:v1.41.0 golangci-lint run -v .PHONY: help help: ## DIsplay this help screen @grep -E '^[a-zA-Z_-]+:.*?## .*$$' $(MAKEFILE_LIST) | awk 'BEGIN {FS = ":.*?## "}; {printf "\033[36m%-20s\033[0m %s\n", $$1, $$2}' プロジェクトの最初期から置いておくと良いと思います。最初期は go run とかを結局たくさん使いますが... Value Object で説明している値オブジェクトを利用できる場面では利用することを意識しました。 具体的にはタスクに紐付けられる時間などが値オブジェクトとして定義されています。 時間については基本的にただの文字列なのですが、値オブジェクトとすることで文字が時間を表現するのに有効かどうかや、時間としての加算減算に対応できるようになっています。ビルドに通ればランタイムでのエラーがほとんど起きなくなったので、値オブジェクトは利用できるシーンでは積極的に利用すべきだと感じています。 GoDoc チョットできるGoプログラマーになるための詳細GoDoc で詳細に述べられていますが、Goではドキュメント自動生成のためのコメントお作法があります。このお作法の通りにコメントを書けばIDEがメソッドの説明とかを出して来れたりします。 コードは大体最初に思い描いたものよりも長く、複雑になるので最初からGoDocを書くことを忘れずにしておくと、エディタの力を最大限に生かすことができます。(ただ本当に面倒くさい)。次回以降もちゃんと書けるかは心の余裕によると思います。 Table Driven Test Table Driven Testをご存知でしょうか? テストの可読性が飛躍的に向上し、DRYなテストを実現できる手法になります。 自分が参考にしたサイトを見つけることができなかったので、お好きなサイトをGoogle先生の書庫から選んで参照いただければと思います。 これを知って実装できたことでテストがかなり書きやすかったのでここで取り上げています。 しばらくはTable Driven Testを使ってテストを書き続けるでしょう。 ライブラリ選定 ライブラリの選定はしっかりとするべきだという話です。 ライブラリを利用するということは依存が一つ増えるということです。 依存が一つ増えたらメンテナンスのための改修が必要になるリスクが一つ増えるということです。 まずはライブラリを使うか自前で実装するべきかという部分を真剣に考えた方が良いというのが最近の考えです。 ライブラリを選定するタイミングで確認するべきは Star数 最終更新日 更新頻度 あたりだと考えていますが、実際に使ってみないとわからない部分も多々あると思うので参考程度に。 todo-cliではtodoリストの表示と、CLIフレームワークにおいて外部ライブラリを使用しています。 それぞれについて少し解説します。 CLIフレームワーク cobra vs urfave/cli 初期段階では urfave/cli を利用していました。 最初からcobraも選択肢には入っていたのですが、同等の人気である urfave/cliが先に検索にヒットしたのでなんとなくで使っていました。 結果としてはcobraに途中で乗り換えており、これからもCLIフレームワークを利用するならcobra一択だろうと考えています。 urfave/cliのよくない点は大きく二つです。 twitter で指摘されているが、謎の情報を送る処理が実装されていた 必ず todo [option] [args] の順番でコマンドを叩く必要があり、タスクの内容の後にオプションをつけるといったことができなかった その他にも細かい点でcobraの方が気が利いている部分が多く、紹介記事やStar数だけでは実際の使用感は判断できないのだなと感じました。 このことから私が得られる教訓は、導入ハードルを恐れずにどんどん知らないツールを触っていけ、ということです。 writerライブラリ 初期段階ではGoの標準パッケージにある tabwriter を利用していました。 表示がリッチである必要はないと考えていたので、これで十分と考えていたのですが、知人が触って来れている時に日本語で表記がずれるという課題を共有いただき更に解決策となるパッケージもご提示していただきました。 途中で乗り換えたライブラリが tablewriter になります。 CJKに対応しており、リッチな表を書くこともできます。 表示系のライブラリではCJK(China, Japan, Korea)言語に対応しているかどうかを気にかける必要があるのだということを学べました。 golangci-lint Goのフォーマッターやリンターは複数のパッケージ、ツールに別れておりそれを統合したツールもいくつかありますが、自分が調べた範囲で2021/07時点では golangci-lint を用いるのが良さそうだと感じました。 使い方等はREADMEや紹介記事も多数あるのでここでは省略します。 ポインタ vs 値 (この節の内容はうろ覚えなので100%信用しないでください。) Goではポインタがヒープ領域に割り当てられます。 そのため、軽量な値に関してはポインタでの受け渡しよりも値渡しを行った方が良いです。 具体的にはプリミティブ型で関数内で値を変更しない場合は値渡しで良いと言えると思います。 これはメソッドのレシーバにも同様のことが言えます。 軽量な構造体に対するメソッドの場合はポインタを指定するよりも値を渡した方が早くなります。 また、ポインタの利用を消極的にすることで関数型言語のように副作用のない(少ない)関数を書くことが意識できます。 スケジューリング この節ではスケジューリング機能をどのように実装したかを説明します。 スケジューリング機能とググると robfig/cron がヒットするのではないかと思います。 当然の話ではありますが、スケジューリングを行うプログラムはプロセスとして常駐する必要があります。 Goのみでこれを行おうとすると、プロセスを走り続ける必要があり軽さや使いやすさという点で疑問が生じる設計しか思いつきませんでした。 よって、基本戦略としてはOSに備わったスケジューリング機能を使用する方針としています。 launchd MacOSではcronを使用するのは非推奨とされており、代わりにLauncdというプロセスをスケジューラとして利用するように言われています。 launchdで定期的にスクリプトを実行 にて詳細に開設されているので、利用に際して困るというようなことはなかったです。 指定箇所に指定フォーマットのXMLファイルを配置するだけなので実装難易度もそこまで高くないです。 cron Linuxではcronがおおよそインストールされていると信じてcronを用いたスケジューリングもできるようにしました。 こちらについてはタスクの登録方法が複数あるのですが、課題となったのは権限です。 launchdと同様の方針で、cronファイルを /etc/cron.d に配置するだけで済むと考えていたのですが、配置するためにはroot権限が必要であり、またcronファイルもroot権限である必要があることがわかりました。 root権限をアプリケーションに与える方針はユーザビリティやセキュリティの観点でありえない選択肢であると感じたので、ファイルを配置する方針は諦めました。 諸々調べた結果 もまとめてあるので見ていただければと思いますが、結局 crontab [file] でスケジュール登録する方針としました。 しかし、こちらの方針にも問題点があり、指定したcronファイルで全てのジョブが上書きされてしまうのです。そのため、ユーザがもともとcrontabを使用していた場合は利用するべきではないですし、自分の登録したジョブも注意を払わなければ最新の一件しかスケジュール登録されないということになってしまいます。詳細な実装についてはリポジトリの方を参照いただければと思います。 通知方法 理想はポップアップを出すことだと考えていました。MacにおいてはシンプルにポップアップをCLIから呼び出すインタフェースが搭載されており、容易に実装することができましたが、linuxについてのポップアップはまだ特に考えられていないです。Slackに投げられればそれで良いだろうと甘えました。 ossascript 遊んでもらえれば楽しいと思うのですが、 osascript -e 'display notification "通知したいメッセージ"' とターミナルで打っていただければ通知センターにメッセージが届きます。 似たような方法で、色々な制御ができる のでこれらを用いてポップアップ通知を実現しています。 実装としては os/exec によって外部コマンドを呼び出す形になっています。 slack SlackではIncomming Webhookを利用すればHTTP POSTリクエストを送ることでメッセージを送信することができます。 Incoming Webhookの導入はユーザに委ねるしかありませんが、その他の解決策もないと思ったので妥協しました。 LINEラブな方のためにLINE Botインタフェースを用意するとかも面白そうではありますね。 まとめ GoでCLIを作るのは非常に楽しかったです。cobraというフレームワークが最高です。kubectlとかを参考にできたところも良かったですね。 開発途中でドメイン駆動設計やクリーンアーキテクチャについて勉強していたので、プロトタイプからどんどん設計周りの改善案が出てきたというのも面白かったポイントです。 今後も開発は続けますし、よければ追加機能や機能修正のPRをお待ちしています。
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む