ソースコードから理解する技術-UnderSourceCode

手を動かす(プログラムを組む)ことで技術を理解するブログ

Golangで2つのgzファイルを連結してみる

gzファイルはコマンドだと以下のように連結することができます。

cat sample1.gz sample2.gz > result.gz

これと同じようにgzファイルを連結する処理をGolangで書いてみました。
検索すれば似たようなことをやっている記事は沢山出てくるかと思いますが、備忘録替わりに載せておきます。

ソースについて

「sample1.json.gz」「sample2.json.gz」というgzファイルをあらかじめ用意しておき、実行すると中身が連結されて「result.json.gz」というファイルに出力されます。
今回はファイルの中身をjsonとしたのですが、特にjson形式を解析して何かを行っているわけではないので、中身の形式は問わないと思います。

ソースは以下になりますが、io.Copy()を使ってio.Readerをio.Writerにコピーしているところや、io.WriterのWrite()で改行を書き込んでいるところなどは
Golangのio周りを意識するのにちょうど良かったように感じました。

package main

import (
	"compress/gzip"
	"fmt"
	"io"
	"log"
	"os"
)

func write(zr1 *gzip.Reader, zr2 *gzip.Reader) error {
	writeFile, err := os.OpenFile("./result.json.gz", os.O_WRONLY|os.O_CREATE, 0644)
	if err != nil {
		return err
	}
	defer writeFile.Close()

	zw := gzip.NewWriter(writeFile)
	defer zw.Close()

	_, err = io.Copy(zw, zr1)
	if err != nil {
		return err
	}

	_, err = zw.Write([]byte("\n"))
	if err != nil {
		return err
	}

	_, err = io.Copy(zw, zr2)
	if err != nil {
		return err
	}

	return nil
}

func main() {
	readFile1, err := os.Open("./sample1.json.gz")
	if err != nil {
		log.Fatal(err)
	}
	defer readFile1.Close()

	zr1, err := gzip.NewReader(readFile1)
	if err != nil {
		log.Fatal(err)
	}
	defer zr1.Close()

	readFile2, err := os.Open("./sample2.json.gz")
	if err != nil {
		log.Fatal(err)
	}
	defer readFile2.Close()

	zr2, err := gzip.NewReader(readFile2)
	if err != nil {
		log.Fatal(err)
	}
	defer zr2.Close()

	err = write(zr1, zr2)
	if err != nil {
		log.Fatal(err)
	}

	fmt.Println("-----finish-----")
}

Golangで構造体を定義せずにjsonを読み込んでみる

ふとした時にGolangjsonを読み込む話になり
事前に構造体を定義しないでreflectで読み込んで云々・・・みたいな話になりました。

そう言えばGolangでreflectをまともに使ったことがないなあ、と思い
勉強がてらサンプルソースを作ってみました。

以下、サンプルソースとその機能について、備忘録替わりに書いておきます。

機能について

サポートされるログと検出されるフィールド - Amazon CloudWatch Logs

こちらにある CloudWatch のjson形式のログを読み込むことにしました。
最初に書いたようにjsonに対応する構造体は定義せず、reflectを使いjsonを読み込みます。
ネストした任意のjsonを読み込み、キー・値・型を取得する機能を想定しています。

実行には上記のjsonを「sample.json」という名前で保存しておく必要があります。

サンプルプログラム

以下のようになりました。

package main

import (
	"encoding/json"
	"fmt"
	"io/ioutil"
	"log"
	"reflect"
)

func doReflect(interfc interface{}) {
	r := reflect.ValueOf(interfc)

	if r.Kind() == reflect.Map {
		iter := r.MapRange()
		for iter.Next() {
			k := iter.Key()
			v := iter.Value()
			_, isStr := v.Interface().(string)
			_, isNum := v.Interface().(float64)
			if isStr || isNum {
				fmt.Printf("keys = %v, val = %v, type = %v\n", k, v, reflect.TypeOf(v.Interface()))
			} else {
				doReflect(v.Interface())
			}

		}
		return
	} else if r.Kind() == reflect.Slice {
		for i := 0; i < r.Len(); i++ {
			doReflect(r.Index(i).Interface())
		}
	}
}

func main() {
	bytes, err := ioutil.ReadFile("./sample.json")
	if err != nil {
		log.Fatal(err)
	}

	var contents interface{}
	err = json.Unmarshal(bytes, &contents)
	if err != nil {
		log.Fatal(err)
	}

	doReflect(contents)
}

