🐙

【go】【docker】初期キャッチアップ<作業時全体のメモ>

2024/10/26に公開

この記事の内容

・go言語について、初期キャッチアップを行なっていきます。
・SMS送信APIのような、実プロダクトに近いアプリケーションを試作する前段階となっています。
・dockerで数回、コンテナを立ち上げた経験のある方(dockerについて、初期よりのキャッチアップ中の方)

対象

go言語を最初から学習したい方。

環境

macbookpro2019(local)

はじめに

AWSでECR/ECSのキャッチアップを終え、本格的なアプリケーションは何か、と考えていた時、某転職サイトにて、「SMS送信API」を利用した企業様がございまして、「SMS送信API」について簡易的に調査していました。求人票などから、使用言語に「go」の記述があったたため、chatGPTに聞いてみました。

goは、サーバーサイドよりのミドルウェアをカスタマイズしたり、apiをバックエンドのように
作成したり、サーバーサイド〜バックエンド系の言語のイメージですが、合っていますか?

はい、その認識で合っています!Goは、その特徴からサーバーサイドやバックエンドの開発に非常に適しています。以下がGoがサーバーサイド・バックエンド向けとされる主な理由です。

Goがサーバーサイド・バックエンドに適している理由

  1. シンプルで直感的な言語設計:

    • Goはシンプルな構文と直感的なコーディングスタイルを持ち、コードが読みやすくメンテナンスしやすい設計になっています。複雑な依存関係を避け、開発者がコードベースを迅速に理解・修正できるため、特にサーバーサイドのアプリケーションやAPIでの生産性が高まります。
  2. 高いパフォーマンス:

    • Goはコンパイル型で、メモリ管理にガベージコレクション(GC)を採用しており、非常に効率よく動作します。HTTPリクエストの処理やデータベースアクセスなど、バックエンドで必要とされるパフォーマンスを提供します。
  3. 並行処理(goroutine):

    • Goの特徴の一つであるgoroutineは、軽量で効率的な並行処理を可能にし、マルチスレッド処理が必要なサーバーサイドのアプリケーション(特に高トラフィックなAPI)で非常に効果を発揮します。複雑なスレッド管理を気にせずに、多数のリクエストやジョブを効率的に処理できます。
  4. 標準ライブラリが充実:

    • Goの標準ライブラリには、HTTPサーバーやJSONエンコード・デコード、データベースアクセスなど、サーバーサイドで頻繁に使う機能が豊富に揃っています。そのため、サードパーティライブラリに頼らず、シンプルにサーバーやAPIを構築可能です。
  5. デプロイの容易さ:

    • Goでビルドされたバイナリは、外部依存を持たない単一の実行ファイルとして生成されるため、どの環境でも容易にデプロイが可能です。この性質により、コンテナ化(Dockerなど)やクラウド上でのデプロイにも適しています。

Goでできる主なサーバーサイド・バックエンドの活用例

  • RESTful APIやGraphQL APIの構築
  • マイクロサービスアーキテクチャのバックエンド開発
  • Middlewareのカスタマイズと組み込み(GinやEchoなどのフレームワークのミドルウェア)
  • バッチ処理やキュー処理(例:メッセージキューやイベント処理)
  • WebSocketサーバーリアルタイム通信のサポート

ここで、さらに「SMS送信API」で利用される「go」のサンプルコードについて、chatGPTに伺ってみました。

go で、sms送信APIを作成したいです。サンプル教えて

GoでSMS送信APIを作成するには、例えばTwilioやAWS SNS(Simple Notification Service)などのSMSサービスを利用する方法があります。以下にTwilioを使ったSMS送信APIのサンプルコードを示します。Twilioを使うには、Twilioのアカウントと、API用のSID、Auth Token、そして送信元の電話番号が必要です。

前提準備

  1. Twilioに登録し、APIのクレデンシャル(Account SIDAuth Token)を取得。

  2. GoでTwilioのライブラリをインストール:

    go get github.com/twilio/twilio-go
    

