# Golang カリキュラム ###### tags: `curriculum` `seeds` [TOC] ## 目標 - Goの動きを一通り理解できるようになる - Goで簡単なアプリを作ってみる - dockerの練習もしときたい - メンターさんにお願い、dockerの練習をしたいのでリポジトリ作成後、勉強会毎にissueを立てて(例:go勉強会1変数について)そこにpushしてもらうようにして、勉強の成果を自分のリポジトリに保存するようにしてほしいです!! - 宿題等もissueを立ててそこので作業してできてもできなくてもpushしてレビューでヒントを与える感じでやってほしい。 ## 成果物 ~~- openAI(chatGPT)を用いたアプリの制作 (私で作れるかどうか調べる、サンプルコードはどこにのせる?)~~ - openAIは利用にお金がかかるのでなしにします。 - 簡単なCRUDアプリの作成[完成例](https://github.com/hikahana/go_learn_answer) ## 構成 - 基本は[A tour of go](https://go-tour-jp.appspot.com/)のやつをまとめ直して進める予定。 - 最初にgoの機能に触れて、最後にCRUDアプリを作る。 ### 第一章 はじめに #### 目標 - Golangの歴史をさらっと知る - 環境構築を一緒にやる #### 内容 - 1-1 Goの歴史をさらっと知る [この記事を一緒に読もう](https://qiita.com/nemui_/items/11ba7e71fa0081b3d0d3) - 1-2 githubリポジトリの作成 [参考](https://docs.github.com/ja/get-started/quickstart/create-a-repo) - githubの右上の+ボタンをクリックして、new repositoryをクリック。 - リポジトリの名前(repository name)と説明(description)を記入する。今回は名前を**go_learn**とする。 - 一番下のcreateボタンを押してリポジトリを作成する。 - 画面にでてきたコマンドをターミナルで実行するとセットアップが完了する。 - Settingsを押し、Collaboratorsから名前検索でメンターを追加する。終わり! - 1-3 vscodeのインストール [参考](https://www.javadrive.jp/vscode/install/index1.html) - [公式サイト](https://code.visualstudio.com/#alt-downloads)に飛んで自分のデバイスに合わせてダウンロードする。 - ダウンロードされたらインストーラを起動してインストールを開始する。特に設定することはないので次へ押しとけばok - vscodeを起動してサイドバーにある拡張機能(Extensions)を押して**Japanese Language Pack**、**Go**、**WSL**をそれぞれインストールして終わり! - 1-4 Goのインストール [参考](https://took.jp/golang-hello-world/) - [公式サイト](https://go.dev/)からDownloadを押して自分のデバイスにあったインストーラを選択する。 - ダウンロードされたらインストーラを起動してインストールして終わり! - 1-5 Dockerで環境構築 - ディレクトリ構成(第3章も兼ねてちょっと複雑になります) go_learn . ├── docker-compose.yml └── .docker &emsp;&emsp;&emsp;&emsp; └── app.Dockerfile - メンターさんが環境構築issueを立てる(やらんなくてもいいかも) - コマンドで環境構築issueに移動する。 `git switch -c <issue名>` - go_learnディレクトリを作成し、移動する。(以下の作業はvscodeで行っても良き) ``` mkdir go_learn cd go_learn ``` - **docker-compose.yml**と.dockerディレクトリの中に**app.Dockerfile**を作成する。 ``` touch docker-compose.yml mkdir .docker cd .docker touch app.Dockerfile ``` - app.Dockerfile、docker-compose.ymlの中身を記入する。 ``` #Dockerfile # Go-version FROM golang:1.19.1-alpine # アップデートとgitのインストール!! RUN apk update && apk add git # appディレクトリの作成 RUN mkdir /go/src/app # ワーキングディレクトリの設定 WORKDIR /go/src/app # ホストのファイルをコンテナの作業ディレクトリに移行 ADD . /go/src/app ``` ``` #docker-compose.yml version: "3" # composeファイルのバージョン services: app: # サービス名 container_name: go_learn build: #Dockerfileの場所 context: .docker Dockerfile: app.Dockerfile tty: true # コンテナの永続化 volumes: - ./app:/go/src/app # マウントディレクトリ ``` - buildしてみる ``` #イメージのビルド docker-compose build --no-cache #成功するとappフォルダが作成されるのでその中でhello.goの作成 #vscodeで作成してもよき touch app/hello.go # エラーがでたら以下を実行 sudo chmod -R 777 ./ ``` - hello.goにサンプルコードを書く ``` package main import "fmt" func main() { fmt.Println("Hello, World from docker!") } ``` - コンテナを立ち上げて実行してみる。 ``` # コンテナの立ち上げ docker compose up -d #作成したファイルの実行 go run app/hello.go #コンテナ閉じるとき docker compose down ``` - appディレクトリの中にlearn(サンプルコード置き場)とhomework(宿題提出用)フォルダを作成して終わりです!!お疲れさまでした! - 1-3 第一章まとめ - GoはGoogleによって作られたプログラミング言語であり、C言語を参考にして開発された。 - Googleでの不満解消のために作られたGoの特徴は処理が速いことと可読性が高い(コードが読みやすい) - dockerという仮想環境でgoの環境構築ができた - 実際にGoの環境構築したので一人でコードを書いて動かしてみてもいいかも #### 宿題 ~~- 環境構築しかしてないのに宿題なんてあるのだろうか...~~ - なしです。構築お疲れ様!! ### 第二章 Goを巡ろう #### 目標 - Goのできることを知る。 - 自分で考えて書けるようになるためいくつかの練習問題をやってもらう。 - サンプルコードを自分デバイスで実行してもらう&自由に変えて動きを見てもらう。 - 勉強会の終わりにgithubリポジトリに送る作業をしてもらえるといい練習になるかも #### 内容 - 2-1 Packages - goのプログラムはPakageで構成されている。 - パッケージごとにコードをまとめると良さ [参考Lottery](https://github.com/NUTFes/lottery/tree/develop/api) - クローゼット(パッケージ)の中に洋服(コード)を入れているイメージ - mainパッケージを作らないとエラーになるの注意!! ``` #サンプルコード pakage.go package main #これを定義する大事!! import ( "fmt" "math/rand" ) func main() { fmt.Println("My favorite number is", rand.Intn(10)) } ``` - 2-2 Imports - パッケージを使いたい時はimportを使って宣言する。 - 宣言(import)しないとエラーが起きるから注意! - よく使いそうな標準パッケージ - fmt 文字の出力させるときに使う。めちゃ使う - math 計算したいときに使う。ランダム関数を呼び出したりできる - net HTTPクライアントとサーバーの実装に使う - io 入出力処理の基本な型とインターフェースを提供する ``` # サンプル #一回ずつ書く方法 import "fmt" import "math" #まとめて書く方法 ()でまとめたらok import ( "fmt" "math" ) ``` - 2-3 Exported Name - 頭文字が大文字になっていると外部パッケージから参照することができる。 - つまり自作したコードをパッケージで呼び出すことができる。 - 他で使いたいときは最初を大文字にすることを意識すればok ``` #サンプルコード exportname.go package main import ( "fmt" "math" ) func main() { fmt.Println(math.Pi) #math.piだと参照できない } ``` - 2-4 Functions - 2-4-1 - mainの中に全部のコードを書くと分かりづらいため、プログラムの処理ごとに関数を定義してまとめる。 - returnを使うと関数の結果を戻り値として渡すことができる。 - 引数は0個以上。 ``` #func関数の基本形 func 関数名(引数名 引数型) 関数型{この中にコード} #引数の型が同じときは省略してもいい func add (x,y int) int{} ``` ``` #サンプルコード function.go package main import "fmt" func add(x int, y int) int { return x + y } func main() { fmt.Println(add(42, 13)) } ``` - 2-4-2 - returnは複数の戻り値を返すことができる。 ``` #サンプルコード multi-result.go package main import "fmt" func swap(x, y string) (string, string) { return y, x } func main() { a, b := swap("hello", "world") fmt.Println(a, b) } ``` - 2-4-3 - 戻り値になる変数にも名前を付けることができる。 - でも長いコードに名前をつけると読みにくくなるので気を付ける。 ``` #サンプルコード named-result.go package main import "fmt" func split(sum int) (x, y int) { x = sum * 4 / 9 y = sum - x return } func main() { fmt.Println(split(18)) } ``` - 2-5 Variables - varを使って変数を宣言する。 - 変数の中に初期値を与えられる。初期値を与えた場合その初期値に合わせた型が入るため型定義を省略できる。 - var宣言の代わりに、:=を使っても宣言できる。しかし、関数の外では使えないので関数内で使う。 ``` #サンプルコード variables.go package main import "fmt" var i, j int = 1, 2 var l int func main() { var c, python, java = true, false, "no!" k := 3 fmt.Println(i, j, k, l, c, python, java) } ``` - 2-6 Types - 2-6-1 - Goの基本型 - bool true/false - string 文字列 - int int8 int16 int32 int64 数値 - uint uint8 uint16 uint32 uint64 uintptr - byte uint8 の別名 - rune int32の別名 - float32/64 少数含む数値 - 初期値を入れないとゼロ値が与えられる。 - 数値型(int,float) 0 - bool型 false - string型 ""(空文字) ``` #サンプルコード zero.go package main import "fmt" func main() { var i int var f float64 var b bool var s string fmt.Printf("%v %v %v %q\n", i, f, b, s) } ``` - 2-6-2 - 型の変換を行うことができる。 ``` #変換方法 変換する型名(変換したい変数名) #intとstringの変換でよく使われる方法 strconv.Atoi str→int strconv.Itoa int→str ``` ``` #サンプルコード type-convert.go package main import ( "fmt" "math" ) func main() { var x, y int = 3, 4 var f float64 = math.Sqrt(float64(x*x + y*y)) var z uint = uint(f) fmt.Println(x, y, z) #シンプルに記述することもできる i := 42 k := float64(i) u := uint(f) fmt.Println(i, k, u) } ``` - 2-6-3 - 明示的に型を指定しないで宣言する場合(:=,var =)、型は右側の変数から推論される。 ``` #サンプルコード type-inference.go package main import "fmt" func main() { v := 42 f := 3.142 fmt.Printf("v is of type %T\n", v) fmt.Printf("f is of type %T\n", f) } ``` - 2-7 Constants - 2-7-1 - 変数のように<b>定数</b>を宣言することができる。 - 定数は文字、文字列。boolean、数値のみで使える。しかし、:=を使っての宣言はできない。 ``` #サンプルコード constants.go package main import "fmt" const Pi = 3.14 func main() { const World = "世界" fmt.Println("Hello", World) fmt.Println("Happy", Pi, "Day") const Truth = true fmt.Println("Go rules?", Truth) } ``` - 2-7-2 - 数値の定数は非常に精度が高く、型のない定数はその状況によって必要な型を取る。 ``` #サンプルコード numeric.go package main import "fmt" const ( Big = 1 << 100 Small = Big >> 99 ) func needInt(x int) int { return x*10 + 1 } func needFloat(x float64) float64 { return x * 0.1 } func main() { fmt.Println(needInt(Small)) fmt.Println(needFloat(Small)) fmt.Println(needFloat(Big)) } ``` - 2-8 For - 2-8-1 - for文は条件に合っているときだけ実行を繰り返す処理です。 - for文はセミコロン(;)で3つの部分に分かれている。 - `初期化ステートメント;条件式;後処理ステートメント{}` のように定義する。 - 初期化ステートメント: 繰り返しの前に初期化が実行される。 - 条件式: 繰り返し毎に条件に合っているか判断される。 - 後処理ステートメント: 繰り返し毎の最後に実行される。 - 初期化ステートメントの変数はfor文のスコープ内でのみ使用できる。 - for文のループは条件式がfalseになったら停止される。 ``` #サンプルコード for.go package main import "fmt" func main() { sum := 0 for i := 0; i < 10; i++ { sum += i } fmt.Println(sum) } ``` - 2-8-2 - 初期化ステートメントと後処理ステートメントは書かなくてもいい。 ``` #サンプルコード for-continue.go package main import "fmt" func main() { sum := 1 for ; sum < 1000; { sum += sum } fmt.Println(sum) } ``` - 2-8-3 - セミコロン(;)を省略することもできる。動きとしてはC言語のwhile文と同じなので疑似的にwhileを扱える。 ``` #サンプルコード for-while.go package main import "fmt" func main() { sum := 1 for sum < 1000 { sum += sum } fmt.Println(sum) } ``` - 2-8-4 - ループ条件を省略することで無限ループになる。サンプルはのせるけどtimeoutエラーになるよ。 ``` package main func main() { for { } } ``` - 2-9 If - 2-9-1 - if文は条件に合っているとき(true)のときに処理が実行される。 ``` #サンプルコード if.go package main import ( "fmt" "math" ) func sqrt(x float64) string { if x < 0 { return sqrt(-x) + "i" } return fmt.Sprint(math.Sqrt(x)) } func main() { fmt.Println(sqrt(2), sqrt(-4)) } ``` - 2-9-2 - if文はfor文のように条件式の前に簡単なステートメントを書くことができる。 - ここで宣言された変数は、ifのスコープ内でのみ使用ができる。 ``` #サンプルコード if-statement.go package main import ( "fmt" "math" ) func pow(x, n, lim float64) float64 { if v := math.Pow(x, n); v < lim { return v } return lim } func main() { fmt.Println( pow(3, 2, 10), pow(3, 3, 20), ) } ``` - 2-9-3 - <b>else</b>を用いることでif文の条件に合わないとき(false)の処理を書くことができる。 - また、ifステートメントで宣言された変数はelseプロック内でも使用できる。 ``` #サンプルコード if-else.go package main import ( "fmt" "math" ) func pow(x, n, lim float64) float64 { if v := math.Pow(x, n); v < lim { return v } else { fmt.Printf("%g >= %g\n", v, lim) } // can't use v here, though return lim } func main() { fmt.Println( pow(3, 2, 10), pow(3, 3, 20), ) } ``` - 2-10 Switch - 2-10-1 - swithはif-elseステートメントのシーケンスを短く書くことができる。 - 選択されたcaseのみを実行し、caseは定数である必要がない。 ``` #サンプルコード switch.go package main import ( "fmt" "runtime" ) func main() { fmt.Print("Go runs on ") switch os := runtime.GOOS; os { case "darwin": fmt.Println("OS X.") case "linux": fmt.Println("Linux.") default: // freebsd, openbsd, // plan9, windows... fmt.Printf("%s.\n", os) } } ``` - 2-10-2 - caseは上から下へと判断する。caseの条件が一致したらそこで停止(自動的にbreak)する。 ``` #サンプルコード switch-order.go package main import ( "fmt" "time" ) func main() { fmt.Println("When's Saturday?") today := time.Now().Weekday() switch time.Saturday { case today + 0: fmt.Println("Today.") case today + 1: fmt.Println("Tomorrow.") case today + 2: fmt.Println("In two days.") default: fmt.Println("Too far away.") } } ``` - 2-10-3 - 条件をつけないことでswitch trueとなる。 - if-elseの長くなるコードをswitchで短く表現できる。 ``` #サンプルコード switch-condition package main import ( "fmt" "time" ) func main() { t := time.Now() switch { case t.Hour() < 12: fmt.Println("Good morning!") case t.Hour() < 17: fmt.Println("Good afternoon.") default: fmt.Println("Good evening.") } } ``` - 2-11 Defer - 2-11-1 - deferステートメントはdeferに渡された関数の実行を、呼び出し元の終わり(return)まで遅延させる。 - deferに渡された関数の引数は、その関数自体はreturnするまで実行されない。 ``` #サンプルコード defer.go package main import "fmt" func main() { defer fmt.Println("world") fmt.Println("hello") } ``` - deferに複数の関数を渡すと、よびだしはスタックされる。関数がreturnするとき、LIFO(last-in-fast-out)後入れ先出しの順番で実行される。 ``` #サンプルコード defer-multi.go package main import "fmt" func main() { fmt.Println("counting") for i := 0; i < 10; i++ { defer fmt.Println(i) } fmt.Println("done") } ``` - 2-12 Pointer - ポインタは変数の値のメモリアドレスを指す。ポインタ演算はない。 - &オペレータで、ポインタを引き出す - *オペレータでポインタの指す変数を示す。 ``` #ポインタの定義 var p *int #ポインタの引き出し i := 42 p = &i ``` ``` #ポインタの定義 pointer.go package main import "fmt" func main() { fmt.Println("counting") for i := 0; i < 10; i++ { defer fmt.Println(i) } fmt.Println("done") } ``` - 2-13 Structs - 2-13-1 - 構造体は名前と型を持つフィールドの集まりである。 ``` #サンプルコード struct.go package main import "fmt" type Vertex struct { X int Y int } func main() { fmt.Println(Vertex{1, 2}) } ``` - 2-13-2 - 構造体のフィールドはドット( . )を用いてアクセスする。 ``` #サンプルコード struct-fiels.go package main import "fmt" type Vertex struct { X int Y int } func main() { v := Vertex{1, 2} v.X = 4 fmt.Println(v.X) } ``` - 2-13-3 - 構造体のフィールドはポインタを通してアクセスすることができる。 ``` #サンプルコード struct-pointer.go package main import "fmt" type Vertex struct { X int Y int } func main() { v := Vertex{1, 2} p := &v p.X = 1e9 fmt.Println(v) } ``` - 2-13-4 - 構造体のリテラルは、フィールドの値を列挙することで初期値の割り当てを行う。 - x: 1のようにフィールドの一部を列挙することができる。 ``` #サンプルコード struct-literal.go package main import "fmt" type Vertex struct { X int Y int } func main() { v := Vertex{1, 2} p := &v p.X = 1e9 fmt.Println(v) } ``` - 2-14 Arrays - 配列は同じ型をもつ要素を並べたものである。 - 配列で一つの変数で複数の値を扱うことができる。 - 配列のサイズは固定長で後から変更することはできない。 ``` #配列の宣言方法1 var 変数名 [配列の長さ]型名 #配列の宣言方法2 変数名 := [...]型名(初期値n個) ``` ``` #サンプルコード array.go package main import "fmt" func main() { var a [2]string a[0] = "Hello" a[1] = "World" fmt.Println(a[0], a[1]) fmt.Println(a) primes := [6]int{2, 3, 5, 7, 11, 13} fmt.Println(primes) } ``` - 2-15 Slices - 2-15-1 - スライスは可変長で柔軟な配列を作ることができる。 ``` #スライスの宣言方法1 var 変数名 []型名 #スライスの宣言方法2 var 変数名 []型名 = []型名{初期値n} ``` ``` #サンプルコード slice.go package main import "fmt" func main() { primes := [6]int{2, 3, 5, 7, 11, 13} var s []int = primes[1:4] fmt.Println(s) } ``` - 2-15-2 - スライスは配列に参照しているようなものである。 - スライス自体にはデータが格納されてなく部分列を示している。 - スライスの要素を変更すると元の配列の要素も変更される。 ``` #スライスの宣言方法3 変数名 := 配列名[start : end] ``` ``` package main import "fmt" func main() { names := [4]string{ "John", "Paul", "George", "Ringo", } fmt.Println(names) a := names[0:2] b := names[1:3] fmt.Println(a, b) b[0] = "XXX" fmt.Println(a, b) fmt.Println(names) } ``` - 2-15-3 - スライスのリテラルは長さのない配列リテラルのようなものである。 ``` #サンプルコード slice-literal.go package main import "fmt" func main() { q := []int{2, 3, 5, 7, 11, 13} fmt.Println(q) r := []bool{true, false, true, true, false, true} fmt.Println(r) s := []struct { i int b bool }{ {2, true}, {3, false}, {5, true}, {7, true}, {11, false}, {13, true}, } fmt.Println(s) } ``` - 2-15-4 - スライスの既定値を代わりに使うことで上限、下限を省略することができる。 - 既定値の下限は0で上限はスライスの長さである。 ``` #サンプルコード struct-bound.go package main import "fmt" func main() { s := []int{2, 3, 5, 7, 11, 13} s = s[1:4] fmt.Println(s) s = s[:2] fmt.Println(s) s = s[1:] fmt.Println(s) } ``` - 2-15-5 - スライスは長さと容量の両方を持っている。 - 長さは要素数で、容量はスライスの最初の要素から元となる配列の要素数である。 ``` #スライスの長さを得る方法 len(スライス名) #スライスの容量を得る方法 cap(スライス名) ``` ``` #サンプルコード slice-len-cap.go package main import "fmt" func main() { s := []int{2, 3, 5, 7, 11, 13} printSlice(s) // Slice the slice to give it zero length. s = s[:0] printSlice(s) // Extend its length. s = s[:4] printSlice(s) // Drop its first two values. s = s[2:] printSlice(s) } func printSlice(s []int) { fmt.Printf("len=%d cap=%d %v\n", len(s), cap(s), s) } ``` - 2-15-6 - スライスのゼロ値は`nil`であり、`nil`は0の長さと容量を持っている。 ``` #サンプルコード slice-nil.go package main import "fmt" func main() { var s []int fmt.Println(s, len(s), cap(s)) if s == nil { fmt.Println("nil!") } } ``` - 2-15-7 - スライスはmakeという組み込み関数を用いても作成することができる。 ``` #makeでの作成方法 変数名 := ([]型名, 長さ, 容量) ``` ``` #サンプルコード slice-make.go package main import "fmt" func main() { a := make([]int, 5) printSlice("a", a) b := make([]int, 0, 5) printSlice("b", b) c := b[:2] printSlice("c", c) d := c[2:5] printSlice("d", d) } func printSlice(s string, x []int) { fmt.Printf("%s len=%d cap=%d %v\n", s, len(x), cap(x), x) } ``` - 2-15-8 - スライスの型が同じときはスライスの中にスライスを代入することができる。 ``` #サンプルコード slice-of-slice.go package main import ( "fmt" "strings" ) func main() { // Create a tic-tac-toe board. board := [][]string{ []string{"_", "_", "_"}, []string{"_", "_", "_"}, []string{"_", "_", "_"}, } // The players take turns. board[0][0] = "X" board[2][2] = "O" board[1][2] = "X" board[1][0] = "O" board[0][2] = "X" for i := 0; i < len(board); i++ { fmt.Printf("%s\n", strings.Join(board[i], " ")) } } ``` - 2-15-9 - 組み込み関数の`append`を用いることで配列に要素を追加することができる。 ``` #appendの使い方 newslice = (slice, 要素) ``` ``` #サンプルコード slice-append.go package main import "fmt" func main() { var s []int printSlice(s) // append works on nil slices. s = append(s, 0) printSlice(s) // The slice grows as needed. s = append(s, 1) printSlice(s) // We can add more than one element at a time. s = append(s, 2, 3, 4) printSlice(s) } func printSlice(s []int) { fmt.Printf("len=%d cap=%d %v\n", len(s), cap(s), s) } ``` - 2-16 Range - 2-16-1 - rangeはforループのようなものでスライスやマップを反復処理するときに使用する。 - スライスをrangeで繰り返すと、インデックスとインデックスの要素のコピーの2つの変数を返す。 ``` #サンプルコード range.go package main import "fmt" var pow = []int{1, 2, 4, 8, 16, 32, 64, 128} func main() { for i, v := range pow { fmt.Printf("2**%d = %d\n", i, v) } } ``` - 2-16-2 - アンダーバー(_)を代入するとインデックスや値を捨てることができる。 ``` #サンプルコード range-continue.go package main import "fmt" func main() { pow := make([]int, 10) for i := range pow { pow[i] = 1 << uint(i) // == 2**i } for _, value := range pow { fmt.Printf("%d\n", value) } } ``` - 2-17 Maps - 2-17-1 - mapはキーと値を関連付ける。 ``` #サンプルコード map.go package main import "fmt" type Vertex struct { Lat, Long float64 } var m map[string]Vertex func main() { m = make(map[string]Vertex) m["Bell Labs"] = Vertex{ 40.68433, -74.39967, } fmt.Println(m["Bell Labs"]) } ``` - 2-17-2 - mapリテラルはキーが必要になる。 ``` #サンプルコード map-literal.go package main import "fmt" type Vertex struct { Lat, Long float64 } var m = map[string]Vertex{ "Bell Labs": Vertex{ 40.68433, -74.39967, }, "Google": Vertex{ 37.42202, -122.08408, }, } func main() { fmt.Println(m) } ``` - 2-17-3 - トップレベル単純な型の場合、リテラルの要素から推定できるので省略することができる。 ``` #サンプルコード map-literal-continue.go package main import "fmt" type Vertex struct { Lat, Long float64 } var m = map[string]Vertex{ "Bell Labs": {40.68433, -74.39967}, "Google": {37.42202, -122.08408}, } func main() { fmt.Println(m) } ``` - 2-17-4 - mapの操作の方法 ``` #要素の挿入や更新 map名[key] = 要素 #要素の取得 要素 = map名[key] #要素の削除 delete(map名,key) ``` ``` #サンプルコード map-mutating.go package main import "fmt" func main() { m := make(map[string]int) m["Answer"] = 42 fmt.Println("The value:", m["Answer"]) m["Answer"] = 48 fmt.Println("The value:", m["Answer"]) delete(m, "Answer") fmt.Println("The value:", m["Answer"]) v, ok := m["Answer"] fmt.Println("The value:", v, "Present?", ok) } ``` - 2-18 Function Values - 2-18-1 - 関数を渡すことができる。 - 関数値は、関数の引き数を取り、戻り値として利用できる。 ``` #サンプルコード function-value.go package main import ( "fmt" "math" ) func compute(fn func(float64, float64) float64) float64 { return fn(3, 4) } func main() { hypot := func(x, y float64) float64 { return math.Sqrt(x*x + y*y) } fmt.Println(hypot(5, 12)) fmt.Println(compute(hypot)) fmt.Println(compute(math.Pow)) } ``` - 2-18-2 - クロージャは外部から変数を参照する関数値である。 ``` #サンプルコード function-clouser.go package main import "fmt" func adder() func(int) int { sum := 0 return func(x int) int { sum += x return sum } } func main() { pos, neg := adder(), adder() for i := 0; i < 10; i++ { fmt.Println( pos(i), neg(-2*i), ) } } ``` - 2-19 go mod - go modとは、モジュールのパスを書いてくれるファイルであり、これを定義することで外部モジュールの管理がしやすくなる。 - `go mod init`コマンドを実行することでgo.modファイルを作成し、その時点でのモジュールのパスをまとめてくれる。 - `go mod tidy`コマンドを実行するとgo.sumファイルを作成し、go.modの依存関係のパスをまとめてくれる。 - なので、最初に`go mod init`をして、使いたい外部パッケージやモジュールがあったらその都度、`go mod tidy`をして依存関係のパスをgo.mod,go.sumにまとめる。 - 第二章まとめ - goの基本について学べた - go modは、モジュールのパスをまとめてくれるので外部内部問わずどんどん使っていこう。一応、第3章でもちらっと触れる。 #### 宿題(ヒント付き) - 2-7まで進めた宿題 累乗と平方根を求める計算をする関数を書こう。 #question1.go ``` pacakage main import ( "fmt" "math" ) #平方根の処理 func sqrt() {} #累乗の処理 func pow() {} func main() {} #上記みたいな感じで書いても見よう #それぞれmathの中に機能で計算できるよ ``` - for二重ループ問題 #question2.go ``` #二重ループを使って九九を表現しよう pacakage main import ( "fmt" ) func main () { for { for { } } } #上記の感じで ``` - if,switchの違いを知る問題 #question3.go ``` #テストの結果を入力して60以下なら不合格、80以下なら合格、それ以上なら大金星を挙げるコードをif,switchそれぞれで書こう func ifでやる func switchでやる func main() { #ここで呼び出す } ``` - 配列を使ったバブルソート #question4.go ``` #配列の中身を値の小さい順に並び変えるコードを書こう #バブルソートとは、左右の数を比べて値を入れ替える処理をひたすらに行って中身を変える方法 #二重ループで表現できると思う。 func main() { numbers := [10]int{9, 8, 7, 6, 5, 4, 3, 2, 1, 0} for { for { if { } } } fmt.Println(numbers) } ``` - map,rangeの問題 #question5.go ``` #連想配列[apple:120,banana:100,orange:150]を作り、rangeでその中身を表示するコードを書こう func main() { i := map[string]string{"apple": "120", "banana": "100","orange": "150"} ``` ### 第三章 GoでCRUDサーバーを立ててみよう #### 目標 - GoでのCRUD操作を知る - echo,swaggerを用いた簡単(多分)なCRUD操作ができるアプリを制作してみる。 - これでフレームワークやGoサーバーの雰囲気を掴んでほしい [完成例](https://github.com/hikahana/go_learn_answer) #### 内容 - 3-0 今回使用するものの紹介 - その1 echo - LabStackによって管理される軽量のWebアプリケーションフレームワーク。 - シンプルな設計なので速度が速く、簡単に拡張することができる。 - その2 swagger - OpenAPIを利用したREST APIを定義するための仕様に基づいて製作されたツールセット。 - ツールはEditer,UI,Codegenの三種類あり、今回はUIというドキュメントの表示を使う。 - ドキュメント(フロントエンド)部分を簡単に書けて即座に実装できてとても快適。 - その3 air - Goのホットリロードツール。 - これを実装することでdockerを立ち上げたままでも変更点の修正を即座に行ってくれる。 - その4 MVCモデル - 役割ごとにプログラムを分け手管理するソフトウェア設計モデル。 - Model,View,Controllerの頭文字をとってMVCモデル。 - ModelはDBとのやり取りをする部分を担当し、結果をControllerに投げる。 - ControllerはModelとViewの制御を担当し、それぞれに指示を送る。 - Viewは表示部分のインターフェイスを担当しているが、今回はswaggerがviewになる。 - ディレクトリ構成 go_learn_answer/ |-- api | |-- api | |-- controller | | |-- user.go | | | |-- docs | | |-- docs.go | | |-- swagger.json | | |-- swagger.yaml | | | |-- go.mod | |-- go.sum | |-- main.go | |-- model | | |-- server.go | | |-- user.go | |-- app - 3-1 go(echo,airの導入)とdbの環境構築 - 新たにapi.Dockerfileを作成して、api.Dockerfileとdocker-copose.ymlに以下を追記する。 #api.Dockerfile ``` FROM golang:latest RUN apt-get update RUN apt-get upgrade -y RUN apt-get install -y locales \ && locale-gen ja_JP.UTF-8 \ && echo "export LANG=ja_JP.UTF-8" >> ~/.bashrc #言語を日本語に変更する RUN export LANG=C.UTF-8 RUN export LANGUAGE=en_US: ENV CGO_ENABLED=0 ENV GOOS=linux #ENV GOARCH=amd64 RUN mkdir /go/src/api WORKDIR /go/src/api #swaggerとairのインストール RUN go install github.com/swaggo/swag/cmd/swag@latest RUN go install github.com/cosmtrek/air@latest CMD ["air", "-c", ".air.toml"] ``` #docker-compose.yml ``` #appの下に追加 db: image: mysql:8.0 container_name: 'db' volumes: - ./mysql/db:/docker-entrypoint-initdb.d # 初期データ command: mysqld --character-set-server=utf8mb4 --collation-server=utf8mb4_unicode_ci environment: #databaseのユーザー情報を定義 MYSQL_DATABASE: goleran_db MYSQL_USER: golearn MYSQL_PASSWORD: password MYSQL_ROOT_PASSWORD: root TZ: 'Asia/Tokyo' ports: - '3306:3306' restart: always api: container_name: 'api' build: context: .docker dockerfile: api.Dockerfile volumes: - ./api:/go/src/api environment: ADMIN_NAME: admin ADMIN_PASS: password ports: - '1323:1323' command: './start.sh' depends_on: - 'db' tty: true stdin_open: true ``` - buildして、下記を実行する ``` docker-compose build #apiフォルダが作成されたら cd api #権限を付与してファイルの作成を行えるようにする sudo sudo chmod -R 777 ./ go mod init go mod tidy #下記はvscodeで作成しても良き touch main.go touch .air.toml ``` - main.goと.air.tomlの中身を記述する #main.go ``` package main import ( "net/http" "github.com/labstack/echo/v4" ) func main() { #echoを起動 e := echo.New() #path指定して処理を実行させる e.GET("/", func(c echo.Context) error { return c.String(http.StatusOK, "Hello, World!") }) #ポートを開く、docker-compose.ymlと同じポート番号にする e.Logger.Fatal(e.Start(":1323")) } ``` #.air.toml ``` #ほぼ初期設定 root = "." testdata_dir = "testdata" tmp_dir = "tmp" [build] args_bin = [] bin = "./tmp/main" cmd = "go build -o ./tmp/main ./main.go" delay = 1000 exclude_dir = ["assets", "tmp", "vendor", "testdata"] exclude_file = [] exclude_regex = ["_test.go"] exclude_unchanged = false follow_symlink = false full_bin = "" include_dir = [] include_ext = ["go", "tpl", "tmpl", "html"] kill_delay = "0s" log = "build-errors.log" send_interrupt = false stop_on_error = true [color] app = "" build = "yellow" main = "magenta" runner = "green" watcher = "cyan" [log] time = false [misc] clean_on_exit = false [screen] clear_on_rebuild = false ``` - dockerを起動して動作確認してみる ``` docker-compose up #タブを開いて http://localhost:1323/ hello world と表示されていたら成功! #apiディレクトリの中にgo.modファイルを作成する go mod init go_learn/api go mod tidy ``` - 3-2 GORMを使用してdbと接続する - 今回はMVCモデルで構築するのでmodelフォルダを作成してserver.goを作成する。 ``` # vscodeで作成してもよき mkdir model cd model touch server.go ``` - server.goに以下を記述する。 - dsnの定義は、user_name:user_password@ycp(ymlに書いたservice名:ポート番号)/database_name?のように記述する。 #server.go ``` package model import ( "log" "gorm.io/driver/mysql" "gorm.io/gorm" ) var DB *gorm.DB var err error func init() { dsn := "golearn:password@tcp(db:3306)/golearn_db?charset=utf8mb4&parseTime=True&loc=Local" DB, err = gorm.Open(mysql.Open(dsn), &gorm.Config{}) if err != nil { log.Fatalln(dsn + "database can't connect") } } ``` - main.goを変更して、dbに接続できるか確認する。 #main.go ``` package main import ( "net/http" "github.com/labstack/echo/v4" #ここは個人のやつに合わせて変更してね "github.com/hikahana/go_learn_answer/api/model" ) func connect(c echo.Context) error { db, _ := model.DB.DB() defer db.Close() err := db.Ping() if err != nil { return c.String(http.StatusInternalServerError, "DB接続失敗しました") } else { return c.String(http.StatusOK, "DB接続しました") } } func main() { e := echo.New() e.GET("/", connect) e.Logger.Fatal(e.Start(":1323")) } ``` ``` #gormモジュールのパスを設定する go mod tidy #main.goを変更したら docker-compose up #エラーが出なかったら http://localhost:1323/ #エラーがでたらわかんない。downしてもう一回upしたらいくかも?  DB接続しました が表示されたらok ``` - 3-3 echo-swaggerの導入 - フロントの部分を今回はecho-swaggerで定義していきます。 - main.goに設定コメントをつける #main.go ``` import ( echoSwagger "github.com/swaggo/echo-swagger" ) type ( Response struct { Int64 int64 `json:"int64"` String string `json:"string"` World *Item `json:"world"` } Item struct { Text string `json:"text"` } ) // @title go_learn API // @version 1.0 // @description go_learn API // @host localhost:1323 // @BasePath / // @schemes http #func main()の上にコメントを付ける func main() { ``` - swagコマンドで自動生成を行う。 ``` #main.goがあるパスでやる必要がある。 cd go_learn/api swag init #行かない場合 #goの環境変数先(GOPATH)を確認する go env export PATH="(GOPATH)/bin:$PATH" source ~/.bashrc swag -v #これでもいかないならお手上げ手入力でもするか? ``` - main.goにswaggerコマンドを追加する。 ``` import ( "github.com/(github名)/go_learn/api/docs" ) func main() { e.GET("/swagger/*", echoSwagger.WrapHandler) } ``` - コンテナを立ち上げてswaggerを確認しよう! ``` docker-compose up http://localhost:1323/swagger/index.html #確認出来たら ctrl + c docker-compose down ``` - 適宜docs.goに追記していきます。 - 3-4 CRUDの実装を行う。Create編 - usersテーブル構造体を定義して、gormでusersテーブルを作成する。 - modelフォルダの中にuser.goを作り、以下を記述する。 ``` #vscodeでもよき cd model touch user.go ``` #model/user.go ``` package model import "time" type User struct { ID uint `json:"id" param:"id" gorm:"primary_key"` Name string `json:"name"` CreatedAt time.Time `json:"created_at"` UpdatedAt time.Time `json:"updated_at"` } ``` - server.goにテーブル作成コマンドを追加する。 ``` func init() { dsn := "golearn:password@tcp(db:3306)/golearn_db?charset=utf8mb4&parseTime=True&loc=Local" DB, err = gorm.Open(mysql.Open(dsn), &gorm.Config{}) if err != nil { log.Fatalln(dsn + "database can't connect") } #ここを追加する。 DB.AutoMigrate(model.User{}) } ``` - Create処理を作成する。 - controllerフォルダを作成して、user.goにCreate処理を記述する。 ``` #vscodeでも良き mkdir controller cd controller touch user.go ``` #controller/user.go ``` package controller import ( "net/http" "github.com/labstack/echo/v4" "github.com/hikahana/go_learn_answer/api/model" ) func CreateUser(c echo.Context) error { nema := c.queryParam("name") user := model.User{ Name: name, } if err := c.Bind(&user); err != nil { return err } model.DB.Create(&user) return c.JSON(http.StatusCreated, user) } ``` - main.goとdocs.goにcreate部分を追加する。 #main.go ``` e.POST("/users", controller.CreateUser) ``` #docs.go ``` #paths /heelo-worldの下に追加する。 #セミコロン(,)をparameterとpathの終わりにつける!! "/users": { "post": { "description": "write on username", "parameters": [ { "name": "name", "type": "string", "in": "query", "description": "ユーザー名", } ], "responses":{ "200":{ "description":"Created user", } } } }, ``` - コンテナを立ち上げて、create処理をやってみる。 ``` docker-compose up http://localhost:1323/swagger/index.html #うまくいってるとPOSTが追加されてる #try itを押してユーザー名を入れてExcuteで実行する。 #下記みたいになってたら成功! Respose body { "id": 3, "name": "test", "created_at": "2023-04-10T04:50:00.364Z", "updated_at": "2023-04-10T04:50:00.364Z" } #確認出来たら ctrl + c docker-compose down ``` - 3-5 CRUDの実装を行う。Read編 - Read(GET)の処理を記述してpostで追加したユーザー情報を確認していきます。 - controller/user.goにread処理を記述する。 - readは全ユーザーとID検索で一人だけを抽出する二つの関数を追記する。 #controller/user.go ``` #Createuserの下に記述 #全ユーザーを取得 func GetUsers(c echo.Context) error { users := []model.User{} model.DB.Find(&users) return c.JSON(http.StatusOK, users) } #ユーザーIDから一件のみを取得 func GetUser(c echo.Context) error { user := model.User{} if err := c.Bind(&user); err != nil { return err } model.DB.Take(&user) return c.JSON(http.StatusOK, user) } ``` - main.goとdocs.goにread部分を追記する。 #main.go ``` e.GET("/users", controller.GetUsers) e.GET("/users/:id", controller.GetUser) ``` #docs.go ``` "/users": { "post": { #省略 } #ここに全ユーザ取得を追加する。 "get": { "description": "show users", "responses":{ "200":{ "description":"Created user", "schema":{ "type":"array", } } } } }, #"users"の下に"users/{id}"を追加する。 "/users/{id}": { "get": { "description": "show users", "responses":{ "200":{ "description":"Created user", "schema":{ "type":"array", } } } } } ``` - 確認する ``` docker-compose up http://localhost:1323/swagger/index.html #うまくいってるとPOSTの下にGETが追加されてる #try itを押してユーザー名を入れてExcuteで実行する。 #確認出来たら ctrl + c docker-compose down ``` - 3-6 CRUDの実装を行う。Update編 - ユーザー名に変更があったときにIDを指定して情報を書き換えられるようにupdate処理を追記する。 - controller/user.goにupdate処理を追記する。 #controller/user.go ``` func UpdateUser(c echo.Context) error { id, _ := strconv.Atoi(c.Param("id")) name := c.QueryParam("name") user := model.User{ ID: uint(id), Name: name, CreatedAt: time.Now(), UpdatedAt: time.Now(), } if err := c.Bind(&user); err != nil { return err } model.DB.Save(&user) return c.JSON(http.StatusOK, user) } ``` - main.goとdocs.goにupdate部分を追加する。 #main.go ``` e.PUT("/users/:id", controller.UpateUser) ``` #docs.go ``` #"users/:id": { "get": { の下に追加する。 }, "put": { "description": "change on username", "parameters": [ { "name": "id", "type": "integer", "in": "path", "description": "ユーザーID", "required": true, }, { "name": "name", "type": "string", "in": "query", "description": "ユーザー名", } ], "responses":{ "200":{ "description":"Updated user", } } }, ``` - 確認してみる。 ``` docker-compose up http://localhost:1323/swagger/index.html #うまくいってるとusers/:idのGETの下にあると思う。 #try itを押してidと変更したいユーザー名を入れてExcuteで実行する。 #実行したらusersのGETを実行して変更されているか見てみよう! #確認出来たら ctrl + c docker-compose down ``` - 3-7 CRUDの実装を行う。Delete編 - ユーザーを誤って作成してしまった!このユーザーはもう必要なしとなったときにIDを指定して情報を削除できるようにする。 - controller/user.goにdelete処理を記述する。 #controller/user.go ``` func DeleteUser(c echo.Context) error { id, _ := strconv.Atoi(c.Param("id")) user := model.User{ ID: uint(id), } if err := c.Bind(&user); err != nil { return err } model.DB.Delete(&user) return c.JSON(http.StatusOK, user) } ``` - main.goとdocs.goにdelete部分を追記する。 #main.go ``` e.DELETE("user/:id" ,controller.DeleteUser) ``` #docs.go ``` #"users/:id"のところに追記する "delete": { "description": "change on username", "parameters": [ { "name": "id", "type": "integer", "in": "path", "description": "ユーザーID", "required": true, } ], "responses":{ "200":{ "description":"Updated user", } } }, ``` - 確認してみる。 ``` docker-compose up http://localhost:1323/swagger/index.html #うまくいってるとusers/:idのところにある。 #try itを押してidを入れてExcuteで実行する。 #実行したらusersのGETを実行して削除されているかを確認する。 #確認出来たら ctrl + c docker-compose down ``` - 完成です!お疲れさまでした!! ## 参考文献 - 第2章 - https://go-tour-jp.appspot.com/ - 第3章 - https://zenn.dev/shimpo/articles/go-echo-gorm-rest-api#update - https://ken-aio.github.io/post/2019/02/05/golang-echo-swagger/ - https://tech.every.tv/entry/2022/03/28/170000