実行すると、jsonのキー・値・型が出力されるかと思います。
型は今回はstring、floatのみですが、jsonによっては型を増やすことも可能かと思います。

gorunでGolangのソースをシェルスクリプトのように実行してみる

https://github.com/erning/gorun
gorunをちょっと試してみました。試した内容としては

  • gorunをローカルにgo getで入れる
  • Golangでソースを書くが、importで他のライブラリを使うものとする
  • ソースをGOPATHの外の場所に置き、シェルスクリプトのようにソースを指定して実行してみる

となります。

他のライブラリを使ってみたのは、Golangで処理を書く場合に単体で書くことは少ないと思ったためです。
以下、やってみたことについて纏めてみます。

やってみたこと

1. gorunなど必要なものを取得

「go get」で必要なものを取得します。今回は「gorun」と、ソースの中で「go-linq」を使ったので、この2つを「go get」で取得します。
https://github.com/ahmetb/go-linq

$ go get github.com/erning/gorun
$ go get github.com/ahmetb/go-linq/v3

2. gorunの取得先を確認

「go get」で取得すると、gorunの本体が「$GOPATH/bin/」配下に取得されます。この「$GOPATH/bin/」にパスが通っているか、もしくは「gorun」がパスが通っている任意の場所に入っていることを確認します。

3. ソースを書く

任意の処理を通常のGolangのように「~.go」のファイル名で実装します。ただし先頭行には「#!/usr/bin/env gorun」を記載します。
これ以外は通常のGolangの処理そのままです。私は 先に挙げた「go-linq」のREADMEのソースをそのまま使いました。

なお、エディタによっては先頭行の「#!/usr/bin/env gorun」があると入力補完やフォーマットが効かないこともあるようです。
この場合は先頭行をコメントアウトして実装し(実装中は「$ go run ~.go」などで動作確認し)、実装が終わったら先頭行を復活させればいいでしょう。

4. ソースの実行

GOPATH以外でも動くかを確認するため、ソースを任意の場所にコピーします。
ソースに実行権限をつけ、「$ ./~.go」とシェルスクリプトのようにファイル名を指定して実行してみます。

まとめ

上記のような手順で、Golangのソースをビルドしてバイナリを作成することなく、シェルスクリプトのようにファイル名で実行することができました。
シェルで書くには複雑だが、インフラ作業などで処理手順としていつでも見れるようにソースとして配置しておいて実行したい、みたいなケースでは使えるかもしれません。

「よくわかるHTTP/2の教科書」を読んだ

GWの最後の2日で「よくわかるHTTP/2の教科書」を読みました。
http://www.ric.co.jp/book/contents/book_1177.html

HTTP/2に限らず、HTTP周りについて非常に分かりやすく書かれていると感じました。

以下、ほぼ「分かりやすかった」の羅列になっていますが、備忘録替わりに各章ごとに注目した部分の感想を書いておきます。

感想

第2章

HTTP・TLSTCPという、「裏」で動いているプロトコルについて、非常に分かりやすかったです。

第3章

HTTPについて。初期バージョンは古く1990年代には使われていましたとあります。
余計な感想かもしれない、かつインターネットがメジャーになった年代を考えれば当然だが、工業製品の歴史と比べると新しいと感じました。

CookieのセッションIDの例や、KeepAliveについては分かりやすいと感じました。

第5章

HTTP/2を解説している、本書のメイン部分。
最初のHTTP/2の概要で、HTTP/2の独自機能が分かりやすく書かれています。
HTTP/2のストリームとフレーム、コネクションについて分かりやすかったです。

第6章

WebSocket、HTTP Live Streaming、Open ID Connectなどについて分かりやすく書かれています。
QUICについてはほぼ初見だったが、こちらも非常に分かりやすかったです。

まとめ

アプリケーション周りの実装を行っているだけだと、HTTP周りのプロトコルについては
フレームワークなどで隠蔽されており、詳細までは意識しないことが多いかと思います。
この辺りを改めて意識する or HTTP周りの知識を整理するのに、いい本だと思いました。

aws cdk でAthenaのNamed Queryを作成する

AWS Athenaにはよく使うクエリを保存する Named Queryという機能があります。マネージメントコンソール上では「Saved Queries」というタブに表示されます。

今回はこのNamed Queryをaws cdkでデプロイしてみました。aws cdkを使ったのは、クエリは特に開発時は何度も変更するため、cdkを使って繰り返しデプロイできるようにしたら開発効率が上がるのではないかと思ったためです。(もちろん画面にて更新することもできますが)