サンプルコード

以下は、Twilioを使用してSMSを送信するGoのサンプルAPIです。

package main

import (
    "log"
    "net/http"
    "os"

    "github.com/gin-gonic/gin"
    "github.com/twilio/twilio-go"
    openapi "github.com/twilio/twilio-go/rest/api/v2010"
)

func main() {
    router := gin.Default()

    // SMS送信用のエンドポイント
    router.POST("/send-sms", func(c *gin.Context) {
        var request struct {
            To      string `json:"to" binding:"required"`   // 送信先の電話番号
            Message string `json:"message" binding:"required"` // 送信するメッセージ
        }

        if err := c.ShouldBindJSON(&request); err != nil {
            c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid request"})
            return
        }

        // Twilioクライアントの初期化
        client := twilio.NewRestClient()

        params := &openapi.CreateMessageParams{}
        params.SetTo(request.To)                          // 送信先の電話番号
        params.SetFrom(os.Getenv("TWILIO_PHONE_NUMBER")) // Twilioの発信元電話番号
        params.SetBody(request.Message)                  // メッセージ内容

        // SMS送信
        _, err := client.ApiV2010.CreateMessage(params)
        if err != nil {
            log.Println("Error sending SMS:", err)
            c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to send SMS"})
            return
        }

        c.JSON(http.StatusOK, gin.H{"status": "Message sent successfully"})
    })

    // サーバー起動
    router.Run(":8080")
}

説明

  1. Twilioのクライアント設定: twilio.NewRestClient()でTwilio APIにアクセスするクライアントを作成。
  2. 環境変数の設定: TWILIO_PHONE_NUMBERに発信元の電話番号を設定(.envファイルに保存するか、環境変数として設定)。
  3. リクエスト処理:
    • リクエストをバインドして、送信先番号とメッセージ内容を取得。
    • client.ApiV2010.CreateMessage(params)でメッセージを送信。
  4. エラーハンドリング: SMSの送信に失敗した場合、エラーメッセージを返す。

起動

環境変数を設定して、アプリケーションを実行します。

export TWILIO_PHONE_NUMBER="+1234567890"   # Twilioの電話番号
export TWILIO_ACCOUNT_SID="your_account_sid"  # TwilioのSID
export TWILIO_AUTH_TOKEN="your_auth_token"    # TwilioのAuth Token

go run main.go

サーバーが起動したら、以下のようなリクエストを送信してテストできます。

curl -X POST http://localhost:8080/send-sms \
  -H "Content-Type: application/json" \
  -d '{"to":"+819012345678","message":"Hello from Go!"}'

注意点

  • Twilioの電話番号は有料プランで取得が必要です(テスト用に無料クレジットもあります)。
  • テスト時にはTwilioの検証済み番号に送信する必要があります。

ということで、goについて学習し、上記のSMS送信APIのサンプルコードが理解できるまで、キャッチアップを行いたいと思います。

方法

上記のサンプルコードでは、go言語のAPIを作成しているため、バックエンドよりのgo言語の利用法となっているようです。
公式チュートリアルや、chatGPTへの質問を通して、

・go言語 公式チュートリアル
https://go.dev/doc/tutorial

上記のサイト1つに集中します。
上記のページの中に、

Tutorial         Description
Getting started	  Say Hello, World with Go.

