たなしょのメモ

日々勉強していることをつらつらと

「動かして考えればよくわかる高効率言語 Rust 書きかた・作りかた」を読んだので所感

動かして考えればよくわかる高効率言語 Rust 書きかた・作りかたを読みました。
Rustについては以前microsoftのRust の最初のステップを読んで以来なので勉強になりました。
章ごとに感想を書いていこうと思います。

1章

Rustの概要やインストール方法、開発環境、PythonとRustの違いを学んだあと、
四則演算を使ってシーザー暗号を使用するツールを作りました。
手を動かす記載がある通り実際に写経して動いたときは面白かったです。

2章

実際ライブラリツールcargoを使用してtreeコマンドを作成しました。
このtreeコマンドは私の方でも再度書き直してwoodを作成しました。

3、4章

所有権やメソッド、ジェネリクス、トレイトについて学びましたが今も所有権についてはわからないことが多く、
未だにvscodeに怒られながらソースコードを書いています。

5章

ここからはものを色々作れる章だったので、とても楽しく読みながら写経をしました。
簡単なチャットアプリを作成するページではチャットサーバーとチャットクライアントを作成してメッセージをやり取りする際のロジックは参考になりました。
webフレームワークはページの都合上少ししか載っていなかったので別の本を読んで補完したいと思います。
最終的にはRustでAPIを作成してみたいですね。

6章

パーサージェネレーターを使用してミニ言語を作成して、webassemblyでweb画面に表示しました。
ミニ言語の作成は理解できないところも多々あったのですが、webassemblyはRustで書いたコードをwebに表示できるとても便利なものだと理解できました。
webassemblyもまた勉強してみたいです。

まとめ

写経好きの自分にはとても良い本でした!
アプリを書いて動かした後、細かく処理の説明が書いてあるので言語の仕様を理解するのに役立ちました。
これからもRustでアプリを作っていきたいです!

GoでJsonファイルを取り扱う際に苦労したのでメモ

Goを使って今書いているCLIのツールで設定ファイルをJsonで書いているのですが、
配列の中にネストされているJsonデータを取り扱いできない問題が発生したのでここに解決策をメモしておきます。

問題のJsonは下記のようなファイルです。