Named Query以外の、データベースやテーブルなどは、今回はマネージメントコンソールから手動で作成しました。以下、今回の作業の流れとなります。

  • S3のバケットを作成し、検索元データのCSVをアップロードする。
  • Athenaの画面にてデータベースを作成する。
  • Athenaの画面にてテーブルを作成する。
  • クエリ結果の保存先「Query result location」をAthenaの画面にて作成する。
  • Named Queryをaws cdkで作成、デプロイする。

以下、それぞれについて書いていきます。

データベース作成

最初のS3のバケット作成、データのCSVのアップロードについては省きます。データのCSVを事前にS3にアップロードしておきます。
データベースについては、Athenaの「Query Editor」に以下のcreate database文を流すことで作成しました。(今回はsample_carsというデータベース名とします。センス無いですが、テーブル名も同じにしました。)

create database sample_cars;

なおデータベースを作成しないと、以下のテーブル作成で使う「Create table」のリンクが出てきませんでいた(私の作業ミスの可能性もありますが。明示的にデータベースを選ばなければならなかったのか?)

テーブル作成

Athenaの画面の左側に「Create table」というリンクがあるので、クリックします。カラム名と型を指定できるので、CSVの中から取得したい対象のカラムを指定して、画面にてテーブルを作成します。画面に表示されるCreate文は以下で使うので保存しておきます。

画面にて作成されたデフォルトの状態だと、CSVの一行目が列名を示すヘッダーとなっている場合、ヘッダーが検索結果として抽出されてしまいます。これを防ぐ為には、画面にてテーブルを作成したときに表示されたCreate文を保存しておき、「'skip.header.line.count'='1'」をプロパティに追加して再度実行します。Create文は以下のようになります。

CREATE EXTERNAL TABLE IF NOT EXISTS sample_cars.sample_cars (
  `id` int,
  (中略)
)
(中略)
TBLPROPERTIES (
  'skip.header.line.count'='1', -- これを追加
  'has_encrypted_data'='false'
);

Query result location

クエリの実行結果の保存先を「Query result location」で指定します。画面右上の「Settings」をクリックするとダイアログが出てくるので、そこで指定します。

aws cdkでNamed Queryを作成

プロジェクト作成

さて本題のNamed Queryです。aws cdkでのプロジェクト作成は、以下の公式チュートリアルや、以前書いた記事を参考にしてください。
Getting Started With the AWS CDK - AWS Cloud Development Kit (AWS CDK)
aws-cdkを触ってみた - ソースコードから理解する技術-UnderSourceCode

aws cdkのプロジェクトフォルダを作成し、以下を実行します。

$ npx cdk init app --language=typescript
$ npx cdk --version

今回はaws cdkの「aws-athena」モジュールを使うので、以下のコマンドでインストールします。

$ npm install @aws-cdk/aws-athena

cdkのプロジェクトが作成されると、「lib」フォルダ内の.tsファイルに「The code that defines your stack goes here」というコメントがあります。ここに任意の作成したいコードを記述します。今回はAthenaのNamed Queryを作成するコードを記載します。.tsファイルは以下のようになりました。

import * as cdk from '@aws-cdk/core';
import athena = require('@aws-cdk/aws-athena');

export class CdkAthenaQueryStack extends cdk.Stack {
  constructor(scope: cdk.Construct, id: string, props?: cdk.StackProps) {
    super(scope, id, props);

    // The code that defines your stack goes here
    new athena.CfnNamedQuery(this, 'MySampleCars', {
      database: 'sample_cars',
      description: 'this is a sample named query.',
      name: 'sample-cars-query',
      queryString: 'select id, maker, model, transmission_type from sample_cars.sample_cars limit 10;',
    })
  }
}

「sample_cars」データベースを指定し、「queryString」にてSelect文を定義しているのが分かるかと思います。次にこれをデプロイしてみます。

Named Queryのデプロイ

デプロイについては標準的なaws cdkの手順通りです。私は以下のコマンドで実行しました。

$ npm run build
$ npx cdk synth
$ npx cdk deploy

デプロイを実行すると、Athenaの画面の「Saved Queries」タブに、クエリが出来ているはずです。選択して実行できることを確認してみてください。

次にクエリの変更についてです。cdkの「queryString」のSelect文を変更し、以下のコマンドを実行します。

$ npm run build
$ npx cdk synth
$ npx cdk diff
$ npx cdk deploy

最初の「npm run build」を忘れると変更が反映されないので注意です。「diff」を使うと変更箇所が分かります(今回はクエリになるはずです)。デプロイして、画面からクエリを実行して結果が変わっていれば成功です。

