🐀

Go Modules時代に書籍「Goプログラミング実践入門」を実践するためのまとめ

2021/09/30に公開

とりあえず動かし方だけ知りたい!という方は詰まった章のヘッダに飛んで下さい!💨

動機

インプレス出版のGoプログラミング実践入門はコンセプト、内容共に、Goの基本文法を終えた学習者が次のステップアップとして取り組んでみる題材として素晴らしいものとなっています。
しかし、2017年に出版された当時、Goのバージョンは 1.7が最新でしたが、2021年現在では1.17が最新になっています。同じメジャーバージョン1なので、Goの素晴らしい互換性により1.17のGoでも書籍に掲載されているソースコードは正しいものとなっています。
ですが、書籍の通り進めていってすぐに問題に突き当たります。ソースコードのビルドができない・・・

このことは、正誤表にも少し書いてあるのですが、本全体として2021年現在のGo環境で実践するには補足しておいた方が良い事柄がいくつかあったので、まとめておくことにしました。

Go初学者の人が、本を手に取って、本の通りに動かしてみたら変なエラーが出て動かなかった。それを理由にこの本をやめてしまわないで済む人が増えれば幸いです。

Special Thanks

この記事で紹介している補足は、私がGoプログラミング実践入門のもくもく会を実施したときに参加者の方々が気づいたものです。問題を共有してくれた参加者の皆様、ありがとうございます。

この記事に書いてあること

書籍の内容を2021年現在Goの最新バージョンである1.17で実践するときに、書籍の通りにしては上手くいかないところに焦点を当てます。

動かないところに対し以下を提示します。

  • 動かない原因
  • 動かすために必要な新機能等の知識
  • 動かすための修正例

新機能等の知識については簡単に説明

第1章

1.10 Hello Go

go install コマンドでビルドするように指示がありますが、いきなり動きません😱
最初のHello Worldでいきなり動かないと悲しいですよね😢
動かないのはGo1.11から試験的に導入された Go Modules が長い検証、移行期間を経て、Go1.16からデフォルトでONになったためです。
この本でGoのソースコードをビルドするときに動かないのは、基本的にGo modulesに移行したことによるものなのでもう少し補足しておきます。

go install [package path] はpackage pathのpackageのソースコードを取得し、go build 実行, 出力バイナリを$GOPATH/binに配置するという動きをします。
書籍で指定する first_webapp はドメインが含まれていないので、PC内(具体的には$GOROOT, $GOPATH)にそのpackageが存在しているか検索しますが、無いので cannot find package "first_webapp" というエラーが出ます。

この対処法は2通りあって、書籍全般を通して同じ対処法が適用できます。

  • Go modulesをoffにする (あまりお勧めしません)
  • module mode で動くように変更を加える

現在のGoはGo modulesを使用していくのが標準になので、module modeで書いていくための練習ということでmodule mode対応していくのがお勧めです。

やり方は以下のようにすればいいのですが、詳細については公式のこちらをぜひ一読して下さい。

go mod init first_webapp
go build
first_webapp

ちなみに、 Go modulesをoffにする方法は、 環境変数GO111MODULEoffを渡してやればよく、対象のpackage path は current directoryを指定します。

$ GO111MODULE=off go install . 
$ first_webapp

第2章

第2章の内容は、後続の章で説明している内容を先取りして紹介しているので、第9章まで一通り終えてから改めて戻ってくるとより理解が深まるような内容になってます。

2.6 PostgreSQL のインストール

2021年では、PostgreSQL version 13 が出ているので、書籍の記載通りではなく、ご自身でインストールしたバージョンに合わせて下さい。
PostgreSQL の if exists に関しての文法が変わっているので、そこだけSQLの書き換えが必要なことに注意すれば最新の13をインストールして進めても問題にはなりません。

2.8 サーバの起動

go build をするように指示がありますが、書籍通りのコマンドでは動きません。
1.10 Hello Go が動かないのと原因は同じなので module modeの場合、対応はほとんど同じです。

Go modulesをoffにして動かしたい場合は一つ手順を加える必要があります。
このサンプルは サードパーティのpackage github.com/lib/pq を参照しているので、これをgo get してソースコードをダウンロードしておく必要があります。

GO111MODULE=off go get github.com/lib/pq
GO111MODULE=off go build

書籍通りのPostgreSQLのバージョンをセットアップしていない場合、データベースに繋がらないようなエラーが出てサンプルが動かない可能性があります。
ここでデータベース接続情報を与えているのですが、ユーザー・パスワードはデフォルトの値を使うようになっていますが、デフォルトの想定が異なる場合は繋がりません。
例えば以下のように明示してあげれば繋がります。

Db, err = sql.Open("postgres", "user=root password=root dbname=chitchat sslmode=disable")

