- 投稿日:2020-12-12T23:59:32+09:00
LeetCodeに毎日挑戦してみた 104. Maximum Depth of Binary Tree(Python、Go)
Leetcodeとは
leetcode.com
ソフトウェア開発職のコーディング面接の練習といえばこれらしいです。
合計1500問以上のコーデイング問題が投稿されていて、実際の面接でも同じ問題が出されることは多いらしいとのことです。golang入門+アルゴリズム脳の強化のためにgoとPythonで解いていこうと思います。(Pythonは弱弱だが経験あり)
23問目(問題104)
104. Maximum Depth of Binary Tree
問題内容
Given the
rootof a binary tree, return its maximum depth.A binary tree's maximum depth is the number of nodes along the longest path from the root node down to the farthest leaf node.
(日本語訳)
root二分木のが与えられた場合、その最大深度を返します。二分木の最大深度 は、ルートノードから最も遠いリーフノードまでの最長パスに沿ったノードの数です。
Example 1:
Input: root = [3,9,20,null,null,15,7] Output: 3Example 2:
Input: root = [1,null,2] Output: 2Example 3:
Input: root = [] Output: 0Example 4:
Input: root = [0] Output: 1考え方
rootが存在しない場合、left,rightにアクセスするとエラーになるので例外処理をはじめにします
左の二分木と右の二分木それぞれ再帰処理で潜っていきます。
その二分木の左の二分木と右の二分木の大きい方をreturnします。
- 解答コード
class Solution: def maxDepth(self, root): if not root: return 0 return 1 + max(self.maxDepth(root.left), self.maxDepth(root.right))
- Goでも書いてみます!
func maxDepth(root *TreeNode) int { if root == nil { return 0 } return diveNode(root.Left, root.Right, 1) } func diveNode(p *TreeNode, q *TreeNode, count int) int { if p != nil && q != nil { return max(diveNode(p.Left, p.Right, count+1), diveNode(q.Left, q.Right, count+1)) } else if p != nil { return diveNode(p.Left, p.Right, count+1) } else if q != nil { return diveNode(q.Left, q.Right, count+1) } else { return count } } func max(values ...int) int { ret := values[0] for _, v := range values { if ret < v { ret = v } } return ret }
- 投稿日:2020-12-12T23:59:28+09:00
Goのginとgormで簡単なWebアプリケーションを作る。
はじめに
どもです。
最近、Webアプリケーションを久しぶりに作るってなって、GoでのCRUDの書き方等を忘れたので、メモといった感じで記していきます。ルーティングとCRUDについて詳しく書いていきます。
githubに今回作成したソースコードを載せておきます
使用する環境
実際の環境
項目 使用したもの OS CentOS8 DB MySQL バックエンド Go Goで使用したパッケージ
用途 パッケージ名 ルーティング gin ORM gorm 開発環境に関しては、すでに終わっていることを仮定します。
実装する内容
今回作成するものは、元から存在するアプリケーションのユーザの管理をするアプリケーションです。
本来、gormのマイグレーションというのもできますが、今回はすでにテーブル等が作成されているものとします。
- ユーザの一覧(Select)
- ユーザの詳細(Select)
- ユーザの削除(Delete)
- ユーザの編集(Update)
といったものを実装していきます。
ディレクトリ構造
. |- handler.go //DBにCRUDする関数を書く |- main.go //GETとPOSTの処理を書く |- routing.go //ルーティングの関数を書く |- views/ |- css/ | |- main.css |- html/ | |- delete.html | |- detail.html | |- index.html | |- update.html |- js/ |- main.jsgin
ginのGoDocはこちらから
ginというのは、GoのWebフレームワークで、とりあえず早い。ライバルとしてはechoがあります。
自分的には、どっちでもいいのかなと思います。ただ、ginのほうが簡潔に書けるという印象があります実装
main.gopackage main import ( "github.com/gin-gonic/gin" ) func main() { r := gin.Default() r.LoadHTMLGlob("./views/html/*.html") r.Static("/css", "./views/css") r.GET("/", index) r.GET("/detail/:id", detail) r.GET("/delete/:id", delete) r.GET("/update/:id", update) r.POST("/do_delete/:id", do_delete) r.POST("/do_update/:id", do_update) r.Run(":80") }routing.gopackage main import ( "fmt" "net/http" "strconv" "github.com/gin-gonic/gin" ) func index(c *gin.Context) { users, err := UserGetAll() if err != nil { fmt.Println(err) c.AbortWithStatus(http.StatusInternalServerError) return } c.HTML(http.StatusOK, "index.html", gin.H{"users": users}) } ~~略~~ func do_update(c *gin.Context) { n := c.Param("id") name := c.PostForm("name") email := c.PostForm("email") id, err := strconv.Atoi(n) if err != nil { fmt.Println(err) c.AbortWithStatus(http.StatusInternalServerError) return } err = UserUpdate(id, name, email) if err != nil { fmt.Println(err) c.AbortWithStatus(http.StatusInternalServerError) return } c.Redirect(302, "/") }解説
主に使っている関数をピックアップして、紹介します。
main.go
まずはここ。main関数の
r := gin.Default()こいつで、
rってのがginパッケージ使いまっせーの宣言をする。r変数は好きにつけてぐださいr.LoadHTMLGlob("./views/html/*.html") r.Static("/css", "./views/css") r.GET("/", index) r.POST("/do_delete/:id", do_delete) r.Run(":80")
rを用いて、このような関数を呼び出してます。
LoadHTMLGlob:htmlファイルの場所の指定
Static:css等の静的ファイルの指定(jsでも同様)
GET:GETの処理(第一引数がドメイン以降のパス指定、第二引数がそのパスに対応した関数の呼び出し)
POST:POSTの処理(第一引数がドメイン以降のパス指定、第二引数がそのパスに対応した関数の呼び出し)
Run:サーバの起動(:80は80番ポートで起動してます)GETとPOSTで呼び出す関数は、次の
routing.goに書いてます。routing.go
GETとPOSTという処理があって、とりあえずGETだったらHTMLを出力してPOSTだったらリダイレクトする的なイメージがあると思います。とりあえず、各関数の引数に
c *gin.Contextとありますが、先ほどのrみたいな感じです。関数の解説
c.AbortWithStatus(http.StatusInternalServerError) c.HTML(http.StatusOK, "index.html", gin.H{"users": users}) c.Redirect(302, "/")
cを用いて、このような関数を呼び出してます。
AbortWithStatus(http.StatusInternalServerError):リクエストの認証に失敗したら、401を返す
HTML:htmlファイルを返す。(第一引数は置いておいて、第二引数はhtmlファイルの指定、第三引数はhtmlに値を渡す変数の定義)
Redirect:リダイレクトをする。(第一引数で302で返す宣言、第二引数でリダイレクト先の指定)HTMLに変数を渡す場合の処理は、
Go Templateとかで検索すると、いろいろ出てくるので、そちらをどうぞ。
main.goとrouting.goは以上。これで、ページ遷移くらいはできると思います。単にHTMLを出力するサンプルコードを載せて、次に行きます。サンプルコードpackage main import ( "github.com/gin-gonic/gin" ) func main() { r := gin.Default() r.LoadHTMLGlob("./views/html/*.html") r.GET("/", index) r.Run(":80") } func index(c *gin.Context) { c.HTML(http.StatusOK, "index.html", gin.H{}) }これで、
index.htmlは、出力するはず。(エラーだったらコメントください)gorm
gormの公式ドキュメントはこちらから
gormというのは、Go用のORMライブラリで、DB操作を簡単にできるすげーいいやつ。
公式ドキュメントが超優秀なので、ぜひ見てみてください実装
handler.gopackage main import ( "fmt" "gorm.io/driver/mysql" "gorm.io/gorm" ) type Users struct { Id int Name string Email string Password string } type Posts struct { Id int Author int Title string Content string Comments int } const connect = "root:root@tcp(127.0.0.1:3306)/laravel?charset=utf8mb4&parseTime=True&loc=Local" func UserGetAll() ([]Users, error) { db, err := gorm.Open(mysql.Open(connect), &gorm.Config{ DisableForeignKeyConstraintWhenMigrating: true, }) if err != nil { return nil, fmt.Errorf("UserGetAll失敗: %w", err) } var users []Users err = db.Select("id", "name", "email", "password").Find(&users).Error if err != nil { return nil, fmt.Errorf("UserGetAll_Select失敗: %w", err) } return users, nil } func UserGetOne(id int) (Users, error) { db, err := gorm.Open(mysql.Open(connect), &gorm.Config{ DisableForeignKeyConstraintWhenMigrating: true, }) var user Users if err != nil { return Users{}, fmt.Errorf("UserGetOne失敗: %w", err) } err = db.First(&user, id).Error if err != nil { return Users{}, fmt.Errorf("UserGetOne_First失敗: %w", err) } return user, nil } func PostGetAll(author int) ([]Posts, error) { db, err := gorm.Open(mysql.Open(connect), &gorm.Config{ DisableForeignKeyConstraintWhenMigrating: true, }) if err != nil { return nil, fmt.Errorf("PostGetAll失敗: %w", err) } var posts []Posts err = db.Where("author = ?", author).Find(&posts).Error if err != nil { return nil, fmt.Errorf("PostGetAll_Select失敗: %w", err) } return posts, nil } func UserDelete(id int) error { db, err := gorm.Open(mysql.Open(connect), &gorm.Config{ DisableForeignKeyConstraintWhenMigrating: true, }) if err != nil { return fmt.Errorf("UserDelete失敗: %w", err) } var users Users err = db.Delete(&users, id).Error if err != nil { return fmt.Errorf("UserDelete_Delete失敗: %w", err) } return nil } func UserUpdate(id int, name string, email string) error { db, err := gorm.Open(mysql.Open(connect), &gorm.Config{ DisableForeignKeyConstraintWhenMigrating: true, }) if err != nil { return fmt.Errorf("UserUpdate失敗: %w", err) } var users Users err = db.First(&users, id).Error if err != nil { return fmt.Errorf("UserUpdate_First失敗: %w", err) } users.Name = name users.Email = email db.Save(&users) return nil }解説
handler.go
各関数の最初に書かれている、
gorm.Openが最重要で、DB(MySQL)に接続する関数で、countで定義したDB情報を呼び出して使ってます。自分の関数の命名についてですが、
〇〇GetAllだと要素の全取得、〇〇GetOneだと一つだけ取得、〇〇Deleteだと削除、〇〇Updateだと更新、みたいな感じです流れとしては、
UsersやPostsのように、必要な情報を構造体として扱って処理をします。
ここでは、gormの関数が多いので、よく使うものだけピックアップします。関数の解説
First:なにか指定したものの最初にマッチした要素を取得する
Where:データの特定をするために、カラムを指定する
Find:Firstと異なり、特定の一つのデータを取得すると、公式ドキュメントにもありますが、関数とSQL文がいい具合に処理できるので、すごく便利。
自分の場合、errとしてエラーハンドリングをしつつ処理をしています。
(確かGoのすごい人からおしえてもらった書き方だった気がする…)終わり
かなり長くなってしまいましたが、これで解説は終わりです。
Goを用いたWebアプリケーション開発というのは、これからもっとはやっていくと思います。そこで、この記事が参考になることを祈ります。
疲れた…
- 投稿日:2020-12-12T23:56:30+09:00
任意のファイルをPNGファイルで隠してみる
はじめに
何故、こんなことをやろうと思ったのか今ではもう思い出せないのですが、ある日の私はファイルを隠したいと突然思いました。
それも、それとなくにです。
色々考え何も分からなくなって途方に暮れていたとき、私は何をとち狂ったのかファイルを連結したらどうなるんだろうと疑問に思いおもむろに連結してみることにしました。
すると、奇妙なことにその連結したファイルは普通にファイルとして使えることがわかりました。
これができればファイルを隠せるぞと喜びに顔をほころばせながら、任意のファイルをPNGファイルで隠すコードを書くため私はVimを起動しました。隠したファイルを取り出す
ファイルの末尾にファイルを書き込めばファイルを隠せることが分かったので、次は取り出す方法を考えてみます。
まあ1つ目のファイルの末尾が分かればそこからEOFまで読み込めば良いだけですね。
矢面に立たせるファイルは構造が単純なものが良さそうです。
なおかつ適当に生成したファイルが存在しても不自然ではなさそう画像ファイルが適してそうです。
なので、PNGにしようと思います。PNGの構造
PNGの末尾を知るためにはPNGについて知らないとどうにもならないので調べてみます。
https://www.w3.org/TR/PNG/
上記を読むにPNGはシグネチャを除いてチャンクというデータの集まりが連なった構造をしているようです。
シグネチャは8バイトでこのファイルがPNGであることを示し10進数で以下のようなデータになっています。
137, 80, 78, 71, 13, 10, 26, 10
チャンクは全部で18種類ありその中でもIHDR、PLTE、IDAT、IENDはcritical chunksと呼ばれ必須なチャンクなようです。
チャンクの構造は以下のようになっています。
1 2 3 4 Length Chunk Type Chunk Data CRC あるいは
1 2 3 Length(=0) Chunk Type CRC
名前 説明 Length Chunk Dataのバイト数を示す。4バイト。符号なし整数。0から2^31-1の値。 Chunk Type チャンクの種類を示す。4バイト。10進数で65から90および97から122しか使えない。つまり、a-Z。 Chunk Data 実データ。ない場合もある。 CRC (Cyclic Redundancy Code) データの破損をチェックするために使用。Chunk TypeとChunk Dataで計算されてる。4バイト。Chunk Dataがなくても常に存在する。 ファイルが隠れてるか調べてみる
最後のチャンクのChunk Typeは必ずIENDなのでそこまでPNGの構造に従って読み込んでいき、そこがEOFか確認すればファイルが隠れているかわかりそうです。
以下のようなコードでPNGファイルの後ろにまだデータがあるか調べることができます。package main import ( "log" "os" ) type png struct { file *os.File } // Length渡してChunk Dataの長さを知る func (p png) getChunkLength(bytes []byte) int { return int(bytes[0])*256*256*256*256 + int(bytes[1])*256*256 + int(bytes[2])*256 + int(bytes[3]) } // シグネチャ見てPNGか調べる func (p *png) isPng() (bool, error) { header, err := p.read(8) if err != nil { return false, err } signature := [8]int{137, 80, 78, 71, 13, 10, 26, 10} for i := 0; i < len(signature); i++ { if int(header[i]) != signature[i] { return false, nil } } return true, nil } // 末尾まで読み込んでそこがEOFか調べる func (p *png) isEOF() (bool, error) { overflowingData := make([]byte, 1) if n, _ := p.file.Read(overflowingData); n != 0 { _, err := p.file.Seek(-int64(n), os.SEEK_CUR) return true, err } return false, nil } func (p *png) read(length int) ([]byte, error) { bytes := make([]byte, length) if _, err := p.file.Read(bytes); err != nil { return bytes, err } return bytes, nil } func (p *png) isHidden() (bool, error) { if isPng, err := p.isPng(); err != nil { return false, err } else if !isPng { return false, nil } for { // Lengthを読む length, err := p.read(4) if err != nil { return false, err } // Chunk Typeを読む type, err := p.read(4) if err != nil { return false, err } if string(type) == "IEND" { // CRCを読む if _, err := p.read(4); err != nil { return false, err } if isEOF, err := p.isEOF(); err != nil { return false, err } else if isEOF { return true, nil } return false, nil } // Chunk DataとCRCを読む if _, err := p.read(p.getChunkLength(length) + 4); err != nil { return false, err } } } func main() { file, err := os.Open(os.Args[1]) if err != nil { log.Fatalln(err) } defer func() { if err := file.Close(); err != nil { log.Fatalln(err) } }() png := png{file: file} log.Println(png.isHidden()) }CLIツールを書いてみる
ここまで分かったら隠したり取り出したりが簡単にできるようなCLIツールを書くのは人間の宿命です。誰も抗うことはできません。
https://github.com/tayusa/hidf
拡張子も埋め込み、取り出したときに.pngからもとの拡張子に戻るようにしてます。
使い方は以下です。
ちなみに生成するPNGファイルはランダムにカラフルな四角と丸が入るようになっていて、あんまり開きたくないようにしてます。これで敵を欺けること間違いなしです。
- 投稿日:2020-12-12T23:33:38+09:00
UdemyでGo言語入門:簡単なHTTPリクエストで洒脱な始業連絡を実装!
はじめに
- こちらは「あなたの学びをシェア!2020年までにUdemyで学んだこと04【PR】Udemy Advent Calendar 2020」、14日目の記事です。
- Go言語を勉強してみたいと思った私が、「現役シリコンバレーエンジニアが教えるGo入門 + 応用でビットコインのシストレFintechアプリの開発」講座の中で学んだ知識を、まずは遊びに使ってみたケースのご紹介となります
![]()
- 受講してよかった点:Go言語初学者にもわかりやすく環境の導入から解説頂けますし、基本的な構文から、実際に動くビットコイントレーディングシステム構築まで幅広く学ぶことができます。おススメです!
- 幅広い内容を習得可能ですが、今回はその中から構造体⇔JSONの扱いや、HTTPリクエストについての学習内容を活用します。
- 成果物は完璧に遊びなので、明日のあなた様を幸せにする要素は皆無です!!
![]()
簡単に自己紹介
- 私は普段はメカ物設計に携わるノンプログラマーです。
- ここ数年、業務でPythonを使うようになったのを皮切りにプログラミングを学習し、自宅用に使う謎アプリから、webサービス開発(⇒頓挫)と、趣味に花を咲かせています。
- プログラミング学習って、基礎的な構文なんかは本やネット記事を読んだりすれば分かるのですが、実践的なテクニックだったり、複数言語を組み合わせて使うケースになると、途端に学習のハードルが上がるなぁ…と悩んでいた折、Udemyさんの様々な講座を発見し、それ以来(セールのたびに)お世話になっております…!
なぜGo言語を学ぼうと思ったか
- Go言語自体の特徴は、こちらの記事がまとめて下さっています
- Python一辺倒だった私個人にとっては、「記述の自由度が少ない=良いコードの書き方に迷いが少ない」ことや「コンパイル型言語だから成果物を周囲に展開しやすい」ことが新鮮に感じられ、学習意欲を駆り立てられました。
あとGopherがかわいい
出典:https://golang.org/doc/gopher/今回何を作ったか
Microsoft Teamsのチャネル上に、私の行きたい温泉地の写真を付けて洒脱に始業連絡を行います…!
何の実益もないです!
※この写真は今回解説用に私が撮った写真を使用していますモチベーションの補足
- 私の勤める会社では、リモートワーク普及時に「上司に始業・終業を連絡する」というルールが敷かれました…。
- Teamsのチャットで毎回「おはようございます、始業します」って打つのも面倒なんですよね…。そこから何か会話するわけでも無いですし。
- じゃあTeamsに始業連絡を投稿するプログラムを作って、それを走らせれば良いんじゃない?
![]()
- そこまでするなら何か遊び心を出していきたいもんだ…。よし、温泉画像を貼って癒されよう
![]()
いざ実装
方針検討
Microsoft TeamsにはIncoming Webhook機能があり、Webhookに所定のjson形式データを送ることで「カード」なるものを投稿できることは知っていました。
出典:Microsoft様公式HPwebhookとは所定のURLにリクエストをPOSTするようなもののようです。そこで冒頭に紹介した講座で習得した、JSON形式の扱いや、HTTPリクエストの投げ方を活用して実装していきます。
機能設計
- 本文は固定する(タイトル:氏名、サブタイトル:おはようございます、始業します)
- 温泉地画像はネット上の使って良い画像を使用する。毎日同じではつまらないから、いくつかの候補からランダムで選択する。
ファイル構成
main.go spas.json
ファイル 説明 main.go その名の通りメイン spas.json 温泉の名前と画像URLをまとめたjsonファイル ソースコード
main.gopackage main import ( "bytes" "encoding/json" "fmt" "io/ioutil" "log" "math/rand" "net/http" "time" ) var apiURL string = "<YOUR WEBHOOK URL>" type heroCard struct { Type string `json:"type"` Attachments []interface{} `json:"attachments"` } type heroAttachments struct { ContentType string `json:"contentType"` Content heroContent `json:"content"` } type heroContent struct { Title string `json:"title"` Subtitle string `json:"subtitle"` Text string `json:"text"` Images []interface{} `json:"images"` } type spa struct { Title string `json:"title"` Image string `json:"image"` } func jsonReader(file string) []spa { bytes, err := ioutil.ReadFile(file) if err != nil { log.Fatal(err) } var spas []spa if err := json.Unmarshal(bytes, &spas); err != nil { log.Fatal(err) } return spas } func createRand(size int) int { rand.Seed(time.Now().UnixNano()) return rand.Intn(size) } func createHero(name, msg, text, image string) heroCard { hero := heroCard{ Type: "message", Attachments: []interface{}{ heroAttachments{ ContentType: "application/vnd.microsoft.card.hero", Content: heroContent{ Title: name, Subtitle: msg, Text: text, Images: []interface{}{ map[string]string{ "url": image, }, }, }, }, }, } return hero } func main() { spas := jsonReader("./spas.json") spa := spas[createRand(len(spas))] heroStr, _ := json.Marshal(createHero("Tenkoh", "おはようございます、始業します", spa.Title, spa.Image)) resp, err := http.Post(apiURL, "application/json", bytes.NewBuffer(heroStr)) if err != nil { fmt.Println("Error") } defer resp.Body.Close() fmt.Println(resp.Body) }spas.json[ {"title":"石見銀山温泉", "image":"URL1"}, {"title":"草津温泉", "image":"URL2"}, {"title":"別府温泉", "image":"URL3"}, {"title":"有馬温泉", "image":"URL4"}, {"title":"奥飛騨温泉", "image":"URL5"} ]解説
Webhook URLの取得
- こちらはたくさん記事がありますので割愛します。
カードを作るための構造体の作成
- 上記で取得したwebhookに、カードを作るための情報をjsonにして渡す必要があります。ベストプラクティスはよく分からないのですが、講座の中では構造体をマーシャルしていたので、まずはそれに倣ってみます。
- 渡すべきjsonの形式ですが、Microsoftさんの公式ホームページを見てもイマイチ分かりません…。ネットの海を漂った結果、Hiro様の素晴らしい解説記事にたどり着きました!そちらで知った情報をもとに、大きめの画像が配置されるヒーローカードを作っていきます。
- いかんせんネストした構造体になりそうですので、初学者である私は
heroCardheroAttachmentsheroContentの3つの構造体に分割して堅実に進めてみます。- ただ、構造体を定義する作業はめっっっっっちゃ面倒です。 今回は修行だと思い手打ちしていきましたが、講座の中で教えてくれるjsonからGo Structに変換してくれるサイトを活用することを推奨します!! (JSON-to-Go) こういう実践テクニックを教えてくれるのも助かりますね
![]()
HTTPリクエストの送信
- HTTPリクエストの送信には様々な種類があり、講座の中でもいろいろなパターンを教えて貰えます。
- 今回はシンプルに
http.PostでPOSTしていきます。結果
- こんな感じのカードがTeamsチャネルに投稿されました。温泉行きたいですね。
![]()
- 私は上記プログラムをコンパイルした.exeファイルを、こちらの方法でスピーディーに呼び出すように活用しています。
おわりに
- 講座で学んだ内容を使って、
どうでもいいソフトを作ることができました!- 今回はHTTPリクエストだけを活用しましたが、講座の主題はサーバ側(API)の作成なので、そちらも自分なりのテーマを創出し、その中で技術を身に着けていきたいと思っています。
補足情報
- 私はWebの仕組みも良く分からなかったので、こちらの書籍を自習して補いました。
- 投稿日:2020-12-12T18:13:15+09:00
GoでGoogleSpreadsheet出力する
フューチャー Advent Calendar 12日目 です。
昨日は @mirai_taiyaki さんによる PG BATTLE 2020参戦記 ~チーム結成法、注意点、作戦など~ でした。はじめに
例えば業務系のシステムなどたくさんのデータをバックエンドで処理したのち、手元で更に加工をしたいといった用途が考えられます。
このとき大きな選択肢としては「CSV形式などによるダウンロード」となりますが、これを実施した場合、ローカルに何らかのバッチ処理を作ることが前提となります。
また、ユーザー数によっては繰り返しデータが出力されることになり、DBへのアクセス数も増えます。
それくらいであれば、一つのバッチ処理で共通してアクセスできるクラウド上にダウンロードする相当のCSVなりのデータをアップロードすることを考えます。
その際たる候補として上がるのがGoogle Driveです。複数のユーザーでファイルの共有が行え、かつ各種APIが提供されたことによって外部システムとの連携も比較的用意となっています。
今回は従来ローカルへのダウンロードを行っていたデータをGoogle Drive上の特定のフォルダ以下のSpreadsheetに出力するシステムを構築してみます。構築するシステム
- ファイルは特定のフォルダ以下に順番に作っていくものとします
- ファイルは年月の単位で新規につくるものとします
- シートは日付単位で切り替えるものとします
- シートは人間が編集することは前提とせず、システムでのみ更新することとします
- ファイル・シートはルールに則ってシステムが自動生成して増やしていくものとします
testFolder ├─sample_202010.spreadsheet │ ├─sheet名:20201001 │ ├─sheet名:20201002 │ ︙ │ └─sheet名:20201031 ├─sample_202011.spreadsheet │ ├─sheet名:20201101 │ ├─sheet名:20201102 │ ︙ │ └─sheet名:20201130 └─sample_202012.spreadsheet └─sheet名:20201201システム構成
最終的な構成として今回バックエンドとしてはAWSを想定します。
NW的に繋がっていればオンプレのシステムでも問題有りませんが、構成のイメージがつきやすいのと、途中で特定のDBに対してIDを取得しにいくものを挟むため、このような構成としています。
今回Goでコードを実装しているのは、図の中のLambdaです。
また、合わせてフローチャートも記載します。実際のコードにも必要になりますが、Google Drive上のファイルはIDで管理されています。
そのため同じ場所に同名のファイルやフォルダを複数作ることができます。
たとえば、フォルダのIDは下記のようにURLから確認できます。ソースコード
今回利用するのは Google Drive API v3とGoogle Sheets API v4です。
Spreadsheetにデータを出力するだけであればGoogle Sheets API v4だけで良いですが、対象ファイルを新しく作る場合はGoogle Drive API v3が必要となります。
今回は上記2つのAPIのsampleを組み合わせ、コードを記載しました。
実際のコードは、こちらから御覧ください。最終的にGoogleSpreadsheetを出力するには
[][]interfaceの形にして関数に与えてあげます。
この2次元のセルがそのままspreadsheetに書かれます。
今回はかんたんのため、A1セルを起点にセルをすべて上書きする形で記載しています。handler.go// 前略: 前半でclsという[][]string型に必要なデータを整形して詰めています v := [][]interface{}{} vv := []interface{}{} for _, c := range cls { vv = []interface{}{} for _, cc := range c { vv = append(vv, cc) } v = append(v, vv) } // シート1のA1セルを起点にして書き込む writeRange := fmt.Sprintf("%s!A1", sheetName) valueRange := &sheets.ValueRange{ Values: v, } _, err = spreadsheetService.Spreadsheets.Values.Update(targetSpreadsheetID, writeRange, valueRange).ValueInputOption("USER_ENTERED").Do() if err != nil { return "", fmt.Errorf("unable to retrieve data from sheet. %v", err) } // 以下略今回はセルの場所と2次元配列の場所をわかりやすくするためのデータしか入れていないですが、適切にデータを収集・編集することができれば、GoogleDrive上に帳票などを出力することも可能になります。
実際の動作
リポジトリに記載したように適切にDriveのIDを取得して環境変数に設定してあげれば、下記のように動作します。
$ make test export DYNAMO_ENDPOINT=http://localhost:4566;\ export DYNAMO_TABLE_EXPORT=local_export;\ export GOOGLE_EXPORT_ROOT_DIR="今回作成したtestFolderのID";\ go test -race -v ./ {"level":"debug","time":"2020-12-12T16:46:04+09:00","message":"unset env variable: GOOGLE_CLIENT_SECRET"} {"level":"debug","time":"2020-12-12T16:46:04+09:00","message":"unset env variable: GOOGLE_DRIVE_ACCESS_TOKEN"} {"level":"debug","time":"2020-12-12T16:46:04+09:00","message":"unset env variable: GOOGLE_SPREADSHEET_ACCESS_TOKEN"} {"level":"info","time":"2020-12-12T16:46:04+09:00","message":"DYNAMO_ENDPOINT is set. http://localhost:4566"} === RUN TestExportSpreadsheet === RUN TestExportSpreadsheet/[正常系]_データ出力 handler_test.go:42: [INFO] command: aws dynamodb --profile local --endpoint-url http://localhost:4566 create-table --cli-input-json file://./testdata/local_export.json {"level":"info","time":"2020-12-12T16:46:09+09:00","message":"url = 実際に出力されたspreadsheetのurl"} handler_test.go:88: [INFO] command: aws dynamodb --profile local --endpoint-url http://localhost:4566 delete-table --table local_export --- PASS: TestExportSpreadsheet (6.58s) --- PASS: TestExportSpreadsheet/[正常系]_データ出力 (6.58s) PASS ok github.com/montblanc18/google-spreadsheet-exporter 6.916s実際にフォルダにもファイルが出力されています。
プログラムでは対象となるシートがなければ新しく作るようにしています。
最初にスプレッドシートを作った段階で「シート1」が作られ、それに追加で「202008」を作っているため、このように動作しています。
なお、先に説明しましたようにGoogle Drive内のファイルはIDで管理されるため同じファイルを複数つくれるので、先のgo clean -testcache && make testを連打すると次のようにファイルがたくさんできます。これを避けるために、一度作成したGoogle SpreadsheetのIDをDynamoDBに保存しておく形にしています。
今回実装したテストの範囲では逐次localstack内のテーブルを消してしまうのでgo clean -testcache && make testのたびにファイルが新規作成されますが、DynamoDB内のテーブルを維持し続ければ、一つのファイルに繰り返し出力されます。最後に
Google Drive および Spreadsheet 連携によって、クラウドにそのままデータが連携できるようになりました。
このままGASに連携したり、あるいはその他のスクリプトを実行したりすることもできます。
もちろん、このまま帳票を作り込んであげることもできます。
また、オンプレのシステムからクラウドに対してデータを打ち上げても良いでしょう。ただし、ここでの注意点としてはデータの整形は手ずから行う必要があることです。
加えて、Google Spreadsheet中にSUMやIFを始めとした関数をセルの参照を使って行いたい場合、列をアルファベット表示にするため、今回は省きましたが次のような関数を挟むような処理が必要です。func Num2Capital(ctx context.Context, colNum int) string { cap := []string{"A", "B", "C", "D", "E", "F", "G", "H", "I", "J", "K", "L", "M", "N", "O", "P", "Q", "R", "S", "T", "U", "V", "W", "X", "Y", "Z"} r := len(cap) if colNum >= r { return fmt.Sprintf("%s%s", Num2Capital(ctx, (colNum-colNum%r)/r-1), cap[colNum%r]) } else { return cap[colNum%r] } }また、Google Spreadsheetへの出力がシステムによるものしかなければよいですが、ユーザーが同時に操作する場合は、上書きしないように注意するなど必要になります。
Google DriveおよびSpreadsheetへの出力機能は便利ではありますが、作り込もうとするとかなり作り込めてしまうので、適宜見極めながら実装していただければと思います。
- 投稿日:2020-12-12T16:24:46+09:00
Goでアスペクト比を保って画像のリサイズを行ってみる
はじめに
画像のリサイズは、業務だとimageFluxを使ったりすることが多いかもしれません。
imageFluxなどを利用することができる環境であれば良いのですが、個人開発や、コストをかけたくないなどの理由で、Goを画像のリサイズを行う方法について書いてみました。処理の流れ
1.画像データのデコードしてwidthとheightの値を取得する
2.画像のリサイズ処理実際の処理
1. 画像データのデコードしてwidthとheightの値を取得する
画像データのデコードと、widthとheightの取得をコードで書くと以下のような感じになります。
package main import ( "fmt" "image" _ "image/jpeg" "os" ) func main() { data, err := os.Open("./image.jpg") if err != nil { fmt.Fprintln(os.Stderr, err) return } defer data.Close() imgData, _, err := image.Decode(data) if err != nil { fmt.Fprintln(os.Stderr, err) return } // 画像の矩形情報を取得しそこからwidthとheightの値を取得する. imgRectangle := imgData.Bounds() width := imgRectangle.Dx() height := imgRectangle.Dy() }ちなみに、widthとheightを取得するだけなら以下のように、image.DecodeConfigでも取得できます。ImgDecodeConfig関数の引数にはos.Openなどで取得した画像ファイルが入ります。
func ImgDecodeConfig(data *os.File) error { config, type, err := image.DecodeConfig(data) if err != nil { return err } fmt.Println(config.Width) fmt.Println(config.Height) }注意点として、 上のコードで、
"image/jpeg"がブランクimportできていない状態で実行すると以下のようなエラーになります。image: unknown format理由は、image.Decode関数(image.DecodeConfigも)が実行されるとき、対象の画像のフォーマットが認識できないからです。
imageパッケージの冒頭にも以下のように記載されています。Decoding any particular image format requires the prior registration of a decoder function
Registration is typically automatic as a side effect of initializing that format's package so thatブランクインポートすることで、インポートされたフォーマットはデコードする際の対象のフォーマットとして利用できるようです。
具体的には、jpegの場合、imageパッケージのRegisterFormat関数が、image/jpegパッケージのinit関数で呼ばれることで初期化されています。// https://golang.org/src/image/jpeg/reader.go L816 func init() { image.RegisterFormat("jpeg", "\xff\xd8", Decode, DecodeConfig) }私はこのブランクインポート忘れて、ずっと
unknown formatエラーにハマってました。。。
またpngの場合は、image/pngをブランクインポートする必要があります。2.画像のリサイズ処理
リサイズ処理のコードはこちらです。記事の最初のほうで共有した画像のデコードとwidthとheightの取得のコードの続きになります。
package main import ( "fmt" "image" "image/jpeg" _ "image/jpeg" "math" "os" "golang.org/x/image/draw" ) func main() { // 最初の実装例コードと同じ // 以下リサイズ処理部分 // widthかheightの長い方を750に固定してリサイズします。 limitEdge := 750 // 以下の条件分岐で、リサイズしたいサイズで空のインスタンスを作成する。 newImgData := &image.RGBA{} // heightがwidth以上の場合 if height >= width { f := float64((width * limitEdge)) w := math.Round(f / float64(height)) newImgData = image.NewRGBA(image.Rect(0, 0, int(w), limitEdge)) } else { f := float64((limitEdge * height)) h := math.Round(f / float64(width)) newImgData = image.NewRGBA(image.Rect(0, 0, int(h), limitEdge)) } // drawパッケージを使って、画像データを反映させる。 // CatmullRomを使っているが、drawパッケージに他にも反映させる関数が用意されている。 draw.CatmullRom.Scale(newImgData, newImgData.Bounds(), imgData, imgRectangle, draw.Over, nil) newImg, err := os.Create("./new_image.jpg") if err != nil { fmt.Fprintln(os.Stderr, err) return } defer newImg.Close() // jpegの場合のエンコード、各フォーマットごとにエンコードの方法は異なります。 if err := jpeg.Encode(newImg, newImgData, &jpeg.Options{Quality: 100}); err != nil { fmt.Fprintln(os.Stderr, err) return } }drawパッケージに定義されているScale関数で画像データを反映させるのですが、drawパッケージに定義されているScalerは4つあります。
drawパッケージScaler定義部分
- NearestNeighbor
- ApproxBiLinear
- BiLinear
- CatmullRom
詳しくは調べきれていませんが、上記の上から処理スピードは早いが画質が悪い順になっていて、1番画質が良いのはCatmullRomですが、処理スピードは1番遅いとのことです。
また、画像のリサイズには、nfnt/resizeというパッケージもあり、使い方も簡単で、githubのStar数も少なくないですが、現在メンテナンスがされていないようです。
最後に
私は実際にこのコードを業務の最中で書いたのですが、その際に以下の記事を参考にしました。とても助かりました。ありがとうございます。
参考記事
- 投稿日:2020-12-12T16:05:58+09:00
パソコンすら使えなかった大学生が9ヶ月で3000時間プログラミングを独学してみた話
バックエンドエンジニアになりたい絶賛就活中の大学生3年生です。
就活が本格化するにあたり、パソコンすら使えかった大学生だった自分がエンジニアになるためにプログラミングを9ヶ月間で3000時間独学した成長を作ってきたものとともに振り返って整理したいなと思い書きました。9ヶ月前の自分のスキル
9ヶ月前は本当にプログラミングどころかパソコンとすら無縁の大学生でした。
具体的には以下のような感じでした。
- C言語でHello Worldしかできない
- プログラミング基礎という文系もやる授業ですら落単
- C言語以外の存在を知らなかった
- キーボードは人差し指だけで打つ
- 使うのはスマホだけでパソコンはめったに使わない
盛っているように聞こえるかもしれませんが全く盛ってないです。今考えると無知すぎてびっくりします。
現在の自分のスキル
現在の自分はこれくらいのWebアプリなら作れるくらいには成長しました。
新歓用マッチングアプリ 「WeCircles」▶ https://wecircles.net/
GitHub リポジトリ ▶ https://github.com/souhub/wecircles
構成はこんな感じです。
エンジニアを目指すことになったきっかけ
きっかけは今年の3月にサークルのホームページを作成することになったことです。
このホームページ作成が本当に楽しすぎました。文字通り人生で初めて寝食を忘れました。朝起きて気づいたら夜8時で本当に驚きました。
このときはプログラムを書いたわけではないのですが当時の自分にとってはCSSやHTMLも十分複雑なプログラムだと感じていて、なによりパソコンの画面のハイライトがかっこよすぎてエンジニアになれたら本当に幸せだなあと心から思い、将来の夢をエンジニアに定めました。
DELLの3万円くらいで買ったWindowsパソコンを使っていましたが、その日にその勢いのままMacBook Pro の16インチ を買いました。具体的に作ったものと学んできたこと
ここから具体的に学んできたことを実際に作ってきたものを見ながら整理していきます。細かいものを合わせるとまだあるのですが、主に次の4つを作りました。
① サークルホームページ
② Railsを使ったWebアプリ
③ 外部APIを使ったミニアプリ
④ Goで作ったSNS一つ一つ振り返っていきます。
① サークルホームページ
作ったもの
サークルのホームページ
きっかけになったホームページを作成しました。レスポンシブデザインの静的ページを作成しました。レンタルサーバーを借りるのは無駄にコストがかかり、なにか手はないかと調べたところ「Amazon が AWS っていうサービスをやってるのかあ。。。S3とやらを使えば静的ページなら格安っぽいぞ」ということでS3からの配信を行いました。学んだ技術
- AWS (S3、IAM)
- HTML
- CSS
- SCSS
- Visual Studio Code
- ドメインの取り方
役立った教材
感想と課題
② Railsを使ったアプリ
作ったもの
Twitter 風Webアプリ
次に作ったのはTwitter風Webアプリの作成です。
完成したとき嬉しすぎてインスタのストーリーにあげてたのが残ってました。プライバシーの関係で一部モザイクかけてます。
(YouTubeに飛びます↓)
経緯はこんな感じです。
HP作成でググりまくった中で動的なWebアプリがあることを知る
↓
次は動的なものを作ってみたいな
↓
Railsという簡単にアプリを作れるものがあるのことを知る
↓
ProgateのRailsコースやる
↓
Railsチュートリアルをがっつり見ながら再現
↓
何も見ないでゼロからTwitter風Webアプリ作る(Google検索のみ)学んだ技術
- AWS(Cloud9)
- Ruby on Rails
- jQuery
- Heroku
- SQLite
- テスト駆動開発
- テスト
- デプロイ
役立った教材
感想と課題
初めてちゃんと動くもの作れてとても楽しかったです。非常に便利なフレームワークである分、ブラックボックスな部分が大きく、なぜ動いているかが全く理解できていないなと感じていました。
このままでRailsだけをやっていたら「プログラミングができる人」ではなく「Railsの使い方を知っている人」になってしまいかねないと危機感を感じたので、次はフレームワークを使わずにアプリを作ってみてプログラミングへの理解を深めることを課題としました。③ 外部 API を使ったミニアプリ
作ったもの
ツイートランキングWebアプリ
GoでTwitter APIを利用したミニアプリを作りました。例えば「AbeShinzo」と入力して「RTが多い順」を選択して検索ボタンを押すと安倍元首相の直近100件のツイートをRTが多い順に並び替えて表示してくれるアプリです。
構成はこんな感じでした。
前回のアプリでRailsを使ったのでその流れでRubyを使っても良かったのですが、ググったところGoが流行ってるらしかったのでGoでやってみることにしました。APIを自分で実装するのは荷が重かったので、外部のTwitter API を叩いてそれを利用するだけのミニアプリ作ることにしました。
このミニアプリの概要はこちら→GoでGORMとTwitterAPIを使ったミニミニアプリ作ってみた
学んだ技術
- Go
- ライブラリの使い方(GORM,go-twitter)
- APIの叩き方
- MySQL
- GORM
- HTTP通信
- JSON
役立った教材
- A Tour of Go
- Go公式ドキュメント
- スターティングGo言語
- TwitterAPIの公式ドキュメント
- Webを支える技術 (サーバーとクライアントの違いすら説明できなかった自分にとって、この本は本当によかったです。)
感想と課題
Railsと異なり、きちんと自分が書いたコードが行っていることを理解する必要があり、難易度が急激に上がってとても難しかったです。数日間全く理解できずとても苦労しました。そのときの気持ちのまま書いたときの記事がこちらです。→Goで Twitter APIを使ってみたら心が3回くらい折れた話。
ここまででアプリケーションレイヤーの基礎の基礎は理解できたのかなと感じました。
しかし、それを支えるインフラや環境構築や自動テスト自動デプロイなどは何もわからず課題は山積みでした。
そこでそこらへんの領域もまるっと学習できるようにもう少し大きなアプリケーションをGoでフレームワークなしで作ることを次の課題としました。④ 大学生向け新歓用マッチングアプリ 「WeCircles」
大学生向け新歓用Webアプリ
新入生歓迎イベントがコロナの影響でできないため、その手助けになるようなものを作りたいなと思い開発しました。
サークルと新入生のマッチング、または新入生同士のマッチングを狙ったアプリです。
参考までにアプリとGitHubリポジトリのURLです。
- WeCircles▶https://wecircles.net/
- GitHub リポジトリ▶https://github.com/souhub/wecircles学んだ技術
- Go
- MySQL
- AWS(S3, RDS, ECR,ECS , ACM, SSM, Route53, CloudWatch)
- Docker
- Docker Compose
- Terraform
- CircleCI
- Linux (Ubuntsu)
- Javascript
- jQuery
- HTML/CSS
- Bootstrap
- Git/GituHub
前回までのアプリとは比べ物にならないほどたくさんの技術に触れることができました。deployブランチにgit pushすると、CircleCIが自動で全部やってくれるようになっています。詳細はREADMEを御覧ください。(https://github.com/souhub/wecircles)
環境構築に関してはDockerを学習しました。で開発環境と本番環境用のDocker file を作り分けました。
インフラはAWSを更に学習し、AWSですべてのインフラを構築しました。また、AWSのGUI以外の操作方法を探したところTerraformが良さそうだったのでTerraformを学習しました。これによりインフラはコードベースで管理を行うことができています。役立った教材
基本はほとんどUdemyで学習しました。あとはすべてGoogle検索で頑張りました。
- 現役シリコンバレーエンジニアが教えるGo入門 + 応用でビットコインのシストレFintechアプリの開発
- AWS:ゼロから実践するAmazon Web Services。手を動かしながらインフラの基礎を習得
- ゼロからはじめる Dockerによるアプリケーション実行環境構築
- もう怖くないLinuxコマンド。手を動かしながらLinuxコマンドラインを5日間で身に付けよう
- Goプログラミング実践入門 標準ライブラリでゼロからWebアプリを作る impress top gearシリーズ (この本についてくるサンプルコードを読むのがとても勉強になりました。)
- 実践Terraform AWSにおけるシステム設計とベストプラクティス (技術の泉シリーズ(NextPublishing))(Terraformはほとんどこれで学習しました。あとは公式ドキュメントを適宜読みました。)
感想と課題
フレームワークを一切使わないことでブラックボックスだった部分がかなり減り、最初よりは自信が持てるようになったと思います。課題であった環境構築やインフラの学習でWebアプリの仕組みをざっくり理解できただけではなくその周りのツールの学習もできて、一石四鳥くらいの学習ができて、作ってよかったなと感じました。
課題に関してはキリがないほどでてくるのですが、
- UIなどフロントエンドの技術力がほぼないこと
- SQLの最低限の知識しかないこと
- セキュリティ知識全般
- 基本的なターミナル操作しかできないこと
- 命名規則など
- 集客なども考慮したビジネス的観点からアプリに搭載する機能は何がベストなのか考える力
- ECSへの理解に自信がないこと
- テストへの理解に自信がないこと
- GitやGitHubの最低限の機能しか使えてないこと
など山積みです。
おわりに
ここまで読んで頂きありがとうございました。最後に「学習方法に関して気づいたこと」と「これからについて」書いて終わりたいと思います。
学習方法に関して気づいたこと
自分のようなプログラミングを勉強してみようという大学生に向けて学習方法の発見をシェアしたいと思います。
受験勉強とプログラミングの勉強は少し違うと感じました。一言でいうと、「理解が最初に来るのが受験勉強で、理解が最後に来るのがプログラミング学習」です。
受験勉強では、教科書読んでインプット
↓
理解できた
↓
演習問題やってみるという順序ででできていました。
しかし、この順序で学習できたのはホームページ作成だけでした。
自分のプログラミング学習のほとんどは書籍や動画をとりあえず全部みてインプット
↓
よくわからないしとりあえずやってみる
↓
なんとなく掴めた
↓
言語化してみる
↓
理解できたという順序でした。
この経験から「理解が最初に来るのが受験勉強で、理解が最後に来るのがプログラミング学習」だと感じました。プログラミングの勉強はとにかく手を動かすすなわちとにかくやってみることが一番大事だと思います。これからについて
Web系の会社でバックエンドエンジニアとして働くために今は就活をとにかく頑張りたいです。
また、3年生の間は学校があまりにも忙しくインターンにいく時間が全く取れなかったので、インターンとして経験を積みたいと考えています。
きっかけはサークルホームページの作成で、自分はサークルの幹部だったので断れずにやったのですが、断っていたらと考えるとゾッとします。この経験からやりたくないことでもとりあえずやってみることは本当に大切だなと感じています。
これからもとりあえずやってみる精神を大切にして決して現状に満足することなく、プログラミングを楽しむ心を忘れずにもっといろんな技術に触れたり色々なものを作っていきたいと思います。
- 投稿日:2020-12-12T11:20:56+09:00
りそなIB用CSVファイルまとめGoプログラム
私が住んでいる田舎にはUFJ銀行が存在しない。
しかし、住めば都。
そして、りそな銀行も存在しない。
それでも生活に支障はないと思い込むことで快適に生活できるのだから不思議だ。
地元になくてもニュースとして流れてくるぐらい有名な銀行だ。実際はATMぐらい地元に欲しいのだが、無い物ねだりをしたところで、私のために用意してくれるとは思えない。
それでも、インターネットバンキング(通称IB)は、使える(りそな以外も使いたい)。と言うことで、取引情報をCSVにして(手動)取得が出来る。
付け込み出来ないため、他の手段で残高確認出来るのはありがたい。
確認できるとは言え、2年前までの遡りが限界で、それより古い取引は紙の通帳のみでしか保存できない(書き込み済みが前提になるという・・・何の解決にもなっていないが)。
当時、口座を作るときに行員の口車に乗せられ、通帳を作らなかったのだが、最近作成した。
紙に記録できる状況があってもCSVファイルで管理できるのはありがたい(しつこいと思うだろうが、何せ付け込みできない生活環境だからな)。ちょっとだけ本題。
今後もCSVファイルを取得するだろう予想は容易に想像出来る。
何はともあれ、ファイルが増えると言うことにもつながり、管理が面倒になる。そのため、1つのファイルにまとめるようにした。
CSVファイル
前置きが長くなるのはいつものこととして、今回は滅多に使わない口座ではあるが、あって困るものではないので、CSVファイルの統合用プログラムを作った。
GitHub
以下に配置している。
https://github.com/chesscommands/resonaCSVIntegration
せっかくなのでプログラムの解説をしようと思ったが、複雑なことは何一つ無いため、やめた。
作った経緯
今回のプログラムを作るつもりは無かったが、せっかくGo言語を勉強したのだから何かしらの入出力関係に絡んだプログラムを作ろうと思い立った。
(本来の目的はGo言語の勉強のためだが・・・勉強と言うからには、解説した方が良いかな・・・。)プログラムの機能
プログラムに渡した複数のCSVファイルをもとに、1つに統合する。
1つの場合は、ほぼ何もせずに出力する。
複数の場合に本領発揮し、ファイルを古い順に並べて出力する(ファイルのタイムスタンプ基準であって、中身ではない)。
また、複数ファイルの場合は、合計行を加工し、最新ファイルの残高を表示するようにした(蛇足機能?)。実行
ごく普通に実行すれば良い。
引数にはcsvファイルを渡す。
複数受け取れるため、上記のスクリーンショットのように、グロブで渡せる(ファイル名のべた書き不要)。
実行後は、りそなIB統合済みcsvファイル.txtというファイルが出力され、この中にまとめられた取引が記録されている。実行すれば分かることだが、基本的にはCSVファイルから取引に限定した取り込みを行っている。
要は、金額は数値型として保存するため、そこに文字列がある場合、エラーになる。
CSVファイルの行頭は、タイトルが設定されており、その文字列があるため、どうしてもエラー出力が発生する。
1行目をスキップさせようとも考えたが、余計なお世話かなとも思い、辞めた。不本意な部分
ファイルの入出力の勉強用に作ったのだが、思い描いたように作れなかった。
いつも通りmattnさんに助けを求めたが、私の望んだ機能がGoには無いようで、ちまちま1カラムずつ入出力させることになった。
ファイルから任意の行のみを抜き出したり、書き出したりしたかった(seek関数がそれだと思ったが、全く違った。)。やはり、どのプログラミング言語でも楽に実装できる部分は限度があるのだろう。
思い描いたパッケージがないから自作することこそがプログラマーの腕がなるというか、プログラミング冥利に尽きるというか・・・。何よりデータの扱いを律儀に考えていたが、金額なども含め、一律string型で対応すればよかったと思い始めた。
そもそもmattnさんからは、Goではゴルーチンやチャネルを使うのが当たり前だと言われていたのに、全く使いこなせず、今年販売の本で勉強したのに、コンテキストパッケージも習得できずじまいだった。
そもそも2020年販売の本にコンテキストの説明あったか!?作成期間
17時間程度かけて作ったようだ(大雑把な集計)。
無駄な作業が多いため、熟練者達は2〜3時間もかけずに作れるだろう。ゴルーチンの使い時が分からず、使わなかったのは、大変致命的だった。何のための勉強用開発だったのか・・・。
懸念点
C言語の癖が抜けきらずに悶々としているのだが、引数はポインタ渡しをすべきだったのでは無いかと思っている。
しかし、本の説明では、値を使い回す場合、値渡しをするとあった。
それでも私の実装方法はその説明に従っていないように思う。困った。
正解が無いため、答え合わせが出来ず、困っている。何より、厳密なテストをしたわけでは無いため、取引内容によっては二重出力されるはず。
私の取引方法は、インフラ契約の引き落としだけなので、その現象が見られず、問題ないと判断した。そろそろGo言語用の辞典が出版されてもいいと思うのだがな・・・。
今後の予定。
以下、改良点。
- りそなIBからのCSV自動取得(できるのか不明)。
- 合計行をもう少し見やすくしたい(無関係な項目をnil出力したい)。
- 文字列利用での半角の長音記号を全角にしたい(日付部分は対象外と言うこと)。
- 引数に渡されたファイルを精査したい(適切なファイルのみ処理したい)。
- 書き込み時の排他制御をしたい。
- GUI画面にしたい。
- Go言語特有のゴルーチンを使いたい。
- 新旧関係なく取り込みたい。
大量の勉強。
アルゴリズムの勉強。
Go言語専用のゴルーチンなど。
メソッドの概念もいまいち理解できなかった(それも取り込めるようになりたい)。キーボードの作成。
そろそろごみHelixキーボードの修正を再開したい。
また、ここ数年はPlanckを使っているが、キースイッチが重く感じ始めたため、軽いのに作り直したい。
そのためにも部品の取り寄せが必要だ・・・あぁ働かなければ。何より、左右分離型が私の机周りの環境にはあっているように思う。
しかし、買い増しするのはな・・・。数年前のプログラム更改
マインクラフトのセーブデータをバックアップするプログラムをPerlで組んだ。
それを今回Goに置き換えた。minecraftのセーブデータを管理するPerlプログラム
今回と違い、わりと短時間で作り上げたとは言え、それでも手間取ることが多かった。
一番の失敗は、こっちでもゴルーチン関数を使わなかったこと。
寂しいよ。残念箇所
プログラムの作成前にアルゴリズムをある程度考えてからプログラミングに挑んだが、そのアルゴリズムはすべてお釈迦になった。
大変残念で、反省すべき点だった。
Go言語の勉強用に作成したのに、アルゴリズムを考える時間が大半だった。あとは、就職活動を本格的に再開すること。
どうしても、アドベントカレンダ投稿用に、この程度のプログラム作成ですら無茶苦茶時間を割いてしまったこと。今回の投稿感想
夏休みの宿題の感想文のような感じになってしまった。
GitHubでのリードミーファイルを充実させて、誰も彼もが使えるようにしたい。
需要は無いと思っているが・・・今時りそなだぜ。
ネットバンキングのやり方が古いと思い、フロッピーディスクでのやりとりを考えていたが、逆だった。
今はフラッシュメモリでのやりとりも古くなりつつあるため、クラウド時代なのかな・・・それも古くなる時代が来るのだろう。
まずは勉強だ。以上。
ちなみに、以下を参考にした。
参考URL
パッケージ filepath
逆引きGolang (ファイル)
Go言語(golang)でShiftJISのファイルをutf-8に変換する
Go言語(golang)でcsvの読み書き Reader/Writer
Go言語(golang) ファイルの読み書き、作成、存在確認、一行ずつ処理、コピー など
Goでファイルの特定位置から読む
Go 言語の ioutil パッケージを使ってみる
Golangでの文字列・数値変換
欲張りなマッチと控え目なマッチ(最小量指定子)
- 投稿日:2020-12-12T10:10:25+09:00
Slackチャンネル内で人気の絵文字ランキングをみたい
1.DockerでGoの開発環境を作ってServerlessでAWS Lambdaにデプロイする
参考
https://qiita.com/kai_kou/items/fc3eab987d7ed2d6f65b2.Slack トークン取得
参考
https://qiita.com/ykhirao/items/3b19ee6a1458cfb4ba213.SlackのPermission設定とメソッド
メソッドとPermissionの一覧
https://www.utali.io/entry/2016/10/03/004421今回設定するPermission
SlackAPI確認方法
https://slack.com/api/channels.history?channel=[CHANNEL]&count=1000&token=[SLACK_TOKEN]&query=after%3Ayesterday4.goのmain.goを絵文字カウント取得コードにする。
参考
https://qiita.com/dondoko-susumu/items/73d83ace88c9b507a371
Lambdaで動かせるようにmain メソッドの中を変更する
main.gofunc main() { lambda.Start(MainProcess) } func MainProcess() { token := os.Getenv("SLACK_TOKEN") if token == "" { log.Fatal("SLACK_TOKEN environment variable should be set") } //続く5.Lambdaの環境変数に2で作成したSLACK_TOKENを設定する
6.完成したので実行する
コンテナ内// ビルド GOOS=linux go build -o bin/main // lambdaにデプロイ serverless deploy // lambdaで実行 serverless invoke -f hello
- その他参考
How to build a Serverless API with Go and AWS Lambda
https://www.alexedwards.net/blog/serverless-api-with-go-and-aws-lambda#creating-and-deploying-an-lambda-function
- 投稿日:2020-12-12T09:00:28+09:00
Goの定数がヤヤコシイ話
問題
以下のコードを実行するとどうなるか?
package main import "fmt" func main() { const c = 2i * 3i var v int = c fmt.Printf("%T/%T", c, v) }
- コンパイルエラー
- 実行時panic
int/intが出力されるcomplex128/intが出力される※14日目の記事で関連する問題が出題されています。先に見ておくといいかも
![]()
解答
解答
4.complex128/intが出力される
解説
Goにおける定数の扱いは独特で理解しづらいものです。本問題の要点は次の2つです。
- Goには型無し(untyped)の定数がある
- 「型無し」といっても、Goの型が定まってないだけで、実は「真偽値」「整数」「複素数」などの“種別”がある
定数の“種別”1は以下のものがあります。
There are boolean constants, rune constants, integer constants, floating-point constants, complex constants, and string constants. Rune, integer, floating-point, and complex constants are collectively called numeric constants.
(Constants)すなわち、“数値の定数”には「ルーン定数(rune constant)」「整数定数(integer constant)」「浮動小数点定数(floating-point constant)」「複素数定数(complex constant)」の4つの“種別”があります。
詳細
実際に
main()の実行を追ってみます。1行目const c = 2i * 3i
2iと3iは虚数リテラル(imaginary literal)です。リテラルが定数として扱われるのは当然ですが、Goではこの場合特に「型無しの定数」になります。Literal constants,
true,false,iota, and certain constant expressions containing only untyped constant operands are untyped.
(Constants)ただ「型無し」といっても先述の通り“種別”はあります。
2iや3iは(そもそも値が実数でないので2)「型無しの複素数定数」となります。
2i * 3iは「型無しの複素数定数」同士の乗算なので、結果も同じく「型無しの複素数定数」となります。値は−6なのですが整数定数にはならないことに注意しましょう。Any other operation on untyped constants results in an untyped constant of the same kind; that is, a boolean, integer, floating-point, complex, or string constant.
(Constant expressions)
const c = …の定数宣言ではcに型を付けていないため、cも「型無しの複素数定数」(値は−6)となります。If the type is omitted, the constants take the individual types of the corresponding expressions. If the expression values are untyped constants, the declared constants remain untyped and the constant identifiers denote the constant values.
(Constant declarations)2行目var v int = cint型変数
vの初期値にcを指定したので、実質的に「cのvへの代入」が発生します。従って、代入可能性(assignability)が問題になりますが、右辺が「型無し定数」である場合は表現可能性(representability)のみが要件になります。A value
xis assignable to a variable of typeT("xis assignable toT") if one of the following conditions applies:
- …(略)…
- x is an untyped constant representable by a value of type T.
数値の表現可能性では「数値そのもの」だけが問題になります。今の場合、「−6」はintで表される整数値であるため、「複素数定数である」ということは無関係で、この代入は可能です。
ちなみに、ここでもし右辺が「型付き」(定数でない場合も含む)である場合は、左辺の型と一致していることが要求されます。つまり以下の例3はコンパイルエラーになります。
var v int = float32(c) // エラー:cannot use float32(c) (type float32) as type int in assignment最後の行になりました。
3行目fmt.Printf("%T/%T", c, v)ここでは
cとvという2つの式の型を考える必要があります。vは当然intとなりますが、「型無し」のcはどうすればいいでしょうか。このように4、「型無し」の定数に「他に制約がない状況」で型を与えないといけない場合は、「デフォルト型」(default type)が適用されることになります。The default type of an untyped constant is
bool,rune,int,float64,complex128orstringrespectively, depending on whether it is a boolean, rune, integer, floating-point, complex, or string constant.
(Constants)つまり、引数の
cは「complex128型の−6」として関数に渡されます。従って、complex128/intが出力されることになります。まとめ
- Goには型無し(untyped)の定数がある
- 「型無し」といっても、Goの型が定まってないだけで、実は「真偽値」「整数」「複素数」などの“種別”がある
Goの定数はヤヤコシイ
“種別”は本記事で仮に用いている用語であり、公式のGoの用語ではありません。 ↩
0iも「型無しの複素数定数」と扱われるようなので、要するに「虚数リテラルは必ず複素数定数を表す」ということなのでしょう。規格では“An imaginary literal represents the imaginary part of a complex constant.”(Imaginary Literals)という記述しかなくてアレなんですが。 ↩
float32(c)は「float32型の浮動小数点定数の−6」となります。この式自体が合法なのは「−6がfloat32型で表現可能である」ためです。 ↩詳しくいうと:
fmt.Printfの第2引数以降はinterface{}型の可変長引数であるため、実質的に「cをinterface{}型変数に代入する」ことになります。変数が持つ値は常に型付きであり、かつ、interface{}型はあらゆる具象型により実装され得るので、「他に制約がない状況で型を与えないといけない」に該当します。 ↩
- 投稿日:2020-12-12T00:10:51+09:00
【Go言語】Fake timeつかったら並行処理のテストの景色が変わった
昨日のAdvent Calendarで、Fake timeつかったら時間のかかるコードのテストが一瞬で終わったという記事をアップしましたが、引き続きFake timeについて書きます。
(ネタが足りなくなって記事を分割した・・・わけではないです念の為?)注: 現時点(2020/12)で "Faketime isn't currently supported on Windows."だそうです。Windows Gopherの方はDockerとか使って下さい?♂️
前置き
Go言語にはGoroutineが標準で組み込まれており並行処理を比較的簡単に書くことが出来ます。
Goroutineはとても強力ですが、いざそのテストを書くときになって悩んだりしたことはないでしょうか(私はあります)。Goroutineのテストが難しくなる理由を挙げてみます。
- 処理のタイミングや実行順序が不定で処理の度に値が変わったりする
- アサーションのタイミングが難しい
- 時間がかかりがち(TimerやTickerを使った処理のテストetc)
本稿は、Fake timeを使うことで上記の問題を(全てではないですが)一部解決することができるのではないか? と思い立って書いた実験的記事となります。
Fake timeとは
Fake timeについての詳細は昨日の記事を参照下さい。
ここではテストにFake timeを利用する手順だけざっとおさらいしておきます。テスト実行
--tagsオプションを付与するだけです。
go test --tags=faketime [packages]Fake timeのときだけ実行
テストコードにbuildタグを付与しておくとFake timeが有効の場合のみ実行するテストを記述出来ます。
// +build faketime package hoge出力からPlayback headerを削除
ツールを作りました。
trimfaketimeインストール↓
go get -u github.com/knightso/trimfaketime/cmd/tftテスト実行↓
go test --tags=faketime [packages] |& tftテスト例
経過時間を制御する
ちょっとマイナーなネタですがCloud Firestore(Datastore)に"500/50/5"ルールというベストプラクティスがあります。
大量のデータアクセスが必要な場合にいきなりスパイクさせるのではなく最初は500くらいからスタートして5分毎に50%増えるくらいのペースで徐々にアクセスを増やしていきましょう、というものですが、サンプルとしてこの"500/50/5"ルールを制御するユーティリティを作成してそのテストを書きたいと思います。テスト対象コード
package faketime import ( "sync" "sync/atomic" "time" ) type RampUp struct { rate float64 interval time.Duration maxCount int throughput int64 closeCh chan struct{} closeOnce sync.Once } func NewRampUp(initial int64, rate float64, interval time.Duration, maxCount int) *RampUp { return &RampUp{ rate: rate, interval: interval, maxCount: maxCount, throughput: initial, closeCh: make(chan struct{}), } } func (r *RampUp) Start() { ticker := time.NewTicker(r.interval) go func() { defer ticker.Stop() count := 0 for { select { case <-r.closeCh: return case <-ticker.C: if count >= r.maxCount { continue } atomic.StoreInt64(&r.throughput, int64(float64(r.throughput)*r.rate)) count++ } } }() } func (r *RampUp) Throughput() int64 { return atomic.LoadInt64(&r.throughput) } func (r *RampUp) Stop() { r.closeOnce.Do(func() { close(r.closeCh) }) }RampUp#Startを実行すると内部でGoroutineが起動し、Tickerで5分毎にthroughput値を1.5倍にしています。
テストコード
// +build faketime package faketime import ( "testing" "time" ) func TestRampUp(t *testing.T) { rampup := NewRampUp(500, 1.5, 5*time.Minute, 18) rampup.Start() defer rampup.Stop() time.Sleep(1) if expected, actual := int64(500), rampup.Throughput(); expected != actual { t.Fatalf("throughput - expected:%d, but was:%d", expected, actual) } time.Sleep(5 * time.Minute) if expected, actual := int64(750), rampup.Throughput(); expected != actual { t.Fatalf("throughput - expected:%d, but was:%d", expected, actual) } time.Sleep(5 * time.Minute) if expected, actual := int64(1125), rampup.Throughput(); expected != actual { t.Fatalf("throughput - expected:%d, but was:%d", expected, actual) } time.Sleep(5 * time.Minute) if expected, actual := int64(1687), rampup.Throughput(); expected != actual { t.Fatalf("throughput - expected:%d, but was:%d", expected, actual) } // omit time.Sleep(14 * 5 * time.Minute) if expected, actual := int64(492319), rampup.Throughput(); expected != actual { t.Fatalf("throughput - expected:%d, but was:%d", expected, actual) } time.Sleep(5 * time.Minute) if expected, actual := int64(738478), rampup.Throughput(); expected != actual { t.Fatalf("throughput - expected:%d, but was:%d", expected, actual) } time.Sleep(5 * time.Minute) // exceed the maxCount if expected, actual := int64(738478), rampup.Throughput(); expected != actual { t.Fatalf("throughput - expected:%d, but was:%d", expected, actual) } }愚直にtime.Sleepで5分ずつ時間を進めながら、値の変化をアサーションしています。
Fake timeを使わずにこの様なテストを記述すると、
いつもはパスするのだけど、たまに(マシンが重いときだけ)失敗するみたいな現象が起きたりします。これはSleepしている間にバックグラウンドのGoroutineの処理が実際にどのくらい進むかを制御出来ないからですが、Fake timeを使うとナノ秒単位で厳密にSleepと動作タイミングを制御することができる為何度実行しても結果が同じになります。実行
test --tags=faketime -timeout=100m .テストコード上100分弱かかっている為、timeoutオプションを付与しています。
ただ 実際にはFake timeの為すぐに終了します(スゴイ!!?)テストのタイムアウトも実経過時間ではなくFake timeの擬似経過時間で効いてしまう為、デフォルト10分を超える場合はtimeoutオプション指定が必要になります。
並行処理から使われるコードのテスト
内部でGoroutineを使っているコードのテストだけでなく、複数Goroutineから利用される想定のコードのテストを書くときにもFake timeは有用です。
弊作キャッシュライブラリのテストコードでもFake timeを使っていますので、よかったら参照ください。
https://github.com/knightso/kocache/blob/main/kocache_test.go抜粋↓
func TestGetAndReserve(t *testing.T) { start := time.Now() cache, err := New() if err != nil { t.Fatal(err) } // create utility method to synchronize testing report. var mux sync.Mutex lock := func(f func()) { mux.Lock() defer mux.Unlock() f() } var wg sync.WaitGroup wg.Add(1) go func() { defer wg.Done() if _, err = cache.Get("key1"); err != ErrEntryNotFound { lock(func() { t.Errorf("ErrEntryNotFound expected, but was:%v", err) }) } }() wg.Add(1) go func() { defer wg.Done() time.Sleep(time.Nanosecond) resolve := cache.Reserve("key1") time.Sleep(5 * time.Nanosecond) resolve("value1", nil) }() for i := 0; i < 10; i++ { wg.Add(1) go func() { defer wg.Done() time.Sleep(2 * time.Nanosecond) value, err := cache.Get("key1") if err != nil { lock(func() { t.Error(err) }) } // キャッシュされるのを待機したことをアサーション // ↓の様にナノ秒単位で経過時間を厳密にアサーションできます\(^o^)/ if actual, expected := time.Now().Sub(start), 6*time.Nanosecond; actual != expected { lock(func() { t.Errorf("expected:%v, but was:%v", expected, actual) }) } if actual, expected := value.(string), "value1"; actual != expected { lock(func() { t.Errorf("expected:%v, but was:%v", expected, actual) }) } }() } wg.Add(1) go func() { defer wg.Done() if _, err = cache.Get("key2"); err != ErrEntryNotFound { lock(func() { t.Fatalf("ErrEntryNotFound expected, but was:%v", err) }) } }() time.Sleep(time.Second) wg.Wait() // ↑のSleepで確実に全てのGoroutineが終了しているはずだが念の為 }テストコード内で複数のGoroutineを起動し、ナノ秒単位で時間を調節しながら処理とアサーションを行っています。
通常のテストならば確実に通ることが保証されない書き方をしていますが、Fake timeならば繰り返し実行しても通ります。最後の1秒Sleepで全てのGoroutineが終了しているはずで本来WaitGroupも不要ですが、さすがにちょっと気持ち悪いので念の為Waitしています^^;
あと、ちょっと脱線しますけど、*testing.Tのレポート系メソッド(ErrorとかFatalとか)ってgoroutine-unsafeなんですね。
テストコード上のGoroutine内で書く場合はロックをとる必要があるので注意です。デメリット
昨日の記事でも書きましたが、Fake timeに依存したテストコードは、Fake time以外で正しく動作しなくなる可能性が高いです。
また、Fake timeと相性の悪いコードやライブラリもありそうです。
特に通信系(netやnet/http等)のコードはハングしてしまうのを確認しています(´・ω・`)Fake timeのときのみ実行したいテストにはビルドタグ
// +build !faketimeをつければ除外出来ます。今後いろいろ試して調べていきたいと思っています。
まとめ
- Fake time使うと並行処理(Goroutine)のテストが書きやすい!
- 実行時間も短縮される (場合がある)!
- デメリットもある
使い始めたばかりなので見えていない罠や落とし穴がまだまだありそうですが、今後いろいろ遊んで試してみたいと思います^^






















