go/goreleaserで初めてのCLIツール作り

公開:2020/10/17
更新:2020/10/20
7 min読了の目安(約7000字TECH技術記事

はじめに

初めまして!
普段はvueとかreactなどのウェブフロントエンドを書いている大学生です。
インターンの選考を通るためにgoを身につけなければならなくなったので勉強がてらCLIツールを作成しました。

注意点

goのインストール方法、基本文法、go moduleの設定などは割愛します。
自分が詰まったところを重点的に書いています。

作成したもの

その日の天気と翌日の天気予報を取得できるCLIツールを作成しました。
よかったらみていってください。
mmmommm/tenki

コードについて

cobraなどのパッケージがあるのは知っていましたが、今回はオプションの数も多くないこともあり、flagを使用しました。
極力短くしているつもりですが、少し長いかもしれないです。

main.go
func main() {
	const defaultPrefecture = ""
	var cPrefecture, nPrefecture, string
	var showVersion bool
	flag.StringVar(&cPrefecture, "current", defaultPrefecture, "--curennt <県名> returns current weather information where you are.")
	flag.StringVar(&cPrefecture, "c", defaultPrefecture, "-c <県名> returns current weather information where you are.")
	flag.StringVar(&fPrefecture, "forecast", defaultPrefecture, "--forecast <県名> returns current weather information where you are.")
	flag.StringVar(&fPrefecture, "f", defaultPrefecture, "-f <県名> returns current weather information where you are.")
	flag.Parse()
	// -cオプションの時の処理を書く
	if cPrefecture != "" && fPrefecture == "" {
		tenki.CurrentWeather(cPrefecture)
	// -fオプションの時の処理を書く
	} else if cPrefecture == "" && fPrefecture != "" && {
		tenki.ForecastWeather(fPrefecture)
	} else {
		fmt.Printf("%s: option arguments is empty !! please type prefecture name. \n", os.Args[0])
		fmt.Printf("%s: try 'tenki --help' or 'tenki -h' for more information\n", os.Args[0])
		os.Exit(1)
	}
}

少し省略していますが、main関数はこのようになっており、オプションごとに分岐してtenkiというディレクトリにある関数が実行されるようになっています。
全てのコードはここにあります。

cmd/current.go
func CurrentWeather(cPrefecture string) {
	err := godotenv.Load()
	if err != nil {
		log.Fatal("Error loading .env file")
	}

	var city string
	city = sub.Prefecture(cPrefecture)

	token := os.Getenv("WEATHER_TOKEN")                           // APIトークン
	endPoint := "https://api.openweathermap.org/data/2.5/weather" // APIのエンドポイント

	// パラメータを設定
	values := url.Values{}
	values.Set("q", city)
	values.Set("APPID", token)
	// リクエストを投げる
	res, err := http.Get(endPoint + "?" + values.Encode())
	if res != nil {
		defer res.Body.Close()
	}
	if err != nil {
		fmt.Println(err)
	}
	defer res.Body.Close()
	// レスポンスを読み取り
	bytes, err := ioutil.ReadAll(res.Body)
	if err != nil {
		fmt.Println(err)
	}
	// JSONパース
	var apiRes OpenWeatherMapAPIResponse
	if err := json.Unmarshal(bytes, &apiRes); err != nil {
		fmt.Println(err)
	}

	aa := sub.ASCIIart(apiRes.Weather[0].Icon)

	fmt.Printf("----------------------------------------\n")
	fmt.Printf("時刻:     %s\n", time.Unix(apiRes.Dt, 0))
	fmt.Printf("天気:     %s\n", apiRes.Weather[0].Main)
	fmt.Printf("詳細:     %s\n", apiRes.Weather[0].Description)
	fmt.Printf("平均気温: %s °C\n", fmt.Sprintf("%.1f", sub.Change(apiRes.Main.Temp))) // ケルビンで取得される
	fmt.Printf("%s\n", aa)
	fmt.Printf("----------------------------------------\n")
}

長くなってしまうのでcオプションの時だけ書きますが、今回はopenweathermapAPIを使用したので.envにAPIkeyを渡してそれをgodotenvを使って取ってきています。
jsonの定義のせいで長くなってしまうので省略します。
全てのコードはここにあります。

sub.ASCIIart()は天気に応じてそれに対応する天気の画像をアスキーアート化したものを取得してくる関数です。
アスキーアートの保存ディレクトリ
sub.Change()はAPIから気温がケルビンで取得されるので-273.15して摂氏に変換する関数です。
fmt.Sprintf("%.1f", 何かしらのstring)とすると小数点第一位で切ってくれるので統一するために使用しています。
subディレクトリはこちら

リリースについて

goreleaserを使用しました。

気をつけて欲しいところ

homebrewのformulaを自動で作成してくれるらしいのでやっていたのですが、はじめにGitHubActionsでやっていたところ、artifactのエラーで永遠につまづき、公式のgithubのissueにも解決法が出ていなかったのでCircleCIに変えたところうまく行きました。
以下は私の.goreleaser.yml.circleci/config.ymlです

.goreleaser.yml
before:
  hooks:
    - go mod download
    - go generate ./...
builds:
  - env:
      - CGO_ENABLED=0
    goos:
      - linux
      - windows
      - darwin
archives:
  - replacements:
      darwin: Darwin
      linux: Linux
      windows: Windows
      386: i386
      amd64: x86_64
checksum:
  name_template: 'checksums.txt'
snapshot:
  name_template: "{{ .Tag }}-next"
changelog:
  sort: asc
  filters:
    exclude:
      - '^docs:'
      - '^test:'
brews:
  -
    tap:
      owner: mmmommm
      name: homebrew-tenki
    description: Returns weather forecast in perticular prefecture.
    homepage: “https://github.com/mmmommm/tenki”
    commit_author:
      name: goreleaserbot
      email: goreleaser@carlosbecker.com
    install: |
      bin.install "tenki"
.circleci/config.yml
version: 2.1
workflows:
  main:
    jobs:
      - release:
          # Only run this job on git tag pushes
          filters:
            branches:
              ignore: /.*/
            tags:
              only: /v[0-9]+(\.[0-9]+)*(-.*)*/
jobs:
  release:
    docker:
      - image: circleci/golang:1.15
    steps:
      - checkout
      - run: curl -sL https://git.io/goreleaser | bash

リリース時に詰まったこと

公式をみていなかったこと

qiitaなどにたくさんの記事がありますがbrewは更新されてbrewsになっていますし、きちんとドキュメントをみて対応したものを書かないとエラーを吐かれます気をつけてください、現時点(2020-10-17)ではこれで動きました。

homebrew-ツール名のリポジトリ作成

goreleaserbotがコミットしてくれなくて詰まりました。
手順としては

  • homebrew-ツール名のリポジトリを作成する。
  • git initなどしてコミットできるようにしておく。
  • goreleaserbotをコラボレータとしてリポジトリに招待する。
  • タグ名をつけてツール側のリポジトリのコードをpushする

をすればよいです、この辺ドキュメントに書いてなくて意味わからなかったです。
私のツールのhomebrew-tenki

招待の仕方はhomebrew-ツール名のリポジトリのsettingsから

Manage accessを押して右側にあるinvite a collaboratorを押し
goreleaserbotと検索して追加すればできます。

最後に関してはgoreleaserのドキュメントにも書いてありますが
1  何かしらコードを書く
2 git add git commitする。
3 git tag v1.0.0する。(自分のリリースの次のtagです。最初ならv0.0.1)
4 git push origin v1.0.0する。(それぞれ上のでつけたtagを指定してください)
5 git push origin mainする。(ブランチ切っているのであればgit push origin head
をすれば良いです。
CIがtagをプッシュした時に動くようになっているのでtagをプッシュしないと動きません。
tagをプッシュした後にリポジトリをみにいったらgoreleaserbotがcommitしてるはずです。

CircleCIにGITHUB_TOKENを設定してなかった

goreleaserを動かすのにGITHUB_TOKENが必要なので設定する必要があります。

やり方は
dashboardのここからProjectSettingsを開き

こんな画面に行くと思うので

左のEnvironment Variablesを選んでGITHUB_TOKENという名前でAPIKeyを登録すれば良いです。

GITHUB_TOKENはgoreleaserの設定の時に取得しているので大丈夫だと思いますが取得方法がわからない場合はこちらを見ればできると思います。

token取得時には権限でrepoにチェックをする必要があります。

確認

CIなどの実行がうまくいったらターミナルなどで
brew tap {ユーザー名}/{ツール名}
brew install {ユーザー名}/{ツール名}/{ツール名}
をして動作確認をしてみてください。

これできちんとインストールできればhomebrewに公開されています、おめでとうございます🎉🎉

最後に

ある程度文法はわかっていましたがgoで作成したのは初めてだったので詰まりながらも進めることができました。(まだ完成していませんが)
詰まっている方の手助けになれば幸いです。