とあるので、「Getting started 」のページを開いてみます。
( https://go.dev/doc/tutorial/getting-started )
すると、

・Install Go (if you haven't already).
・Write some simple "Hello, world" code.
・Use the go command to run your code.
・Use the Go package discovery tool to find packages you can use in your own code.
・Call functions of an external module.

と表示され、上から順に、

goをinstallする。
Hello, worldを(goで)表示させる。
(goを)走らせるコマンドを使う。
(goの)利用用途に応じたパッケージの探し方をチェックする。
外部のモジュール関数を呼ぶ。(goのライブラリに当たるものをインストールして利用する。)

の手順で、インストールしてから基本的な関数を呼ぶところまで、何もみずに再現できるようになるまで自身がチェックする用の手順書を作成します。

キャッチアップ(手順書作成)

公式を参考に、自身で手順書を作成します。精度が高い手順を作成でき、その手順を記憶していれば、何も見ずに同じ手順が完全再現できるはずです。忘れた際には、もう一度同じ手順書を作成し、公式とのdiffを確認します。うまくいけば、3回ほどで、一致がほぼ取れるようになるかと思われます。
ターミナルやVScodeなどを開き、cd でホームディレクトリへ移動し、mkdir golangなどでフォルダを作成し、cd golang、その中へ移動します。permissionなどでなぜか作成できない場合は、別のフォルダで行うか、sudo suでroot権限で行なってしまいます。(「Time is money.」のため。)

go mod init example/hello

上記コマンドが1つ目の手順になります。
コマンドが理解できていないと、筆者が必ず忘れるタイプなため、go helpを行いますが、当然動きません。なぜなのでしょうか。

正解は、「go」をインストールしていないからです。

chatGPT4様に伺い、下記の手順で「go」のインストールを行います。

brew install go

brewを使ってgoをlocal(macbookpro2019)へinstallします。その後、versionチェックで、コマンドが効くか、確認します。

go version

念のため、以前自分がbrewをインストールしていないか、下記のコマンドで確認しましょう。

brew --version

Homebrew 4.4.0
と表示されたため、「やった!!インストールできる!」でインストールしてしまいそうなところを、敢えて周辺情報を確認してみます。

brew --help

で、コマンドを確認します。
ちょっと数が多かったので、grepで絞ります。

brew --help | grep -A 3 update 

筆者的には、"update"に関わる部分を見たかったので、grepで検索条件に指定しています。
| のパイプで、brew --helpの結果が標準出力へ出力されるため、右のコマンドの入力へ渡しています。-Aのオプションで、下をついでに引っ張ってきます。「3」が指定されているため、3行分です。

  brew update
  brew upgrade [FORMULA|CASK...]
  brew uninstall FORMULA|CASK...
  brew list [FORMULA|CASK...]

と表示されました。

brew update

した際の結果の詳細を確認します。
上のコマンドの
brew upgrade [FORMULA|CASK...]
の[FORMULA|CASK...]が意味がわからなかったのですが、コマンドの結果に表示されていた

==> New Casks
 adlock font-doto langgraph-studio  sketchup cocoapacketanalyzer

から、New Casksのようなリストの下に表示される名詞を指定することで、その"CASK"についてupgradeができるのでは?と思いました。そこで、何かを指定したい、と思いつつ、作業には関係がなさそうなのでここは横に置いておきます。

brew --version

Homebrew 4.4.2
と表示されました。
4.4.0 => 4.4.2 へ、わずかにversion upが行われています。
ここで、

brew list
brew --help

を確認し、brew listで表示される結果がinstallされているもののリストか確認します。
以前、yum list installed のように、install済みのlistをチェックするコマンドがあったため、brew listだけではインストール済+インストール候補の両方が表示される可能性があるのでは?と思ったためです。結果、install済だけのようです。筆者のlocalでは、

==> Casks

の下に何も表示されませんでした。先ほどの

==> New Casks
 adlock font-doto langgraph-studio  sketchup cocoapacketanalyzer

では、候補が表示されていましたね。この結果から、brew listはinstall済みのもの、brew update時にはNew Casksが紹介される、といったコマンドのようです。断定はできませんが・・

ということで、goをinstallする前段階のbrew updateが行えました。次に、

brew install go

でgoをinstallします。

brew list

の中に、goがあったかもしれませんね。

brew list | grep go
argon2
go

2個表示されましたが、goはあるみたいですね。installも成功しました。

go version  
go version go1.23.2 darwin/amd64

goは、--versionではなく、versionとして、コマンド定義されているようです。--でオプション化している訳ではないのですね。

go lang

下記のコマンドへ戻ってきました。筆者はgitを利用したことがあり、
git init
のようなコマンドで、ブラウザのgit hubで作成したリポジトリ(リモートリポジトリ)を作成した際の手順に、ローカルリポジトリと連携を行う手順が出力されます。その手順の中に、最初に必ずgit initがあり、そのコマンドの意味は、local repositoryをcurrent directory(今いるディレクトリ)直下に作成する、「.git」ディレクトリがインストールされるイメージかと思っています。そのため、下記のgo mod init example/helloは、go専用のrepositoryをカレントディレクトリ直下にインストールするのではないか?と予想ができます。しかし、実際わからないので、やってみて、周囲の情報(chatGPT様・・・)と一致を確認し、仮説が正しいか、検証します。

go mod init example/hello

を、いきなり実行しようとしましたよね。
筆者的に、helpでどういうものかチェックした後に叩きたいです。
なので、

go help

を実行してみました。失敗した場合、「go --help」でオプション化もチェックする予定です。万全?の体制で意気揚々とコマンド実行。すると、

Go is a tool for managing Go source code.

Usage:

        go <command> [arguments]

The commands are:

        bug         start a bug report
        build       compile packages and dependencies
        clean       remove object files and cached files
        doc         show documentation for package or symbol
        env         print Go environment information
        fix         update packages to use new APIs
        fmt         gofmt (reformat) package sources
        generate    generate Go files by processing source
        get         add dependencies to current module and install them
        install     compile and install packages and dependencies
        list        list packages or modules
        mod         module maintenance
        work        workspace maintenance
        run         compile and run Go program
        telemetry   manage telemetry data and settings
        test        test packages
        tool        run specified go tool
        version     print Go version
        vet         report likely mistakes in packages

Use "go help <command>" for more information about a command.

Additional help topics:

        buildconstraint build constraints
        buildmode       build modes
        c               calling between Go and C
        cache           build and test caching
        environment     environment variables
        filetype        file types
        go.mod          the go.mod file
        gopath          GOPATH environment variable
        goproxy         module proxy protocol
        importpath      import path syntax
        modules         modules, module versions, and more
        module-auth     module authentication using go.sum
        packages        package lists and patterns
        private         configuration for downloading non-public code
        testflag        testing flags
        testfunc        testing functions
        vcs             controlling version control with GOVCS

Use "go help <topic>" for more information about that topic.

上記のような結果が出力されました。
ここで、何を探していたのでしょうか?

go mod init example/hello

のコマンドの意味を調べようとしていましたね。先ほどのコマンドの中に「mod」が含まれていました。なので、「mod」でgrepしてみましょう。

go help | grep mod

mod module maintenance
と表示されていました。
量が多すぎると、人からすると、確認しにくいですね。grepは便利です。
話が逸れていましたが、「mod」は「module」を指しているようです。「module」というと、「他の言語でも、初期設定には入っていないけど、installすると便利機能が追加されますよ」的なイメージを持っており、おそらく合っています。
react なんかで「npm install react-router-dom」をよくreact上でrouting(ルーティング)を行う際に、moduleとして利用しています。パスを通すイメージです。https://react.co.jpというパスが/(root)である場合に、https://react.co.jp/page1 で作成したpage1.tsxを表示させたい場合に、上記moduleを利用します。

ということは、

go mod init example/hello

は、go言語で、「git の repositoryの仕様を真似して、moduleとしてgo専用のリポジトリを作成する」という最新のトレンドを押さえたおしゃれな言語だと思います。

自分が言うのもおこがましいですが・・笑

と言うことで、

go mod init example/hello

を思い切って叩きます。

go mod init example/hello
go: creating new go.mod: module example/hello

予想通り、新しいmodule,example/helloができましたね。
go.modが気になります。
先ほどのhelpの中にあったので、grepします。

go help | grep go.mod
        go.mod          the go.mod file

なんてことはない、普通の答えが返ってきましたねw

ファイルだそうです。(え?)

・・・

意味は、多分ファイルです。(え?)

先ほどのコマンドの続きで「ls」してみましょう。

ls
go.mod

go.modと言うファイルができていました。
go.modをcatでのぞいてみましょう。

cat go.mod 
module example/hello

go 1.23.2

moduleのリポジトリのようなものが確認できて、versionのような数値も表記されています。
おそらくcatではripo name, go versionの2つが確認できます。詳細は不明です。
ディレクトリなのかな?と思い、cdしてみます。

cd go.mod
cd: not a directory: go.mod

.modが拡張子だったようです。と言うことは、.gitや.envのように、lsで見えないファイルでなくて、普通の.modと言う拡張子を持つファイルのようです。

hello.go

あまり立ち止まらず、手順作成に専念します。
下記の流れで「hello.go」を作成し、起動確認します。

vi hello.go
package main

import "fmt"

func main() {
    fmt.Println("Hello, World!")
}
:wq
go run hello
Hello, World!

成功しました。

quote.go

次に、「quote.go」を作成し、起動確認します。

vi quote.go
////////////////
package main

import "fmt"

import "rsc.io/quote"

func main() {
    fmt.Println(quote.Go())
}
////////////////
:wq
go mod tidy
go: finding module for package rsc.io/quote
go: could not create module cache: mkdir /Users/fujitajunya/go: operation not permitted

「operation not permitted」で権限拒否されました。公共Wifi(VPN)だからでしょうか?
「go: could not create module cache」を直接GPT4に投げました。すると、
macbookpro2019で、「Linux/macOS: $HOME/go/pkg/mod」のパスにできるcache?にファイルを操作する権限がないようです。cacheについて、下記をchatGPTに投げました。

googlechromeのキャッシュは、.cacheのようなフォルダ内にありますか

すると、

~/Library/Caches/Google/Chrome/Default

に、キャッシュされたデータが保存されているそうです。中を覗くと、

ab65de3c96gh809d_0

のような [英数字の16字の乱数]_0 がファイルとして沢山ありました。

less ab65de3c96gh809d_0

で、

"ab17bac9ba8f1a84_0" may be a binary file.  See it anyway?

と表示されたため、binary file(バイナリファイル)として、低級よりの言語で処理を行いやすく保存されている?かもしれません。

Google Chromeは、「ホームディレクトリ/path/to/.cache」な管理方法で、cacheが保存されているようです。

GoogleChromeと似たように、go言語でも同じ管理方法をとっているのでは?と言う思考になりました。

一旦ここで手詰まりしました。

macOSのホームディレクトリ上では、ファイル作成ができないため、brew installした場合、ホームディレクトリに自動で.cacheに当たるファイルが生成されない(operation not permmited)だからです。

ここで、ローカルでは起動できないとわかりました。
では、次にEC2インスタンスでできないか、を考えました。今、ちょうどスマホの充電器を忘れ、電源が切れて入るため、MFA認証ができない状態です。(何をやってるんだ・・)

では、Docker hub上にあるLinuxリポジトリを借用し、仮想環境(コンテナ)を立ち上げ、その中へrepoをpushし、goをinstallすれば、起動確認を行えるかもしれません。

キャッシュに関わる部分であれば、その方法で対応可能かもしれない。

と言うことで検証します。

https://hub.docker.com/_/amazonlinux

AWSで使う、「Amazon Linux」を使ってみます。
なぜだかテンションが上がりますね笑

docker Desktopを立ち上げ、docker を起動状態にします。
「systemctl start docker.service」コマンドと同じ役割です。

この状態で、docker用のディレクトリへ移動し、amazonlinuxに当たるdocker imageをpullします。

mkdir docker
cd docker
mkdir amazonlinux_with_golang
cd amazonlinux_with_golang
docker pull amazonlinux

pullされたimageの情報を確認し、そのimage IDを使ってdocker runし、そのプロセスを確認し、起動されているか、確定させます。

docker images
docker run -d [IMAGE ID] sleep 14400
(-dはバックグランドでコンテナを起動、sleep は起動時間の指定、4時間、止めるときはdocker stop [CONTAINER ID])
docker run -d 44dceb23ffa1 sleep 14400
docker ps -a

起動が確認できたら、containerの中に入ります。

goをコンテナ内にinstall

「docker ps -a」で「docker run」されていることを確認した後、下記の流れに沿ってgoをコンテナ内にinstallします。

docker exec -it [CONTEINER ID] bash
docker exec -it ebef1cb47b33 bash

上記コマンドの後、lsやpwdで、何もない、かつ/にいればコンテナにいます。

ls   (ディレクトリ内に何があるか、lsします。何もないはずです。)
pwd  (位置の確認、/が出るはずです。)
curl ipinfo.io/ip  (コンテナがあるIPを確認します。左記のエンドポイントを叩くと、IPを返してくれます。自身のPCが接続しているルーターのグローバルIPが返ってきていれば、ローカルでコンテナが立ち上がっていることが確認できます。別のターミナルを立ち上げ、自身のホームディレクトリなどで同じコマンドを叩き、ipが一致して入れば、コンテナIPとローカルIPが同じなので、ローカルでコンテナを立ち上げることに成功しています。)
yum update  (基本的な基本的なコマンド類や、モジュール類?をインストールします。)
ls        (binやlocalなど、ディレクトリが確認できればOKです。)

上記でcontainer内に入れることを確認したら、基本的なコマンドを扱えるように、sudo,vi,bash,golangをinstallし、go versionで起動確認します。

yum update
yum install sudo vi bash golang
cd
ls
pwd
go version

で、goのversionが表示されればOKです。

cache問題は解決されたのでしょうか。検証phase

quate.goを作成時に手詰まりした現象が、すんなり通ればOKです。
では、やってみます。

vi quote.go
////////////////
package main

import "fmt"

import "rsc.io/quote"

func main() {
    fmt.Println(quote.Go())
}
////////////////
:wq
go mod tidy
go: finding module for package rsc.io/quote
go: could not create module cache: mkdir /Users/fujitajunya/go: operation not permitted

go module用のリポジトリ(go mod)を作成し忘れていました。
下記のコマンドで、example/ticdyを作成します。

go mod init example/tidy
go mod tidy

で、うまくmoduleがinstallされたみたいです。

go run quote.go 
Don't communicate by sharing memory, share memory by communicating.

失敗したのかと思いきや、chatGPT4様へ質問しました。

出力の"Don't communicate by sharing memory, share memory by communicating."は、
rsc.io/quoteパッケージ内のquote.Go()関数によって生成される有名なGoのフレーズです。
このメッセージが表示されたということは、rsc.io/quoteパッケージが正しくインストールされ、
使用できていることを示しています!

成功していました笑
ラッキーです笑(?)

vi quote.go
    fmt.Println(quote.Hello())   
    fmt.Println(quote.Go())      
    fmt.Println(quote.Glass())   
    fmt.Println(quote.Opt())     

他のメソッドのようなコマンドを4つ試すと、別々のメッセージが返ってくるようです。
中身は、自分で確認してみてください!!

:wq
go run quote.go

で、無事4つ確認できました!

長文すぎてまとまっていないですが、作業全体を記述しました。
goを起動しようとmacのlocalで公式チュートリアルの2つ目のquate.goが起動しない。
→ cache的な問題? → dockerを立ち上げコンテナ内で行うことでlocalでなく仮想環境でgoを走らせることを決定 → 実作業 → quate.go が走る。
と言う流れで、起動確認まで行えました。

この後、goの実コードを触り、SMS送信APIに関わる部分の操作と、AWS側で「SNS」と呼ばれるサービスを起動させ、送信が本当にできるか、検証など進めたいと考えています。

長文読んでいただき、ありがとうございました!

Discussion