{
    "settingArray": [ 
        {
            "id": 1,
            "mainRepository": "/home/AutoBranchUpdate/src",
            "logRepository": "/home/AutoBranchUpdate/src",
            "masterBranch": "master",
            "logName": "sampleLog",
            "targetRepository": [
                "feature",
                "feature2"
            ]
        }
}

文字列配列のJsonファイルからデータを取り出す

まずは以下のJsonファイルからデータを取り出します。

{
    "testArray": [
        "a",
        "b",
        "c"
    ]
}

このような配列のみのJsonファイルの場合、Goで宣言する構造体は下記のようになります。

type Te struct {
    TestArray []string `json:"testArray"`
}

出力処理は下記のようになります。(ファイル名以外共通の出力処理です。以降は省略します。)

func main() {
    raw, err := ioutil.ReadFile("./test.json")
    if err != nil {
        fmt.Println(err)
        os.Exit(1)
    }

    var test Te
    if err = json.Unmarshal(raw, &test); err != nil {
        fmt.Println(err)
        os.Exit(1)
    }

    fmt.Println(test)
}

実行結果は以下のようになり、データを取得できています。

{[a b c]}

複数のオブジェクトを配列でもっているJsonファイルのデータを取り出す

ここからが本題になります。
複数のオブジェクトを配列で持っている下記のようなJsonファイルからデータを取り出します。

{
    "testArray": [
        {
            "id": 1,
            "name": "test"
        },
        {
            "id": 2,
            "name": "tanaka"
        }
    ]
}

このような配列の場合、構造体の配列を宣言すれば解決できました。
Te2構造体はTestArrayフィールド持ちます。TestArrayフィールドの中身はData構造体の配列です。
Data構造体にはid, nameのフィールドを持っています。

type Te2 struct {
    TestArray []Data `json:"testArray"`
}

type Data struct {
    Id   int    `json:"id"`
    Name string `json:"name"`
}

出力結果は下記のようになります。

{[{1 test} {2 tanaka}]}

複数(文字列配列も含む)のオブジェクトを配列でもっているJsonファイルのデータを取り出す

最初に提示した複数(文字列配列も含む)のオブジェクトを持っている下記のJsonファイルからデータを取り出します。

{
    "testArray": [
        {
            "id": 1,
            "name": "test",
            "notion": [
                "pochi",
                "pochi"
            ]
        },
        {
            "id": 2,
            "name": "tanaka",
            "notion": [
                "chi",
                "chi"
            ]
        }
    ]
}

先ほどの構造体とほぼ同じです。 追加点はData構造体にはNotionの文字列配列のフィールドを追加しました。

type Te3 struct {
    TestArray []Data2 `json:"testArray"`
}

type Data2 struct {
    Id     int      `json:"id"`
    Name   string   `json:"name"`
    Notion []string `json:"notion"`
}

出力結果は下記のようになります。

{[{1 test [pochi pochi]} {2 tanaka [chi chi]}]}

最初に提示したJsonファイルのデータを取り出す

これまで試してきた過程を含めてJsonファイルからデータを抜き出したい場合、
配列に含まれるオブジェクトを持つSettingData構造体を定義します。
上記SettingData構造体の配列をフィールドにもつSetting構造体を定義して準備OKです!

type SettingData struct {
    Id               int      `json:"id"`
    MainRepository   string   `json:"mainRepository"`
    LogRepository    string   `json:"logRepository"`
    MasterBranch     string   `json:"masterBranch"`
    LogName          string   `json:"logName"`
    TargetRepository []string `json:"targetRepository"`
}

type Setting struct {
    SettingArray []SettingData `json:"settingArray"`
}

出力結果は以下のようになり、データが取得できました!

{[{1 /home/AutoBranchUpdate/src /home/AutoBranchUpdate/src master sampleLog [feature feature2]}]}

まとめ

複数のオブジェクトを配列に持つjsonファイルの場合は、 配列内のオブジェクトの構造体を定義した後に、配列内のオブジェクトの構造体の配列を持つ構造体を定義することでデータを取り扱えました。

GW中にVimプラグインを作った

GW中にVimプラグインを作った

http://0xcc.net/unimag/1/ から影響を受けたメモツールを作成しました。
日々のメモや開発メモなどに活用してみてください!
https://github.com/jacoloves/vim-ChangeLog

動作概要

Vimで動作します!(NeoVimでの動作は確認中です。)
・以下の機能を作成しました。

  1. 新しいChangelog.txtファイルを作成します。
  2. Changelog.txtが作成されている場合、現在時刻と名前とメールアドレスを先頭の行に挿入します。
  3. メモを書いた日付を検索します。
  4. キーワードを検索します。(文頭に*が記載されてるものだけです)

使い方

ChangeLogOpen

新しいChangelog.txtファイルを作成します。
Changelog.txtが作成されている場合、現在時刻と名前とメールアドレスを先頭の行に挿入します。

この処理は初めに create_changelog()Changelog.txt の存在を確認してなければ作成する処理になります。
・create_changelog()のソースコード

function! s:create_changelog() abort
   if !filewritable(expand(glob(join([g:changelog_save_path, s:filename], s:sep))))
       execute "redir > " . join([g:changelog_save_path, s:filename], s:sep)
       execute "redir END"
       call s:first_write_date()
   endif
   return
endfunction

その後 check_date() で現在日付が存在するか確認し、存在しない場合は新規記載し、存在する場合はそのままChangelog.txtを開きます。
存在しない場合の新規記載はfirst_write_date()で処理します。
結合した日付ファイルに下記出しています。
・first_write_date()のソースコード

function! s:first_write_date() abort
    if !exists("g:user_full_name") || empty("g:user_full_name")
        let user_name = "anonymous"
    else
        let user_name = g:user_full_name
    endif

    if !exists("g:user_mail_address") || empty("g:user_mail_address")
        let user_mail_address = "anonymous@hogehoge"
    else
        let user_mail_address = g:user_mail_address
    endif

    let title_row = s:time . " " . user_name . " " . "<" . user_mail_address . ">"

    let lines = [title_row, "\t"] 

    call writefile(lines, expand(join([g:changelog_save_path, s:filename], s:sep)))

    return
endfunction

ファイルが作成されていてその続きにメモを記載する場合は、rewrite_date()で現在日付を文頭に挿入します。
first_write_date()と処理が似ていますが、一度配列にメモファイルの中身を取り出してその配列の先頭に新規日付を挿入しています。
rewrite_date()のソースコード一部

" Store all memo data in an array once. 
let write_lines = []
for line in readfile(expand(join([g:changelog_save_path, s:filename], s:sep)))
    call add(write_lines, line)
endfor

" Insert the date in the first row of the array and the tab in the second
" row.
call insert(write_lines, title_row, 0)
call insert(write_lines, "\t", 1)

SearchDatechangeLog

メモを記載した日付を検索します。エンターキーを押すと目的の日付の行にジャンプします。
qを押すとバッファが閉じます。

ChangeLog#searchDate() でメモから抽出した日付をバッファに表示します。
日付を選択すると ChangeLog#jump_date_row() を呼び出して対象の行にシャンプします。
date_searchdict()はパターン解析で日付がある行か確かめ配列に格納する。
・date_searchdict()のソースコード

unction! s:date_searchdict() abort
    let date_lines = []

    let line_cnt = 1 
    for line in readfile(expand(join([g:changelog_save_path, s:filename], s:sep)))
        if line =~ '[1-2][0-9][0-9][0-9]\-[0-1][0-9]\-[0-3][0-9]\s[0-9a-zA-Z]\+\s'
            let composite_str = line_cnt . ": " . line
            call add (date_lines, composite_str)
        endif
        let line_cnt = line_cnt + 1 
    endfor

    return date_lines 
endfunction

ChangeLog#searchDate()date_searchdictで取得した行をバッファに出力します。
qを押すとバッファを閉じて、エンターキー押すとChangeLog#jump_date_row()を実行します。
ChangeLog#searchDate()のソースコード

function! ChangeLog#searchDate() abort
    set nomodeline
    let search_list = s:date_searchdict()

    if empty(search_list)
        return
    endif
    
    if bufexists(s:date_search_list_buffer)
        let winid = bufwinid(s:date_search_list_buffer)
        if winid isnot# -1
            call win_gotoid(winid)
        else
            execute 'sbuffer' s:date_search_list_buffer
        endif
    else
        execute 'new' s:date_search_list_buffer
        set buftype=nofile

        " 1. Press 'q' on SEARCH_DATE_LIST to delete buffer
        " 2. Press 'Enter' to jump target date
        " Define two key mappings.
        nnoremap <silent> <buffer>
                    \ <Plug>(datesearch-session-close)
                    \ :<C-u>bwipeout!<CR>

        nnoremap <silent> <buffer>
                    \ <Plug>(jump-date)
                    \ :<C-u>call ChangeLog#jump_date_row(trim(getline('.')))<CR>

       " <Plug> map to key
       nmap <buffer> q <Plug>(datesearch-session-close)
       nmap <buffer> <CR> <Plug>(jump-date)
    endif

    " Delete all text in the temporary buffer and insert the retrieved date
    " search list into the buffer.
    %delete _
    call setline(1, search_list)
endfunction

ChangeLog#jump_date_rowは対象の行にジャンプして、日付を表示してるバッファを閉じます。
ChangeLog#jmp_date_rowのソースコード

function! ChangeLog#jump_date_row(target_date) abort
    let dates = split(a:target_date, ":")
    let date_row = dates[0]

    let changelog_path = join([g:changelog_save_path, s:filename], s:sep)
    if bufexists(changelog_path) 
        let winid = bufwinid(changelog_path)
        if winid isnot# -1
            call win_gotoid(winid)
        else
            execute 'buffer ' changelog_path
        endif
    else
        execute 'edit ' changelog_path
    endif
    execute date_row
    " delete SEARCH_DATE_LIST buffer 
    execute 'bwipeout!' s:date_search_list_buffer
    return
endfunction

SearchKeywordChangeLog

キーワードを検索します。(文頭に*が記載されてるものだけです)
エンターキーを押すと目的のキーワードの行にジャンプします。
qを押すとバッファが閉じます。

ChangeLog#searchKeyword() でメモから抽出したキーワードを表示します。 キーワードを選択すると ChangeLog#jump_keyword_row() を呼び出して対象の行にジャンプします。

keyword_searchlist()は検索してキーワードを検索して配列に格納します。

function! s:keyword_searchlist(keyword) abort
    let keyword_line_list = [] 
    let line_cnt = 1
    for line in readfile(expand(join([g:changelog_save_path, s:filename], s:sep)))
        if stridx(line, expand(a:keyword)) !=# -1
            let composite_str = line_cnt . ":" . line
            call add(keyword_line_list, composite_str)
        endif
        let line_cnt = line_cnt + 1
    endfor
    
    return keyword_line_list
endfunction

ChangeLog#searchKeyword()keyword_searchlistで取得した行をバッファに出力します。
ソースコードChangeLog#searchDate()と作りが同じ為割愛します。

ChangeLog#jump_keyword_rowは対象の行にジャンプして、キーワードを表示してるバッファを閉じます。
ソースコードChangeLog#jump_date_rowと作りが同じ為割愛します。

所感

Vimのバッファを利用することで便利なメモツールを作成することができました。
今回作成して学んだことを通してまた新しいVimプラグインを作成していきたいです。

Linuxのしくみを読んだの感想を書く

Linuxのしくみを読んだの感想を書く

www.amazon.co.jp

低レイヤーを勉強して行く中でとてもよい書籍だと評判なので読んだ。
結論はとてもわかり易い書籍だった。
特に図が豊富なのとCやShellで書かれたプログラムを実行してタスクの稼働状況見る部分や その他コマンドラインで使われていたコマンドもとても参考になるものが多く勉強になった。
(この書籍のおかげで最近メインの開発PCのOSをUbuntuに変更した。)

1章

カーネルにはどのような機能があるのかの簡単な説明がされいる。
これから各章ごとにその機能についてかぼっていく内容に軽く触れいている。

2章

straceコマンドを使ってCのプログラムが内部でどのように動くのかを解説していて良かった。
Pythonのプログラムでもどうようにstraceしてみてちょっと長くなるけど同様な結果(writeシステムコールが呼び出されている)になることの確認をする。
親プロセスを得るgetppid()やプロセスがユーザーモードカーネルモードどちらかで実行されているか確認するsarコマンドの存在を知ることができた。
(sarコマンドはUbuntuには標準で入っていないのでインストール必要があった)

3章

fork.cというプログラムを作って親プロセスと子プロセスを作成して、メモリの動きをみた。
それぐらいしかできなかった汗

4章

プロセス1つないし複数動かしてCPU時間を使う処理を書いてプロセスの複数実行について学んだ。
プロセス複数あればレイテンシーは遅くなるし単体で動いていれば速くなる。
ps asコマンドでプロセスをkillしていて便利だなと思った。(Linuxを使ってる人には当然できることだと思うんだけど汗)
スケジューラがラウンドロビン方式でないと複数のタスクを実行するととても時間がかかることがわかった。
(どおりで応用情報技術者の問題で頻繁に出題されてるわけだ。)
niceシステムコールでプロセスに優先度をつけることができるらしい。

5章

ページフォールトの説明がわかりやすかった。
ページフォールトが起こるとページフォールトハンドラという処理が動くんだな。
mmapというシステムコールを使って新規メモリを要求するプログラムを書いた。
malloc()は内部的にmmap()を呼び出してるということだ。
メモリを退避する領域をスワップ領域というらしい。(だからArch Linuxのインストールガイドにswapのパーティションを切っている例が記載されているのか)

6章

キャッシュメモリを使用するプログラムを書いた。
データサイズを倍々に増やしていくと処理時間が遅くなっていくよねっていう実験をした。
キャッシュメモリの説明が主で後半でハイパースレッドなどにも触れていた。

7章

ext4、XFSもファイルシステムの仲間なんだな。
ファイルシステム不整合にことが書いてあったがイマイチ理解できなかった…
マルチボーリュームの図がとてもわかり易かった(Arch Linuxのインストールしていたらあーこのこといあってるのねと納得すると思う。)

8章

HDDの書き込みについて説明がされていて、応用情報の午前問題でよく問題をといたなーと懐かしい気持ちになった。
違うセクタにいるときの読み出し方法も図示されていたのでわかりやすかった。
HDDとSSDの呼び出しの速さの違いもグラフにされていてこれもわかりやすかった。

所感

一回で理解するのはなかなか難しいので低レイヤーを学ぶ中で、この本を片手に困ったらこの本を引くことで少しずつ理解できるのではないかと思っている。
この本の中にも紹介されていた「ふつうのLinuxプログラミング」を読む際にもう一度振り返ると思う。

エンジニアのためのGitの教科書を読んだので感想でも書こう

新しく未経験の方が入社して、自分がメンターとして教えることになったので会社で使っている技術を再度復習しておこうと思って読んだ。
今まで自分なんとなくでGitExstensionsを開いて、もしくはPHPStormからgit commitgit pushなどの基本的なコマンドを使っていたので、改めて読んで知らないコマンドや使い方を学べてとてもよかった。

1章

Gitのインストールや基本コマンドを学んだ。
CLIで実際コマンド打つ内容になっており、普段GUIでなんとなくやってしまうこともコマンドを打つことでその機能を学べるのはとても良いことだと思った。
新しく入社する人にも時間が余った時に実際コマンドを打ってもらいながら一連のプロジェクトの作業をやってもらうのがいいかもしれない。
コミットの粒度について書かれおり後述でも言及するがコミット粒度は小さくするのが正義だということらしいので、このことも教えていこうと思う。

以下のコマンドは業務で使えそうだったので使っていこう思う。

  • 直前のコミットの状態に戻す
git reset --hard
  • 削除対象のファイルの確認
git clean -n
  • .gitignoreもすべて含めて削除
git clean -fdx
  • コミットログを一行で見る
git log --pretty=oneline
  • コミットメッセージを修正
git commit --amend

2章

チーム開発で使える知識が書かれていた。
git pullやgit pushについてこの章から出てきた。GitHubなどのリモートリポジトリについてやプルリクエストについても触れいていた。
うちはプルリクエストは行わないのでさらっとよむくらいにとどめた。

下記のコミットの運用ルールについて参考になったので自分の個人開発でも取り入れていきたいと思う。
◆コミット運用ルール 下記の場合もそれぞれ別でコミットする

- フォーマッタの適用
- インデントの修正
- typoの修正
- コメントの修正

また下記コマンドが役立ちそうっだのでメモ

  • 追跡ブランチを設定している
git push --set-upstream origin master
  • 特定のコミット適用する
git cherry-pick <コミット番号>
  • 特定のコミットでブランチを切る
git branch <新ブランチ名> <コミット番号>

3章

開発フローや継続的デリバリのことが書かれていた。
開発フローはプロジェクトごとに違うので軽く流し読む程度にした。
継続的デリバリもGitHubActionsなどCI/CDツールが出てるのでそちらを改めて学習しようと思ったので、ここも軽く流し読む程度にした。
3章の内容は改めてどこかで学習できればなという感じであまり新しい発見はなかった。

最後に

Gitの基礎を抑えるという趣旨ではよかった。
ある程度ブランチの動きが頭で描けるようになったのと知らなかった知識も手に入れることができたので、 新しく入る未経験の人にも一通りのことは教えてあげよう!

HerokuとGitHub ActionsとGoを使って祝日APIを作った話

APIをつくろう

Goを学んでいてせっかくならなにか作ろうと思い、社内のチャットで祝日APIについて話題が出ていたので この機会に祝日APIをGoで作成しました。

今回作る祝日APIPのデータは内閣府国民の祝日についてというサイトのCSVから2021~2023年までのデータを使って作成しています。 「2021」や「2022」と年を入力して検索すると入力された年の祝日一覧がJSON形式で見れるようにしています。

システム構成

f:id:bonashochang:20220409124851p:plain

極力お金をかけずかつDBを使わず、運用にも手間がかからないようにしたかったので - 祝日のデータはGoで構造体として持たせて、処理でJson形式に変換する。 - GitHub Actionsを使ってBuild/Test/Lintの工程を自動で実施してもらって、ある程度の品質を担保する。 - GitHubのmasterリポジトリにマージしたら、自動でHerokuリポジトリにデプロイされるようにして即時公開できるようにする。

という形にしてみました。

今のところ公開したばかりなので問題は出てないですが、データの数や扱うパラメータが増えたらシステム構成からまた見直そうかなと考えています。

## 完成したもの

以下今回の成果物になります。
https://github.com/jacoloves/go-sample-holiday-api

リクエストは2パターンありまして、 - https://go-holiday-api.herokuapp.com/holidayもしくはhttps://go-holiday-api.herokuapp.com/holiday/のどちらかをリクエストすると2021~2023年のすべての祝日のデータを取得する子ができます。 - https://go-holiday-api.herokuapp.com/holiday/year/yyyyをリクエストすることで「yyyy」年の祝日を取得することができます。

上記の2つがあります。

使用例

curl -s https://go-holiday-api.herokuapp.com/holiday/year/2022

レスポンスは以下の通りです。

[
  {
   "Title": "元旦",
   "Date": "2022-01-01"
  },
  {
   "Title": "成人の日",
   "Date": "2022-01-10"
  },
  {
   "Title": "建国記念の日",
   "Date": "2022-02-11"
  },
  {
   "Title": "天皇誕生日",
   "Date": "2022-02-23"
  },
  {
   "Title": "春分の日",
   "Date": "2022-03-21"
  },
  {
   "Title": "昭和の日",
   "Date": "2022-04-29"
  },
  {
   "Title": "憲法記念日",
   "Date": "2022-05-03"
  },
  {
   "Title": "みどりの日",
   "Date": "2022-05-04"
  },
]

カレンダーデータは以下の構造になります。

f:id:bonashochang:20220409124823p:plain

以下今回作ったもの成果物になります。
https://github.com/jacoloves/go-sample-holiday-api

プログラム

今回はGoで構造体を作ってそれをJsonに変換する方法でAPIを作成しました。 jsonライブラリのMarshallIndentが綺麗にデータを整形してくるので便利でした。 https://pkg.go.dev/encoding/json#MarshalIndent

sever.go

後述する祝日が構造体で書かれているGoファイルを読み込んで、dateHandlerという構造体に順々に格納しています。
そのあと取得できたリクエストにより出力の方法を変更しています。

package main

import (
    "encoding/json"
    "log"
    "net/http"
    "os"
    "regexp"
    "sync"
)

var (
    listHolidayRe = regexp.MustCompile(`^\/holiday[\/]*$`)
    getHolidayRe  = regexp.MustCompile(`^\/holiday\/year\/(\d+)$`)
)

type Data []struct {
    Title string `json:"Title"`
    Date  string `json:"Date"`
}

type datastore struct {
    m map[string]Data
    *sync.RWMutex
}

type dateHandler struct {
    store *datastore
}

func (h *dateHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
    switch {
    case r.Method == http.MethodGet && listHolidayRe.MatchString(r.URL.Path):
        h.List(w, r)
        return
    case r.Method == http.MethodGet && getHolidayRe.MatchString(r.URL.Path):
        h.Get(w, r)
        return
    default:
        notFound(w, r)
        return
    }
}

func (h *dateHandler) List(w http.ResponseWriter, r *http.Request) {
    h.store.RLock()
    holiday := make([]Data, 0, len(h.store.m))
    for _, v := range h.store.m {
        holiday = append(holiday, v)
    }
    h.store.RUnlock()
    jsonBytes, err := json.MarshalIndent(holiday, " ", " ")
    if err != nil {
        internalServerError(w, r)
        return
    }
    w.WriteHeader(http.StatusOK)
    _, err = w.Write(jsonBytes)
    if err != nil {
        log.Fatal(err)
        return
    }
}

func (h *dateHandler) Get(w http.ResponseWriter, r *http.Request) {
    matches := getHolidayRe.FindStringSubmatch(r.URL.Path)
    if len(matches) < 2 {
        notFound(w, r)
        return
    }
    h.store.RLock()
    y, ok := h.store.m[matches[1]]
    if !ok {
        w.WriteHeader(http.StatusNotFound)
        _, err := w.Write([]byte("year not found"))
        if err != nil {
            log.Fatal(err)
            return
        }
    }
    jsonBytes, err := json.MarshalIndent(y, " ", " ")
    if err != nil {
        internalServerError(w, r)
        return
    }
    w.WriteHeader(http.StatusOK)
    _, err = w.Write(jsonBytes)
    if err != nil {
        log.Fatal(err)
        return
    }
}

func internalServerError(w http.ResponseWriter, r *http.Request) {
    w.WriteHeader(http.StatusInternalServerError)
    _, err := w.Write([]byte("internal server error"))
    if err != nil {
        log.Fatal(err)
    }
}

func notFound(w http.ResponseWriter, r *http.Request) {
    w.WriteHeader(http.StatusNotFound)
    _, err := w.Write([]byte("not found"))
    if err != nil {
        log.Fatal(err)
    }
}

func main() {
    port := os.Getenv("PORT")
    h1 := holiday_2021()
    h2 := holiday_2022()
    h3 := holiday_2023()

    mux := http.NewServeMux()
    dHandler := &dateHandler{
        store: &datastore{
            m: map[string]Data{
                "2021": Data(h1),
                "2022": Data(h2),
                "2023": Data(h3),
            },
            RWMutex: &sync.RWMutex{},
        },
    }

    mux.Handle("/holiday", dHandler)
    mux.Handle("/holiday/", dHandler)

    if err := http.ListenAndServe(":"+port, mux); err != nil {
        log.Fatal(err)
    }
}

holiday_YYYY.go

各年の祝日を構造体で格納してあるファイルです。
今回は2021、2022、2023の3つのファイルを用意しました。

package main

type Data_2021 []struct {
    Title string `json:"Title"`
    Date  string `json:"Date"`
}

func holiday_2021() Data_2021 {
    holidays_2021 := Data_2021{
        {
            Title: "元旦",
            Date:  "2021-01-01",
        },
        {
            Title: "成人の日",
            Date:  "2021-01-11",
        },
        {
            Title: "建国記念の日",
            Date:  "2021-02-11",
        },
        {
            Title: "天皇誕生日",
            Date:  "2021-02-23",
        },
        {
            Title: "春分の日",
            Date:  "2021-03-20",
        },
        {
            Title: "昭和の日",
            Date:  "2021-04-29",
        },
        {
            Title: "憲法記念日",
            Date:  "2021-05-03",
        },
        {
            Title: "みどりの日",
            Date:  "2021-05-04",
        },
        {
            Title: "こどもの日",
            Date:  "2021-05-05",
        },
        {
            Title: "海の日",
            Date:  "2021-07-22",
        },
        {
            Title: "スポーツの日",
            Date:  "2021-07-23",
        },
        {
            Title: "山の日",
            Date:  "2021-08-08",
        },
        {
            Title: "休日",
            Date:  "2021-08-09",
        },
        {
            Title: "敬老の日",
            Date:  "2021-09-20",
        },
        {
            Title: "秋分の日",
            Date:  "2021-09-23",
        },
        {
            Title: "文化の日",
            Date:  "2021-11-03",
        },
        {
            Title: "勤労感謝の日",
            Date:  "2021-11-23",
        },
    }

    return holidays_2021
}

Test

sever_test.go

今回は一般に公開することもあり、はじめてGoでテストを書きました。
Httpのステータスコードの確認しか行っていませんが、将来的にはデータの確認もしていきたいと思います。
(あともっと綺麗な書き方があったと思うので今後改善していきたいです。)

package main

import (
    "fmt"
    "net/http"
    "net/http/httptest"
    "sync"
    "testing"
)

func TestMain(t *testing.T) {

    h1 := holiday_2021()
    h2 := holiday_2022()
    h3 := holiday_2023()

    dHandler := &dateHandler{
        store: &datastore{
            m: map[string]Data{
                "2021": Data(h1),
                "2022": Data(h2),
                "2023": Data(h3),
            },
            RWMutex: &sync.RWMutex{},
        },
    }
    ts := httptest.NewServer(http.Handler(dHandler))
    defer ts.Close()
    // /holiday pass Test
    resp, err := http.Get(fmt.Sprintf("%s/holiday", ts.URL))
    if err != nil {
        t.Fatalf("Excepted no error, got %v", err)
    }
    defer resp.Body.Close()

    if resp.StatusCode != http.StatusOK {
        t.Fatalf("Expected status code 200, got %v", resp.StatusCode)
    }

    // /holiday/ pass Test
    resp2, err := http.Get(fmt.Sprintf("%s/holiday/", ts.URL))
    if err != nil {
        t.Fatalf("Excepted no error, got %v", err)
    }
    defer resp2.Body.Close()

    if resp2.StatusCode != http.StatusOK {
        t.Fatalf("Expected status code 200, got %v", resp2.StatusCode)
    }

    // /holiday/year/2022 pass Test
    resp3, err := http.Get(fmt.Sprintf("%s/holiday/year/2022", ts.URL))
    if err != nil {
        t.Fatalf("Excepted no error, got %v", err)
    }
    defer resp3.Body.Close()

    if resp3.StatusCode != http.StatusOK {
        t.Fatalf("Expected status code 200, got %v", resp3.StatusCode)
    }

    // /holiday/year/2021 pass Test
    resp4, err := http.Get(fmt.Sprintf("%s/holiday/year/2021", ts.URL))
    if err != nil {
        t.Fatalf("Excepted no error, got %v", err)
    }
    defer resp4.Body.Close()

    if resp4.StatusCode != http.StatusOK {
        t.Fatalf("Expected status code 200, got %v", resp4.StatusCode)
    }

    // /holiday/year/2023 pass Test
    resp5, err := http.Get(fmt.Sprintf("%s/holiday/year/2023", ts.URL))
    if err != nil {
        t.Fatalf("Excepted no error, got %v", err)
    }
    defer resp5.Body.Close()

    if resp5.StatusCode != http.StatusOK {
        t.Fatalf("Expected status code 200, got %v", resp5.StatusCode)
    }

    // /holiday/year/2020 not pass Test
    resp6, err := http.Get(fmt.Sprintf("%s/holiday/year/2020", ts.URL))
    if err != nil {
        t.Fatalf("Excepted no error, got %v", err)
    }
    defer resp6.Body.Close()

    if resp6.StatusCode != http.StatusNotFound {
        t.Fatalf("Expected status code 404, got %v", resp6.StatusCode)
    }

    // /holiday/year/2024 not pass Test
    resp7, err := http.Get(fmt.Sprintf("%s/holiday/year/2024", ts.URL))
    if err != nil {
        t.Fatalf("Excepted no error, got %v", err)
    }
    defer resp7.Body.Close()

    if resp7.StatusCode != http.StatusNotFound {
        t.Fatalf("Expected status code 404, got %v", resp7.StatusCode)
    }
}

GiHub Actions

こちらも初めてGitHub Actionsを使ってみたのですが、とても便利でした。
ymlの書き方がわからなかったのですが、テンプレートを子会してくださっている方がいたのでそれをコピペしてちょっと直せば動きました。
テンプレート元

また初回はGitHubにpushできない事象が発生しましたが、設定でworkflowをチェック入れることで解消しました。 https://zenn.dev/urasaku77/articles/dd94110dd1041a

go-sample-holiday-api.yml

name: Go

on:
  push:
    branches: [ master ]
  pull_request:
    branches: [ master ]

jobs:
  # setup
  setup:
    runs-on: ubuntu-latest
    steps:
      - name: set up
        uses: actions/setup-go@v2
        with:
          go-version: 1.17
        id: go
      - name: check out
        uses: actions/checkout@v2

      - name: Cache
        uses: actions/cache@v2.1.0
        with:
          path: ~/go/pkg/mod
          key: ${{ runner.os }}-go-${{ hashFiles('**/go.sum') }}
          restore-keys: |
            ${{ runner.os }}-go-

  # build
  build:
    needs: setup
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v2
      - name: build
        run: go build ./...

  # test
  test:
    needs: setup
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v2
      - name: test
        run: go test ./... -v

  # lint
  lint:
    needs: setup
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v2
      - name: golangci-lint
        uses: golangci/golangci-lint-action@v2
        with:
          version: v1.29

振り返り

今回初めてGitHub Actionsを使ったのですが便利です!
Build/Test/Lintを自動でやってくれて品質をある程度担保できるので、リリースするまでの心理的なハードルがだいぶ下がります!
今後もなにか作る際は採用していきたいと思います。
久しぶりにHerokuを採用しましたが、簡単なアプリケーションをデプロイする際はとても便利なので、AWSや他Iaasを使うほどでもないものには使っていきたいです。

Rust勉強してみた 10日目

改行ありの長い文章

改行ありの長い文章を代入したい場合は、"\で書き始める。

let contents = "\
Rust:
safe, fast, productive.
pick three.";

cargo build

cargo buildは下記の様に二つあるあって、コンパイラが異なるプロファイルを使用する。

cargo build
cargo build --release

devとreleaseではopt-levelが異なる。
opt-levelは最適化の度合いを制御している。
opt-levelが0だとコンパイルは速いが動作は遅い。
opt-levelが3だとコンパイルは遅いが動作は速い。

[profile.dev]
opt-level = 0

[profile.release]
opt-level = 3

cardo doc --open

下記のように///を記載するとドキュメンテーションコメントになる。

cargo doc --open

上記コマンドでHTMLファイルを生成する。
便利