第3章

3.4 HTTP/2 の使用

手元のcurlによっては、--http2 に対応していない場合があるようです。
nghttp2 の表示があれば問題ないようです。

$ curl --version
curl 7.64.1 (x86_64-apple-darwin20.0) libcurl/7.64.1 (SecureTransport) LibreSSL/2.8.3 zlib/1.2.11 nghttp2/1.41.0

第4章

4.2.3 MultipartForm

動作しないということはないのですが、multipart/form-data を指定したフォームでの実行結果が書籍と一致しません。
これはこのissuePostFormValue() の動作が修正されているためです。

第6章

6.4.1 データベースの準備

用意したPostgreSQLのバージョンによって、drop table ~ if exists が動作しないことがあります。
用意したPostgreSQLのバージョンのドキュメントを参照して、 drop tableの書き方を調べてみて下さい。(PostgreSQL 13の場合はこんな感じで、書籍と違うので修正が必要です)

drop table posts ccascade if exists;
drop table comments if exists;

6.5.1 Sqlx

リスト6.17 で以下の部分のコードはstruct tagの書き方が間違っています。

	AuthorName string `db: author`

また、 type Post struct のコードが2回登場するので、間違っている1回目のものは無視すると良いと思います。

第10章

10.2 Herokuへのデプロイ

godep という依存ファイル管理ツールを使う手順が紹介されていますが、プロジェクトはアーカイブされており、READMEでは dep を使うように促していますが、depも同じくアーカイブされています。
Go modulesを使いましょう。

go mod init ws-h
go mod tidy

また、書籍に記載されている手順の中ではPostgreSQLについての説明が省かれているので、herokuにWebサーバーをデプロイ成功しても残念ながら動きません。
サンプルコードでは、elephantsql.com でPostgreSQLを用意して、そこに繋がるようになっています。このサーバーは2021年09月現在でも動作しているので、このサンプルコードの接続情報を使えば繋ぐことができます。

(繋いで見ればわかりますが、数人が同じように試しているので、いくつかのデータが既に登録済みになっています)

elephantsql.com はFREEプランも用意しているので、そこでサンプルと同じように自身でDBを新しく作るのが一番手っ取り早いと思います。

HerokuのPostgresサービスを使って動かす

また、Herokuも制限付きながら無料でPostgreSQLデータベースを提供しているので、そちらで動かしたい方向けに簡単に手順を記載しておきます。

Heroku上の情報の取得にコマンドを使用していますが、HerokuのWeb画面でも同じ情報を確認することができます。

こちらのDocs通り、ソースコードのDB接続設定を環境変数 DATABASE_URL から取得するように変更します。

Db, err = sql.Open("postgres", os.Getenv("DATABASE_URL"))

PostgreSQLサーバーを作成して、作成したDBの情報を取得します。 ws-h-xxxxxx のところは heroku create を実行したときに指定したapp nameにして下さい。

heroku addons:create heroku-postgresql:hobby-dev
heroku pg --app ws-h-xxxxxx

Add-on: postgresql-tapered-12345 のような値がdatabase nameです。

PostgreSQLにpsqlを使用して、CREATE TABLEが記載された setup.sqlファイルを実行します。(psqlコマンドを使うので、インストールされている必要があります)

heroku pg:psql postgresql-tapered-12345 --app ws-h-xxxxxx < ./setup.sql

Heroku AppのWeb URLを確認することで、curl で動作確認することが出来ます。

heroku info ws-h-xxxxxx

10.3 Google App Engineへのデプロイ

補足

情報が正確ではない可能性がありますが、調べてみると書籍が執筆された当時は、Google App Engineから外に通信をすることは課金対象だったためか、Socket APIを使う必要があったようです。
Cloud SQLはその例外だったため、手順としてCloud SQLでMySQLを起動して接続するという方法となっているようです。

現在はApp Engine Standardのランタイム説明によると外部へはフルアクセスとなっているようです。
また、Cloud SQLもMySQLだけでなく、PostgreSQLもサポートするようになっているので、PostgreSQLを指定して作成するようにすれば、ソースコード上のクエリのプレースホルダを? から $1 などに変更する必要もなくなっています。

Google Cloud SDKのインストール

https://cloud.google.com/sdk/docs/downloads-interactive

Google Cloudへのログインが必要になります。あらかじめアカウントを作成しておいて下さい。

gcloud init
gcloud components install app-engine-go

プロジェクトの準備

ProjectIDはユニークである必要があります。なので、ここからの手順で登場するxxxxxは全て任意の文字列で読み替えて下さい。また、指定しなくてもいいオプションがいくつかありますが、なるべく料金がかからない設定を明示的に指定しています。