gofakeitでサンプルデータを作ってみる

GolangでサンプルデータのCSVを作成したかったので、いくつかライブラリを検索したところ、「gofakeit」というライブラリを見つけました。
GitHub - brianvoe/gofakeit: Random fake data generator written in go

READMEを見れば使い方は分かりますが、備忘録として私が作ったものも載せておきます。
やったことは以下になります。

  • 10件ほどのサンプルデータをCSV形式で作成
  • データはgofakeitを使ってランダムに作られるようにする
  • CSVの出力はgocsvを使う

以下、ソースと実行方法です。

インストール

$ go get -u github.com/gocarina/gocsv
$ go get github.com/brianvoe/gofakeit/v5

ソース

package main

import (
	"fmt"

	"github.com/brianvoe/gofakeit/v5"
	"github.com/gocarina/gocsv"
)

type Car struct {
	ID               int    `csv:"id"`
	Maker            string `csv:"maker"`
	Model            string `csv:"model"`
	TransmissionType string `csv:"transmission_type"`
}

func main() {
	gofakeit.Seed(0)

	cars := []*Car{}

	for i := 0; i < 10; i++ {
		car := &Car{
			gofakeit.Number(1, 100),
			gofakeit.CarMaker(),
			gofakeit.CarModel(),
			gofakeit.CarTransmissionType(),
		}
		cars = append(cars, car)
	}

	data, err := gocsv.MarshalString(&cars)
	if err != nil {
		panic(err)
	}
	fmt.Println(data)
}

実行

「go build」でビルドし、できたバイナリ名が「main」とします。

$ ./main # 出力されるデータを確認する
$ ./main > sample_cars.csv # パイプでcsvに出力する

「Code Craft エクセレントなコードを書くための実践的技法」を読んだ

「Code Craft エクセレントなコードを書くための実践的技法」を読みました。
https://www.amazon.co.jp/dp/B00P7R545M/ref=dp-kindle-redirect?_encoding=UTF8&btkr=1

私が読んだのはKindle版ではなく、紙の本ですが。。。
2007年に初版発行された本ですが、現在でも通用する名著だと思いました。
以下、読んだ時に気になったところを各章毎にメモ書き形式で挙げておきます。読み進めると納得できるような記述が(以下に挙げること以外にも)次々と出てくるところが名著である由縁だと思いました。

第1章 守りを固める

「防衛的プログラミング」について。防衛的プログラミングではないことについて、エラーチェックや(ユニット)テストを挙げている。
本書でも触れられているが、アサーションによるエラーチェックや、厳しいユニットテストが防衛的プログラミングだと勘違いすることが多い(私もそうだった)。
詳細にはここでは書かないが、本書でいう「防衛的プログラミング」はコーディングスタイル、適切なデータ構造、カプセル化コンパイルエラーを全てONにするなどといった安全にプログラムを書く基本を守ったプログラミングのことである。

第2章 見事に描かれた設計図、第3章 名前の意義

この辺りは最近だとフォーマッタの適用や、「名前重要」という言葉などで言われていることだと思う。
勿論、フォーマットの形式を決めることなど、プログラミングにおいて意識しておく必要があることではある。

第4章 必要な情報を余さず書く

関数から早めにreturnする文を書くこと、安易な「最適化」は読みやすさを損なうこと、適切な名前を使用することについて書かれている。
「コードをアトミックな関数に分解する」の項目は、こちらも参考になると思う。
https://speakerdeck.com/sonatard/coheision-coupling

第5章 的確なコメント

コメントについて扱っている章で「この節も読み飛ばさないでください!」って書いてあった(笑)。コメントを軽視しがちなウチらの心理を見透かしてるなと思った。(実際に自分は読み飛ばしそうになった)。

第7章 プログラマーの道具箱

「適度な大きさのプロジェクトの作業を、これまでと違った環境で」やってみることを勧めている。
言語について。言語は「それ自体がツール」だとハッキリ書いてある。(エディタとかのツールを説明してきた流れで)。そして数種の言語を学ぶことを書いている。まあこの辺りは、一つだけの言語で済む仕事は少ないはず…。

第8章 テスト

「コードを書きながら」テストすることを訴えている。いわゆる「テストファースト」にも触れているが、絶対視はしていないようだ。このあたりは自分も同意見。

第9章 誤りの検出

本章より巻末の方が興味深い。デバッガを使うべきタイミング、デバッガで無闇やたらとソース内を動くのは危険というのには納得。
デバッガを使う頻度について、年中は多すぎ、使わないのは少なすぎとある。

