この記事を書いた目的
大学2回生の終わりからプログラミングの勉強を始めたいわゆる「一般的な学生エンジニア」がどのような道筋で新しい言語の勉強をしたのか公開することで同じような状況の人に気づきを与えることができれば良いなと思い書き始めました。
僕自身が勉強を始めた時は記事などを見るとほとんどの人がjavaやrubyなどバックエンド言語を以前に勉強している状況で勉強を始めたという人が多かったので・・・。(僕はgoを勉強するまでフロントエンド分野しか勉強したことがありません)
またこの記事は勉強記録的な意味合いもあるのでところどころ読みづらいかもしれません、そこはすいません!
なぜGOが使われるのか
少し冗長ではあるが学んだことがある人には理解しやすく、シンプルであるから。
実装効率とパフォーマンスのバランスがいい。
コンパイル言語でネイティブコードに変換されるため、各種OS向けにビルド可能
信頼できる情報源
Standard Library: https://pkg.go.dev/std
Go Release Note: https://go.dev/doc/devel/release
Effective Go: https://go.dev/doc/effective_go
The Go Blog: https://go.dev/blog/
Go Wiki: https://go.dev/wiki/
Go by example: https://gobyexample.com/
プログラミング言語GO完全入門: https://docs.google.com/presentation/d/1RVx8oeIMAWxbB7ZP2IcgZXnbZokjCmTUca-AbIpORGk/edit#slide=id.g4f417182ce_0_0
やったこと
①https://go.dev/tour/welcome/1
web上で実行できるのでまずは環境構築をせずにgoの感覚を掴みたい人はやってみるといいと思います。
②https://go.dev/doc/tutorial/
公式チュートリアルです。以下の写真の3つのチュートリアルを行いました。
3つ目のチュートリアルをすることで簡単な感覚がつかめたと思います。ここからは実務や実際の個人開発などで使い方を学んでいこうと思います。
③Chat gptを使ってapiを作成した。またnext.jsと連携してボタンを押したら情報を取得するようにした。
④公式チュートリアルの4つ目をやった。
postgresqlと接続してクエリを取得したり、データの追加などを行うことができる関数を作成した。
⑤公式チュートリアルの5つ目をやった。
goでginフレームワークを使って簡単なrestful APIを作成した。
⑥公式チュートリアルの6つ目をやった。
genericsを使うことで複数の型を使用できる汎用性を得た。また、動的型付け言語とは異なりgenericsを利用しても型チェックはしてくれるので保守性も向上する。しかし、コードが複雑になったり学習コストが高いことが懸念点。
⑦書籍:goプログラミングエッセンスを読んだ。
⑧個人開発でバックエンドでgoを使用したアプリを作成中。
⑨書籍:go言語webアプリケーション開発を読み、todoアプリのハンズオンを行った。
いきなりハンズオンに取り組むと難しかった。次の書籍を先にやってその後にするといい。
⑩書籍:APIを作りながら進むGo中級者への道を読み、8章までハンズオンを行った。
この書籍は個人的にはかなりわかりやすかった。⑦の書籍を読んだ後に先にこの書籍をやっておけばと少し後悔。フォルダの構成なども参考になり、インターン先で僕が開発中のコードの改善に大きく貢献してくれた一冊。
11. インターン先でのバックエンド開発(Go)
実務経験に勝る勉強はないとよく言われるが今回それを実感した。適度な責任感や焦燥感があることで入社前に勉強していたフロントエンド分野よりはるかに早く、正確に理解できてきていると思う。自分の書いたコードが新規プロダクトに使用されるというワクワク感も相まってモチベーションが明らかに高い状況で勉強できている。
・関数をパッケージにして使用する
・環境変数をmain.goに直張り → ファイルから値を取得する
・Makefileを整理してSAMのビルドからデプロイまで一貫して行う
・dynamoDBへの接続
・ゴルーチンを使用した処理の実現
・複数のパッケージに使用される構造体はtypesフォルダにまとめる
・関数の命名をgoのベストプラクティスに則って変更
これらを1ヶ月ほどで経験させていただき、とても貴重な機会となった。もちろんまだまだ問題点が山積みなので勉強を続けます。
GOにCIを導入(Github Actions)
※Githubに接続されていることを前提とします
①プロジェクトディレクトリ配下にディレクトリを作成
mkdir -p .github/workflows
②yamlファイルを作成
以下の内容をgo.ymlファイルに記述する
name: Go CI
on:
push:
branches:
- main
pull_request:
branches:
- main
jobs:
build:
runs-on: ubuntu-latest
steps:
- name: Check out the repository
uses: actions/checkout@v2
- name: Set up Go
uses: actions/setup-go@v3
with:
go-version: 1.x # 使っているGoのバージョンを指定
- name: Install dependencies
run: go mod tidy
- name: Run tests
run: go test ./...
③githubに変更を反映
行った変更をリモートリポジトリに反映させて完了
go mod initの意味
パッケージ管理やバージョン管理のために必要。
go mod init
を実行することでモジュールが初期化される。またこれにより依存関係を示すgo.modファイルが作成され、静的型付け言語であるgoはこれを参考に依存関係を把握する。これにはややこしさがあるものの異なる環境での再現性を向上させるという利点がある。
実務ではgraceful shutdownする
以下サンプルコード
ctx, stop := signal.NotifyContext(context.Background(), syscall.SIGTERM, os.Interrupt, os.Kill)
defer stop()
srv := &http.Server{
Addr: port,
Handler: mux,
}
go srv.ListenAndServe()
<-ctx.Done()
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
err = srv.Shutdown(ctx)
if err != nil {
return err
}
文法
リテラル・変数宣言
関数外で宣言した値を再度関数内で宣言するとそれは関数の外側には反映されない。
またgoでは型変換をする際は明示的に行う必要がある
var num1 int = 123
num2 := 456 #これは関数内部でのみ使用可能、また未使用だとエラーが発生する
ゼロ値
変数宣言を行ったが値を設定していない変数はゼロ値で初期化される。
数値なら0、文字列なら空文字列が設定される。
スライス
goには可変長配列が存在しない。だがスライスを配列のような構造として使用できる。goは安全性を重視しているため、範囲外アクセスがあるとプログラムが終了する。以下のように使用可能。
var nums [3]int = [3]int{1, 2, 3}
var nums1 []int
// 1, 2, 3の要素を持つスライスを作成して代入
nums2 := []int{1, 2, 3}
// あるいは既存の配列やスライスからも範囲アクセスでスライス作成
nums3 := nums[0:2] // 配列から
nums4 := nums2[1:3] // スライスから
// 配列と同じようにブラケットで要素取得可能
// 範囲外アクセスはパニック
fmt.Println(nums2[1]) // 2
// 要素の割り当ても可能
nums2[0] = 100
// 長さも取得可能
fmt.Println(len(nums2)) // 3
// スライスに要素を追加
// 再代入が必要
nums2 = append(nums2, 4)
// use-slice
fmt.Println(nums1, nums2, nums3, nums4)
マップ
型は map[キーの型]値の型 とする必要がある
hs := map[int]string{
200: "OK",
404: "Not Found",
}
// makeで作る
authors := make(map[string][]string)
// ブラケットで要素アクセス
// 代入
authors["Go"] = []string{"Robert Griesemer", "Rob Pike", "Ken Thompson"}
// データ取得
status := hs[200]
fmt.Println(status)
// "OK"
// 存在しない要素にアクセスするとゼロ値
fmt.Println(hs[0]) // panic
// あるかどうかの情報も一緒に取得
status, ok := hs[304]
// status = ""
// ok = false
// use-map
fmt.Println(hs, ok)
switch文
if, if elseのような動作をするが、1つのcaseに当てはまったらその処理を抜けるという特徴を持つ
switch s {
case "running":
fmt.Println("実行中")
case "stop":
fmt.Println("停止中")
default:
fmt.Println("その他")
}
deferによる後処理
関数の処理を抜けるタイミングで後処理をするメカニズムである。ファイルを閉じるなどの処理を行う。
defer.res.Body.Close()
終了処理
os.exit()は0以外でプログラムが強制終了する
構造体の定義
以下のように階層構造のAPIも定義できる
type NippoApiResponse struct {
Results struct {
MovementDetail struct {
LoadedDistance int `json:"loadedDistance"`
WorkStartTime string `json:"workStartTime"`
WorkEndTime string `json:"workEndTime"`
RoadRunningDuration int `json:"roadRunningDuration"`
WorkDuration int `json:"workDuration"`
} `json:"movementDetail"`
MovementEnd struct {
EndTime string `json:"endtime"`
} `json:"movementEnd"`
MovementStart struct {
StartTime string `json:"startTime"`
} `json:"movementStart"`
} `json:"results"`
}
変数の宣言
このようにスライスを定義することもできる
var albums = []album{
{ID: "1", Title: "Blue Train", Artist: "John Coltrane", Price: 56.99},
{ID: "2", Title: "Jeru", Artist: "Gerry Mulligan", Price: 17.99},
{ID: "3", Title: "Sarah Vaughan and Clifford Brown", Artist: "Sarah Vaughan", Price: 39.99},
}
エラー文
nilは「何もない」ということを示す。javascriptでいうnullのようなもの。
if err != nil {
log.Fatal("Failed to connect to the database: ", err)
}
このようにerrがnilではない場合〇〇を表示するといったエラー文がよくある。
postgreSQLとの接続
connStr := "user=aaa password=aaa dbname=mydb sslmode=disable"
var err error
db, err = sql.Open("postgres", connStr)
if err != nil {
log.Fatal("Failed to open the database: ", err)
}
defer db.Close()
// データベースへの接続を確認
err = db.Ping()
if err != nil {
log.Fatal("Failed to connect to the database: ", err)
}
fmt.Println("Connected to PostgreSQL!")
golangのfmt.Printf関数
%s:文字列(string)
%d:符号付き整数(int, int8など)
%t:論理値(bool)
%T:型
%v:デフォルトのフォーマット
%d:符号なし整数(uint, uint8など)
%g:浮動小数点数(float64など)
%g:複素数(complex128など)
%p:チャネル(chan)
%p:ポインタ(pointer)
%%:%を出力
%c:文字
%b:2進数
Goで作成するAPI(HelloWorld)
フレームワークを使用せずにgoのAPIを作成すると以下のようになる。
package main
import (
"io"
"log"
"net/http"
)
func main() {
helloHandler := func(w http.ResponseWriter, r *http.Request) {
io.WriteString(w, "Hello, World!\n")
}
http.HandleFunc("/", helloHandler)
log.Println("Listening on port 8080")
log.Fatal(http.ListenAndServe(":8080", nil))
}
ginを使ったAPI作成
GETメソッド
localhost:8080/albumsにアクセスされた時にgetAlbums関数が呼び出され、albums(宣言してある変数)が表示される。
func main() {
router := gin.Default()
router.GET("/albums", getAlbums)
router.Run("localhost:8080")
}
// getAlbums responds with the list of all albums as JSON.
func getAlbums(c *gin.Context) {
c.IndentedJSON(http.StatusOK, albums)
}
POSTメソッド
受け取ったデータをnewAlbumに格納。albumsにappendする。
c.IndentedJSON(http.StatusCreated, newAlbum)ではcurlコマンドでPOSTした時にステータスコードと追加したデータであるnewalbumを表示するようにしている。
func postAlbums(c *gin.Context) {
//newAlbumにクライアントから受け取ったJSON(データ)を格納
var newAlbum album
// Call BindJSON to bind the received JSON to
// newAlbum.
if err := c.BindJSON(&newAlbum); err != nil {
return
}
// Add the new album to the slice.
albums = append(albums, newAlbum)
c.IndentedJSON(http.StatusCreated, newAlbum)
}
GETメソッド(ID指定)
cにはリクエストパラメータの情報が入っているので、IDを取得してidに格納する。
for文を回し、albumの一つの塊であるaのIDがidと同じだった場合ステータスとaを表示する。
func getAlbumByID(c *gin.Context) {
id := c.Param("id")
// Loop over the list of albums, looking for
// an album whose ID value matches the parameter.
for _, a := range albums {
if a.ID == id {
c.IndentedJSON(http.StatusOK, a)
return
}
}
c.IndentedJSON(http.StatusNotFound, gin.H{"message": "album not found"})
}
Generics
本来は以下のように型が違うことによって同じような処理でも2つの関数を作成する必要があるが、genericsを使用すると1つで済む。
Genericsを使用していないコード
func SumInts(m map[string]int64) int64 {
var s int64
for _, v := range m {
s += v
}
return s
}
// SumFloats adds together the values of m.
func SumFloats(m map[string]float64) float64 {
var s float64
for _, v := range m {
s += v
}
return s
}
Genericsを使用したコード
func SumIntsOrFloats[K comparable, V int64 | float64](m map[K]V) V {
var s V
for _, v := range m {
s += v
}
return s
}
fuzzing
ルール
関数名の最初がFuzzである必要がある。またファイル名も
〇〇_test.goである必要がある。以下のコマンドで実行できる。
go test