gcloud projects create gowebprog-xxxxx

標準のprojectを変更しておきます。

gcloud config set core/project gowebprog-xxxxx

GAEへアップロードするソースコードのアップロード先bucketを明示的に作成します。
(us-east1 は free-tier 対象)

gsutil mb -b on -l us-east1 gs://gowebprog-xxxxx/

Cloud SQLの準備

Cloud SQLは elephantsql.com のFREEプランに比べると、それなりにちゃんとした料金がかかるため、気になる人はCloud SQLを準備せずにDBの接続情報を elephantsql.com に設定して下さい。

sql API を有効にします。

gcloud services enable sql-component.googleapis.com sqladmin.googleapis.com

free-tier は無いのでPostgreSQL最小の db-f1-micro を指定して作成します。

gcloud sql instances create gowebprog-db-xxxxx --database-version=POSTGRES_13 --tier=db-f1-micro --region=us-east1 --availability-type=zonal 

STATUSを確認します。

gcloud sql instances list

root:password のユーザーを作成します。

gcloud sql users create root --password=password --instance=gowebprog-db-xxxxx
gcloud sql databases create gowebprog --instance=gowebprog-db-xxxxx
gcloud sql connect gowebprog-db-xxxxx --user=root --database=gowebprog

PostgreSQLに接続します。

$ gcloud sql connect gowebprog-db-xxxxx --user=root --database=gowebprog
Allowlisting your IP for incoming connection for 5 minutes...done.
Connecting to database with SQL user [root].Password:
psql (13.4, server 13.3)
SSL connection (protocol: TLSv1.3, cipher: TLS_AES_256_GCM_SHA384, bits: 256, compression: off)
Type "help" for help.

gowebprog=>

gowebprog=>のプロンプトが出てきたら、テーブル作成するSQLを実行します。

create table posts (
id serial primary key,
content text,
author varchar(255)
);

テーブルが作成出来たことを確認します。

gowebprog=> \dt

GoのソースコードをGoogle App Engine用に修正

Goのソースコードの修正は、こちらのCloud SQLを使用するサンプルを参考にすると良さそうです。
https://github.com/GoogleCloudPlatform/golang-samples/tree/master/appengine/go11x

cloudsqlのサンプルコードを頼りにapp.yamlを作成します。

app.yaml
runtime: go116
env_variables:
  # Replace INSTANCE_CONNECTION_NAME with the value obtained when configuring your
  # Cloud SQL instance, available from the Google Cloud Console or from the Cloud SDK.
  # For Cloud SQL 2nd generation instances, this should be in the form of "project:region:instance".
  CLOUDSQL_CONNECTION_NAME: INSTANCE_CONNECTION_NAME
  # Replace username and password if you aren't using the root user.
  CLOUDSQL_USER: root
  CLOUDSQL_PASSWORD: password

INSTANCE_CONNECTION_NAMEの部分はDBへの接続名を取得して確認します。

gcloud sql instances describe gowebprog-db-xxxxx | grep connection

サンプルにはCloud SQL PostgreSQLに接続するための設定を環境変数から読み出しているコードもあるのでそのまま使わせてもらいましょう。

	var (
		connectionName = mustGetenv("CLOUDSQL_CONNECTION_NAME")
		user           = mustGetenv("CLOUDSQL_USER")
		dbName         = os.Getenv("CLOUDSQL_DATABASE_NAME") // NOTE: dbName may be empty
		password       = os.Getenv("CLOUDSQL_PASSWORD")      // NOTE: password may be empty
		socket         = os.Getenv("CLOUDSQL_SOCKET_PREFIX")
	)

	// /cloudsql is used on App Engine.
	if socket == "" {
		socket = "/cloudsql"
	}

	// connection string format: user=USER password=PASSWORD host=/cloudsql/PROJECT_ID:REGION_ID:INSTANCE_ID/[ dbname=DB_NAME]
	dbURI := fmt.Sprintf("user=%s password=%s host=/cloudsql/%s dbname=%s", user, password, connectionName, dbName)
	conn, err := sql.Open("postgres", dbURI)

App Engineを準備

App Engineを作成します。

gcloud app create --region=us-east1

コードをデプロイします。

$ gcloud app deploy --project=gowebprog-xxxxx --bucket=gs://gowebprog-xxxxx/