第11章 速さを追い求める

パフォーマンスについては、開発の開始時点から考えるべきだと書いてある。これについては同意だが誤解していた。。。
とはいえ、「コードの最適化は必ず代償を伴うもの」とも書いてある。

第12章 不安の固まり

この章はセキュリティに関してだったが、今のアプリをクラウド上で実行する仕事だと、クラウドでのインフラ設定でセキュリティを確保することが多いと思う。勿論、SQLインジェクション対策などプログラムでのセキュリティホールは作らない前提とはなるが。

最初の方の章になるが、読みやすいコードを書く上での適切な関数分割やコメントなどについて言及しているところがあった。高級言語によるプログラムでの考え方は分かったが、yamlなど設定ファイルでの読みやすさの担保の方法も知りたい。

第13章 グランドデザイン

プログラミングは設計作業であると書いてある。ただし「テキストエディタは書くべき内容を練る場所ではない」とも書いてある。

第14章 ソフトウェアアーキテクチャ

システムアーキテクチャは関係者全員が見れる所にドキュメントとして記述することとある。全体像が一枚絵になっていないシステムって多いんだよなとか思っていたら、その後に優れたアーキテクチャは「一片のエレガントな図に要約できるかどうかが鍵」って書いてあった。

第15章 進化か革命か?

保守によるソースの修正で「老朽化」することを指摘している。「警告のサイン」の項であげられる事は、あるある過ぎると思った。
コードが混乱する理由は「複雑だから」とある。この章の最後に保守について、良くないコードを扱う覚悟と、書いた人の「メンタルモデル」を理解することが書かれている。

第17章 チームの力

チームワークは優れた開発者に不可欠であるとハッキリ書いてある。
大切だと思うので、いくつか抜粋する。
「ハイクオリティなプログラマーになるためには、ハイクオリティなチームプレイヤーにならなくてはなりません。」
「そして、有益なチームメンバーとなるためには、技術以外のさまざまなスキル、特質、姿勢をまずは伸ばさなくてはなりません。」
プログラミング言語に対する技能や設計能力について考えるのは、その後です。」

コードの特定部分の「所有者」のように振る舞うことの弊害についても書いてある。この「チームの力」の章は、大切な章だと思った。

最後に一つ抜粋。良いプログラマーについては「ソフトウェアシステムの向上に役立つのであれば、どんな種類の開発作業でもこなす」。

第19章 仕様の具体化

仕様の具体化について。要求仕様書の内容について「離散要求」「非離散要求」に分かれると書いてある。これ、いわゆる「機能要件」「非機能要件」のことだと思う。

仕様書については「本当に必要なドキュメントのみ」書くべきだと言っている。これには同意。システムの全体像、インフラ構成など、ソースから読めない情報がドキュメント化されていないのは困る(大抵、有識者を探して聞くことになる)。

仕様書を「誰も読んでくれない」から書かないのではなく、「『あなたの』脳を活性化」するために書くべきと言っている。また仕様書は後々まで残り、後任の役にも立つだろうと。。

「仕様書を書く時間がなければ、まともなコードを書く時間もないと思え」ともあった。

第20章 美しき獲物たちか?

レビューについての章。「『批判に晒されていない人は、大したことをしていないのだろう』 - Donald H. Rumsfeld」との記述が、章の最初にある。

第21章 完了日は未定

見積もりについては、例外なく経験からの推測に過ぎないと、まず最初に書いてある。見積もりの精度を上げる方法として、作業内容をできるだけ小さい単位に分割する、分割した単位ごとに人時 or 人日の見積もりをする、単位ごとに見積もりを合算する、とある。

この章の最後に、気を散らすものとしてメールや電話を挙げ、断ることを学ぶとあるが、一方で別の章ではコミュニケーションの大切さも説いていた。今だとチャットとかだろうけど、この辺は万能な対応策は無いんだろうと思う。

スケジュールに対して前向きで楽観的であり続けることも大切だと書いてあるが、、、ここもやはり万能な策が無いが故の精神論になるのだと思う。

見積もりについては以前読んだ「ソフトウェア見積り 人月の暗黙知を解き明かす」も大変参考になった。
https://www.amazon.co.jp/ebook/dp/B00KR96M6K

第24章 次の行き先は?

最後の章。コーディング能力を高める方法は「習うより慣れろ」しかないと書いてある。


どの章も大切なことが沢山書かれていましたが、中でも1章と17章は特に重要だと思いました。