デプロイしたAppにアクセスするためのURLは、デプロイ時のlogに target url: [https://gowebprog-xxxxx.ue.r.appspot.com] のような形で出力されています。

片付け前のバックアップ

この手順は、PostgreSQLのデータを残しておきたい場合に必要となる手順なので、興味のある人だけやってみて下さい。

Cloud SQL PostgreSQLのデータExport手順

普通にexportコマンドを実行したいところですが、前準備としてCloud SQL instance から bucketへのpermissionを与える必要があります。

Cloud SQL instance の service account email 情報を取得します。

export serviceAccountEmail=$(gcloud sql instances describe gowebprog-db-xxxxx | grep service | cut -d " " -f 2)
echo $serviceAccountEmail

Bucket Object Creator roleを Cloud SQLに与えます。

gsutil iam ch serviceAccount:$serviceAccountEmail:roles/storage.objectCreator gs://gowebprog-xxxxx

他に与えれる権限はこちらを参照して下さい。
https://cloud.google.com/storage/docs/access-control/iam-roles

role割り当てを確認します。

gsutil iam get gs://gowebprog-xxxxx

exportを実行(バックアップファイル名は重複NGなので、日時をsufixにしています)

gcloud sql export sql gowebprog-db-xxxxx gs://gowebprog-xxxxx/sql-backup-$(date +%y%m%d%H%M).sql --database gowebprog

出力されたファイルを確認します。

gsutil ls gs://gowebprog-xxxxx

ファイルをダウンロード。

gsutil cp gs://gowebprog-xxxxx/sql-backup-2109211907.sql .

gsutil のコマンド一覧はこちらを参考にして下さい。
https://cloud.google.com/storage/docs/gsutil/commands/help

Cloud SQLを一時停止する

一旦Cloud SQLを停止しておいて、後で再度使う場合に課金を抑える手順です。

Cloud SQLを一時停止と再開手順

Cloud SQLを停止してもIPアドレスとstorage分は微量ですが課金されます。

gcloud sql instances patch gowebprog-db-xxxxx --activation-policy=NEVER
gcloud sql instances describe gowebprog-db-xxxxx | grep state

一時停止しているだけなので、再起動する場合は以下を実行します。

gcloud sql instances patch gowebprog-db-xxxxx --activation-policy=ALWAYS
gcloud sql instances restart gowebprog-db-xxxxx

後片付け(プロジェクトの削除)

Cloud SQLを削除します。

gcloud sql instances delete gowebprog-db-xxxxx

App Engine instanceは、一定時間アクセスがなければ自動的に削除される仕組みになっており、手動で削除することは想定されていないようです。

SERVICE, VERSION, ID の値を使って現在起動中のインスタンスを削除することは出来ますが、あえて消す必要はありません。

gcloud app instances list -s default --project gowebprog-xxxxx
gcloud app instances delete $INSTANCE_ID -s default -v $VERSION

bucketを削除します。

gsutil rm "gs://gowebprog-xxxxx/*"
gsutil rb gs://gowebprog-xxxxx

projectを削除します。

gcloud projects delete gowebprog-xxxxx

間違えて消した場合は、一定期間の間(多分30日)は完全に消えずに復活できます。

gcloud projects undelete gowebprog-xxxxx

10.4 Dockerへのデプロイ

2021年現在では、HerokuがDockerコンテナの実行に対応しています。
なので、Digital OceanよりはHerokuの方が簡単に試せると思います。

10.4.4 Go WebアプリケーションのDocker対応

コードの修正

書籍のDockerfileのままdocker buildを実行すると、次のようなエラーが出てしまうと思います。

#  > [5/5] RUN go install github.com/sausheong/ws-d:
#  ##10 0.231 go install: version is required when current directory is not in a module
#  ##10 0.231       Try 'go install github.com/sausheong/ws-d@latest' to install the latest version

これは、Dockerfile冒頭で FROM golangの部分でtagを指定していないためにFROM golang:latest、 つまり書籍執筆時とは異なる最新のGoのイメージを使うことになってしまっているためです。
go install の仕様がGo1.16から変更され、module modeではない場合はパッケージ versionを github.com/sausheong/ws-d@x.y.z のような形で指定する必要があるためです。

かといって、Go versionを書籍の付録に記載のversionと同じFROM golang:1.7.4にすると、今度は github.com/lib/pqがGo1.13以上を想定しているために動きません。

ということで、DockerfileのFROM は以下のようにすると動きます。

FROM golang:1.13

また、Herokuの場合、環境変数 PORT でWebサーバーの接続を受け付ける必要があるので、server.go$PORTを使うようにしておきます。

server.go
	port := os.Getenv("PORT")
	if port == "" {
		port = "8080"
	}
	server := http.Server{
		Addr: fmt.Sprintf(":%v",port),
	}

HerokuへのDockerデプロイ

herokuコマンドで次のようにするとデプロイできます。

heroku create ws-d-xxxxxxx
heroku container:login
heroku container:push --app ws-d-xxxxxxx web
heroku container:release --app ws-d-xxxxxxx web

PostgreSQLについては 10.2 Herokuへのデプロイ と同じなので省略します。

参考

Discussion