Skip to content

Instantly share code, notes, and snippets.

@yut148
Forked from nasa9084/golang101.md
Created July 20, 2019 05:02
Show Gist options
  • Save yut148/172f0eb36f87f0a84f61db728c0b291f to your computer and use it in GitHub Desktop.
Save yut148/172f0eb36f87f0a84f61db728c0b291f to your computer and use it in GitHub Desktop.

Golang 101

事前準備

Go言語での開発をするために、まずは環境整備をしましょう。

Go言語のインストール

公式サイトより、ご自身の環境に合わせてインストーラをダウンロードし、Go言語の実行環境をインストールしてください。 バージョンは最新(本ガイドを書いた時点では1.12.6)をインストールしてください。

インストールが完了したら、次のコマンドでGo言語のバージョンを確認してください。

$ go version

go version go1.12 darwin/amd64(macOS、Go1.12の場合)のように表示されればOKです。

環境変数の設定

環境変数GO111MODULEonを設定してください。Linux/macOSでは、export GO111MODULE=onと実行する(現在のセッションに適用する)か、.bashrcや.zshrcなどに記載する(永続的に適用する)ことで設定できます。

エディタの用意

Go言語はお好みのエディタで開発を行うことができます。日常的に使用しているエディタがあればそちらをそのまま使用してください。 一般的にプログラミングで使用されているエディタであれば、ほとんどの場合Go言語拡張が用意されています。

  • emacs: go-modeをインストールしてください。
  • vim: vim-goが有名なようです。
  • Visual Studio Code: vscode-goプラグインをインストールしてください。
  • JetBrains(IntelliJ)ユーザ: GoLandが人気のようです(ライセンスについては各自ご確認ください。

プログラムの実行

準備ができたら、早速Go言語のプログラムを実行してみましょう!

任意の場所にディレクトリを作成し、その中にGoのプログラムを作成します。お好きなエディタで、次の内容でmain.goファイルを作成します。

package main

import "fmt"

func main() {
	fmt.Println("Hello, world!")
}

作成できたら、端末上で作成したディレクトリへ移動し、次のコマンドを実行します。

$ go run main.go

Hello, world!と表示されたら成功です!

次に、Goのプログラムをビルド(コンパイル、と言っても良いです)してみます。

$ go build main.go

同じディレクトリにmainという実行ファイルが作成されます。これはお手元のPCのOS、CPUアーキテクチャにより違う形式のファイルが生成されます。

実行ファイルができましたから、実行してみましょう。

$ ./main

先ほどと同じように、Hello, world!と表示されたでしょうか。Goのプログラムはこのように、ビルドして実行形式のファイルを出力して、配付することができます。実は先ほどのgo runも内部では一時ディレクトリ(Linux, macでは/tmp)にビルド結果を配置して、それを実行しています。

また、詳細な説明は省きますが、環境変数を設定することで、違うOSやアーキテクチャの(例えば、macOS上でLinuxの)実行ファイルをビルドすることもできます。そのため、macOSで開発したコマンドラインツールを、WindowsユーザやLinuxユーザ向けにビルドして配付することが可能です。これが、Go言語が人気な理由の一つです。

packageとmain関数

さて、作成したプログラムを見ながら、Go言語のプログラムについて説明していきましょう。

Go言語のプログラムは「パッケージ」という単位で開発をします。パッケージは複数(もちろん、一つの場合もあります)の .go ファイルを含む一つのディレクトリで、他のパッケージから読み込むことができます。一行目に書いた package mainは、この、他のパッケージから読み込まれる際のデフォルトの名前を指定しています。この場合は、mainパッケージであることを指定しています。

実はmainパッケージはちょっと特殊なパッケージで、「このパッケージは実行するためのパッケージである」ということを示します。go rungo buildでビルドされるのはmainパッケージと決まっています。逆に、他のパッケージから読み込まれるためのパッケージはmainという名前にしてはいけません。

他のパッケージを読み込む際はimport"(ダブルクォート)で括ったパッケージ名を記述します。標準パッケージの場合はそのままパッケージ名を記述します(例: "fmt")。GitHub上にあるパッケージを使用したい場合は、github.comから始まるパスを記述します(例: "github.com/nasa9084/go-totp")。標準パッケージの一覧と詳細なドキュメントはgolang.org/pkgで見ることができます。

パッケージ名は文字から始まり、文字と数字、_(アンダーバー)を使って自由に名前を付けることができます(誰もやりませんが、日本語で名前を付けることもできます)。あまり長くない名前で、_や数字を使っていない名前が多いです。
一つのディレクトリには一つのパッケージ(ちょっと例外もありますが)しか入れることはできません。ディレクトリ内の.goファイルはすべて同じパッケージ名を持つようにしてください。

package宣言とimport宣言を終えたら、本体のプログラムを書きます。今回はmain関数を書きました。

mainパッケージを実行する際に一番最初に実行されるのがmain()関数です。関数の定義の仕方などは後述しますが、func main()が始めに呼ばれます。C言語などでは引数にコマンドライン引数を受け取ったりしますが、Go言語のプログラムでは受け取りません。返値もない、ただのfunc main()です。覚えやすいですね。

コマンドラインツールなどの実行可能なプログラムを書くときはまず、main関数から書き始めるということを覚えておいてください。このあと、いくつかのサンプルプログラムを例示しますが、特に関数定義や説明のない場合はmain関数の中で実行すると動作を確かめることができます。

The Go Playground

Go言語の学習を始める前に、The Go Playgroundを紹介しておきましょう。The Go Playgroundはweb上でGo言語のプログラムを実行できる環境です。ウェブブラウザで開き、黄色い背景のエディタ部分にコードを入力、画面上部のRunボタンを押すことで実行することができます。標準出力は画面下部に出力されます。

学習の際、ちょっとした挙動を確かめるために非常に便利ですし、URLでコードの共有もできますから、是非積極的に使ってみてください。

Go言語の言語仕様

Go言語の言語仕様は非常にシンプルで、1ページにまとまっています。本テキストも言語仕様標準パッケージドキュメントを参考に作成されています。

Go言語の基本文法

コメント

Go言言のプログラム中に何らかのコメント(例えばドキュメントだったり、将来的に実装しなければならない、というTODOであったり)を書き込むには、次のように書きます。

// 行コメント。行末までがコメント
/* コメント */

セミコロン ;

C言語やJavaといった言語と同様、Go言語では処理のひとまとまりを表す終端記号として セミコロン; を使用しますが、ほとんどの場合省略可能です。 省略した際は、ビルド時にセミコロンが自動挿入されます(挿入ルールは公式ドキュメント: セミコロンを参照してください)。

定数と変数

Go言語では値を格納しておく(あるいは値に付けるラベル)として、定数と変数の二種類が用意されています。定数は値が変更できず、変数は変更できます。他の言語では定数を定義する際にはすべて大文字で命名することとされている場合も多いですが、Go言語では変数も定数も同じように命名します。

定数を宣言するときにはconst文を使用します。これは関数の外でも、関数の中でも宣言することができます。

const Foo = 100

変数を定義する方法は二つあります。一つ目はvar文を使用した方法で、(ほとんど)どこでも使用することができます。

var Bar int = 0
var Baz = 100  // 型宣言を省略することもできる
var Baz int    // 変数名だけを宣言することもできる

コメントに記載したように、型の宣言を省略することもできます。この場合、変数が型を持たないわけではなく、右辺から型情報が推測されます。 また、三行目のように変数名の宣言だけを行い、初期化処理を省略した場合には値としてそれぞれの型の初期値が設定されます(例えば、int型であれば0、string型であれば空文字列)。 そのため、次の三つの変数宣言は同じ意味を持ちます。

var X int = 0
var X = 0
var X int

もう一つは:=を使った方法で、関数内でのみ使用することができます。play

// https://play.golang.org/p/Fen0IU5gfQ0

func main() {
  foo := 100
  fmt.Println(foo * 2)
  // Output:
  //  200
}

関数内で定義した変数が使用されていない場合、コンパイル時にエラーとなりますので注意しましょう。

constvarは括弧を使用してまとめることもできます。

const (
  MinFoo = 0
  MaxFoo = 100
)

命名と公開範囲

変数や定数、あるいは関数など、Go言語で命名を行うときには、先頭の文字が非常に大きな意味を持ちます。Go言語ではpackageという単位で複数のソースコードファイルをひとまとめにしますが、小文字から始まる変数などはpackage内でしか参照できません。packageの外から参照したい場合は名前を大文字で始める必要があります。

iota

iotaは定数を定義するときに使用できる特殊な値で、連続した整数値として取り扱うことができます。説明が少々難しいので、例を見てみましょう。play

// https://play.golang.org/p/EBRDD9qLYlu

const (
   A = iota // A = 0
   B = iota // B = 1
   C = iota // C = 2
   D = iota // D = 3
)

括弧でくくったconstグループの中であれば、二度目以降のiotaを省略することもできます。play

// https://play.golang.org/p/1YavGGarLpu

const (
   A = iota // A = 0
   B        // B = 1
   C        // C = 2
   D        // D = 3
)

iotaに対して何かの演算を行った場合、二つ目以降が省略された場合でも、最初に指定した演算と同じものとして扱われます。play

// https://play.golang.org/p/z9I1o5TjvBL

const (
    Flg1 = 1 << iota // 1
    Flg2             // 2
    Flg3             // 4
)

この、bit shiftとiotaを用いた定数定義は複数選択可能なフラグの管理などに便利です。

iotaconstを書くごとに0にリセットされます。play

// https://play.golang.org/p/svXJ0UEqZkA

const A = iota  // A = 0
const B = iota  // B = 0

const (
    Foo = iota // Foo = 0
    Bar        // Bar = 1
)

const (
    Hoge = iota // Hoge = 0
    Fuga        // Fuga = 1
)

Blank指定子

Go言語では変数を宣言しているのに使用していないという場合、コンパイルエラーとなります。しかし一方で、値が必要ない場合(例えば、複数の返値の内一部だけが必要な場合など)もあります。そういった場合に使用できる、「値を捨てるための変数」がブランク指定子です。ブランク指定子は_(アンダーバー)1文字の変数で、どこでも使用することができます。

リテラル

リテラルとは、ソースコード中で、数値や文字列を直接に記述した定数のことです。例えば、これまで整数を直接記述した例がいくつか出てきましたが、これは整数リテラルです。整数リテラル、浮動小数点数リテラルはそのまま数値を記述することができます。

文字列を記述するためのリテラルが文字列リテラルで、二つの書き方があります。一つ目は "..."(ダブルクォート)を使用する書き方で、改行等を含むことができませんが、"\n"(改行)や"\t"(TAB)などのエスケープシーケンスを使用することができます。二つ目は ...(バッククォート)を使用する書き方で、改行を含むことができますが、エスケープシーケンスは解釈されません。play

文字を表現するリテラルとしてRuneリテラルも用意されており、 '.'(シングルクォート)を使用します。

// https://play.golang.org/p/9Ef3XKhQKOU

dq := "Hello\nWorld"
fmt.Println(dq)
bq := `Hello\nWorld`
fmt.Println(bq)
// Output:
//   Hello
//   World
//   Hello\nWorld

真偽値リテラルは二つの値があり、truefalse(それぞれすべて小文字)をそのまま書くことができます。

foo := true
bar := false

Go言語独特のリテラルとして、複合リテラルがあります。複合リテラルは後述する配列やSlice、mapや構造体を初期化するためのリテラルです。型名の後に {}(中括弧)を付けるかたちで記述します。中括弧の中には値を列挙するか、index:valueの形で値を記述します。play

// https://play.golang.org/p/HF35Tl0RTJc

// 文字列のSliceの初期化
ss := []string{"foo", "bar", "baz"}

// キーが文字列、値も文字列のmapの初期化
mp := map[string]string{
    "foo": "hoge", 
    "baz": "fuga",  // 改行する場合は最後の要素でも行末にカンマが必要
}

// いくつかのメンバーを持つ構造体Fooの初期化
foo := Foo{
  Hoge: "hogehoge",
  Fuga: true,
}

Go言語の型

Go言語の変数は必ず型を持っています。変数の型によって格納できる値が違いますし、用途も違います。

真偽値型

真偽値型(bool)は、真(true)と偽(false)のいずれかを値として持つ二値の型です。主として条件分岐に用いられます。

数値型

数値型はその名の通り、数値を格納する変数型で、大きく分けて三つあります。一つは整数(Integer)で、符号付きのものと符号なしのものがあります。通常はint型がよく使われ、実行系によりますが、-2147483648〜2147483647または-9223372036854775808〜9223372036854775807の範囲で整数を表現することができます。実行系によらず64bitの値を使用したい場合はint64を使用します。ここでは説明を省きますが、符号なしのものは正の値に範囲を絞ることで約二倍の範囲を表現することができます。int8(8bit)や、int64(64bit)といった、サイズを指定した型を使用することも可能です。

もう一つは浮動小数点数型で、その名の通り小数点を含む実数を表現することができる型です。浮動小数点数型は32bit(float32)のものと64bit(float64)のものがあり、float64の方がよく使用されているようです。

最後は複素数型で、使用されることは多くないと思いますので、ここでは説明を省きます。

また、uint8(8bit符号なし整数)の別名としてbyteが、int32(32ビット符号あり整数)の別名としてruneが用意されています。byteは名前からわかるようにbyte値を、runeはちょっとわかりにくいのですが、Unicodeのコードポイントを表現するための値です。

文字列型

文字列型は文字列を格納する型です。言語により実装が大きく違うこともあるのが文字列の型ですが、Go言語では文字列型は、UTF-8などの文字列エンコードは考慮せず単なるbyteの配列として表現されます(Go言語のソースコード自体はUTF-8で書くことになっていますので、プログラム中でリテラルの形で宣言した文字列はUTF-8のbyte列が格納されることになります)。そのため、日本語のようにマルチバイト文字を扱う場合には一部直感的ではない挙動になる場合がありますので注意が必要です。

文字列の長さ: len()関数

文字列の長さ(byte数)を取得するには、組み込み関数であるlen()関数が使用できます。play

// https://play.golang.org/p/CwOQ6KZkjKM

s := "foobar"
fmt.Println(len(s))
// Output:
//  6

前述の通り、文字列は単なるbyteの列で、len()関数はbyte数を返しますので、日本語が含まれる場合は「文字の数」ではない値が返ってきますので注意しましょう。play

// https://play.golang.org/p/Di7sbMSlKDp

s := "こんにちは"
fmt.Println(len(s))
// Output:
//  15

(Unicode文字列の)文字数を取得したい場合、いったんruneのSliceに直してから長さをとります。play

// https://play.golang.org/p/YtmDa4FGsrb

s := "こんにちは"
fmt.Println(len([]rune(s)))
// Output:
//  5

文字列に関する標準パッケージ

Go言語の標準パッケージには文字列を取り扱うものが多くあります。代表的なモノとして次の様なものがあります。

  • bytes/strings
    • 文字列の分割や結合など、標準的な文字列処理をひとまとめにしたパッケージです。bytesstringsはそれぞれ[]byte型とstring型を対象としており、おおよそ同じ関数が用意されています。
  • strconv
    • 文字列と他の型(数値や真偽値)とを変換するための関数をまとめたパッケージです。
  • unicode
    • Unicodeのコードポイントを取り扱うためのパッケージです。特にその文字がどういった文字であるかを判別するIsXXXという形の関数が充実しています。
  • regexp
    • 正規表現を取り扱うためのパッケージです。

配列/Slice

配列はある特定の型(この型は何でもよいです)のリストを保持する型です。配列に格納された値それぞれのことを要素と呼びます。最初に要素の数(長さともいいます)を指定して宣言します。

var arrStr [2]string // 文字列を二個格納できる配列
var arrInt [10]int   // 整数を十個格納できる配列

配列は要素番号(これを「添え字」と呼びます)を使用することで要素にアクセスできます。添え字は最初の要素が0で、その後1, 2, 3...と続きます。play

// https://play.golang.org/p/Fy5THz9O49F

arr := [5]string{"foo", "bar", "baz", "qux", "quux"}
fmt.Println(arr[3])
// Output:
//   qux

:(コロン)を使って範囲を表現することもできます。この時得られるのがSliceです。Sliceは配列の一部を表現する型です。可変長の配列として扱われることも多く、配列より目にする機会が多いでしょう。[開始番号:終了番号]とした時、開始番号で得られる要素は含まれますが、終了番号で得られる要素は含まれないことに注意が必要です。play

// https://play.golang.org/p/ggIKj3ryRhp

arr := [5]string{"foo", "bar", "baz", "qux", "quux"}
fmt.Println(arr[2:4])
// Output:
//   [baz qux]

配列の宣言と同様に、要素数を省略するとSliceを宣言することができます。

var sliceStr []string
var sliceInt []int

要素の追加: append()関数

配列は長さが変更できないため、要素の追加をすることはできないのですが、Sliceではできます。その際には、組み込み関数であるappend()を使用します。append()は第一引数に任意のSlice、第二引数に追加したい要素を取り、Sliceに要素を追加した新しいSliceを返します。元のSlice(第一引数に与えられたもの)は変更されません。元の配列に追加したい場合には次の様にします。play

// https://play.golang.org/p/hocpa8Y4YiU

ss := []string{"foo", "bar"}
ss = append(ss, "baz")

配列/Sliceの長さ: len()関数とcap()関数

配列やSliceには、容量(Capacity)と長さという、二つの「長さ」が存在します。 詳細な説明は省きますが、容量はcap()関数で、長さはlen()関数で取得することができます。 配列の場合は容量と長さは同じ値です。

Map

Mapは配列のようにいくつかの値をまとめた型です。配列とは違う点として、添え字に任意の型が使用できる点、順序を持たない点が上げられます。他の言語では「連想配列」「HashMap」「辞書(dict)」などと呼ばれているものと同様のものです。宣言時には添え字に使う値(キー)と、キーに対応する値を:(コロン)で区切りながら宣言します。play

// https://play.golang.org/p/GmG84oktVZU

age := map[string]int{
  "Alice": 20,
  "Bob": 18,
  "Charlie": 22,
}
fmt.Println(age["Charlie"])
// Output:
//   22

存在しないキーを指定した場合はゼロ値が返ります。単に値を得る場合は第一返値のみで使用できますが、第二返値を受け取ることもできます。第二返値として要求したキーが存在するかどうかが返されます。play

// https://play.golang.org/p/pGjlQu5ms0B

age := map[string]int{
  "Alice": 20,
}
_, ok1 := age["Alice"]  // ok1 == true
_, ok2 := age["Bob"]    // ok2 == false

要素の追加は代入で行います。play

// https://play.golang.org/p/CnHUgJROIYI

age := map[string]int{}
age["Dave"] = 30
fmt.Println(age["Dave"])
// Output:
//   30

要素の削除: delete()関数

要素の削除は組み込み関数であるdelete()を使用します。第一引数にMap自体を、第二引数に削除したいキーを与えます。play

// https://play.golang.org/p/PAFfyIuq4v8

age := map[string]int{
  "Alice": 20,
  "Bob": 18,
}
a1, ok1 := age["Alice"]  // ok1 == true
fmt.Println(a1)
delete(age, "Alice")
a2, ok2 := age["Alice"]  // ok2 == false
fmt.Println(a2)
// Output:
//   20
//   0

Function

これまでにもいくつかの組み込み関数を紹介しましたが、ある処理をひとまとめにしたものを「関数」と呼びます。関数は自分で定義することもでき、次のように定義します。

func 関数名(引数) 返値の型 {
  処理内容
}

関数名は変数名と同様、大文字で始まる場合はパッケージ外に公開されます。引数は変数定義と同様に、変数名 型のセットをカンマ区切りで指定します。 Go言語では関数は返値を複数返すことができます。一つしか返さない場合は括弧でくくらず、型をそのまま記述します。 処理中で返値を返すときは return文を使用します。

例として、足し算を行う関数を見てみましょう。play

// https://play.golang.org/p/Ux5wsnjGucT

func Add(a int, b int) int {
  return a + b
}

この関数はint型の引数を二つ取り、int型の値を一つ返します。このように、同じ型の引数が連続する場合、型宣言をまとめることもできます。 次の関数は先ほどのものと全く等価です。play

// https://play.golang.org/p/07rmFc6BS7j

func Add(a, b int) int {
  return a + b
}

返値を複数返す場合は、返値を括弧でくくり、カンマ区切りで記述します。このとき、エラーを返したい時には、エラーを 最後の返値にする のが慣例です。 (エラーではない場合は好きな順番で良い)play

// https://play.golang.org/p/OcTk6d5WQZ3

func AddString(a, b string) (int, error) {
  // strconv.Atoi()は文字列をintに変換する関数。
  // 与えられた文字列が数値ではない場合、エラーを返し、そうでなければ第二返値はnilを返す。
  ai, err := strconv.Atoi(a)
  if err != nil { // 後述のif文でエラーチェックをする
    return 0, err
  }
  bi, err := strconv.Atoi(b)
  if err != nil {
    return 0, err
  }
  return ai + bi, nil
}

Struct

構造体(Struct)は、複数のデータをまとめて一つの塊にしたものです。それぞれのデータはフィールドと呼ばれ、名前と型を持ちます。 例えば、一人の人間(ここでは名前と年齢をフィールドとして持っていることとします)を表現する構造体は次のように定義します。

type Person struct {
  Name string
  Age int
}

構造体は複合リテラルを使って初期化することができます。また、それぞれのフィールドへは.(ドット)を使ってアクセスすることができます。play

// https://play.golang.org/p/fUVDYNC3nP7

alice := Person{
  Name: "Alice",
  Age:  20,
}
fmt.Println(alice.Age)
// Output:
//   20

フィールドの名前を小文字で始めた場合は、普通の変数等と同様に、パッケージの外からは直接アクセスできなくなります。

フィールドに加えて、構造体はメソッドと呼ばれる関数を持つことができます。メソッドはフィールドと同様、.(ドット)を使って呼び出すことができます。 メソッドの定義は、関数定義とほとんど同じで、関数名の前にレシーバーと呼ばれる、構造体の受け取り情報を記述します。play

// https://play.golang.org/p/J-EzWvR9bXh

func (p Person) IsYoung() bool {
  return p.Age < 30
}

alice := Person{
  Name: "Alice",
  Age: 20,
}
fmt.Print(alice.IsYoung())
// Output:
//   true

レシーバーとしては、構造体の値ではなく、ポインタを指定することができます。ポインタを指定した時に限り、メソッド内でレシーバーの状態を変化させることができます。play

// https://play.golang.org/p/CB5K2-qdSpW

type Car struct {
  IsRunning bool
}

func (car Car) RunWithValue() {
  car.IsRunning = true // メソッド外では影響がない
}

func (car *Car) RunWithPointer() {
  car.IsRunning = true // 元の値を書き換える
}

myCar := Car{} // 値を与えないと初期値(IsRunning = false)となる
myCar.RunWithValue()
fmt.Println(myCar.IsRunning)
myCar.RunWithPointer()
fmt.Println(myCar.IsRunning)
// Output:
//   false
//   true

Pointer

ポインタはあるデータ(すべての型が対象です)の実体(メモリ上でのアドレス)を指す型です。「文字列のポインタ」や「intのポインタ」など、「xxxのポインタ」という形で呼びます。

ポインタをとりたいある型をTとすると、Tのポインタは*Tと書きます。

var pi *int  // intのポインタ
var si *string  // stringのポインタ

type Person struct { /* ... */ }

var pp *Person  // Personのポインタ

また、ある変数やある値のポインタを取得するためには、&演算子を使用します。play

// https://play.golang.org/p/2DW-pYgMF_Y

i := 10
fmt.Println(&i)  // iのポインタ(アドレス)を表示

逆に、ポインタから値を得たい場合は*演算子を使用します。play

// https://play.golang.org/p/Cn47DrOwLfy

var sp *string
s := "hello"
sp = &s
fmt.Println(*sp)
// Output:
//   hello

ポインタ型を定義するときも*を使用し、ポインタから値をとるときも*を使用するので、混乱しがちなので頑張りましょう。

interface

interfaceはメソッドの組を定義した型です。Javaの様に構造体の定義時に実装するinterfaceを記述する必要はなく、interfaceで定義されたメソッドを充足している構造体はそのinterfaceを実装しているとして取り扱うことができます。例えば、io.Reader interfaceは次の様に定義されています。

type Reader interface {
  Read([]byte) (int, error)
}

*bytes.Buffer構造体や*os.File構造体はこれを満たしているため、io.Readerを要求する関数などに対して*bytes.Buffer*os.Fileを使用することができます。また、自分でこれを満たす様な構造体を実装することもできます。io.Writerを簡単に実装してみましょう。play

// https://play.golang.org/p/Yvf0E8T2Lz1

// io.Writerは次の様に定義されている
// type Writer interface {
// 	Write([]byte) (int, error)
// }

type myWriter struct {}  // 特にio.Writerを実装していることを明示しているわけではない

func (w myWriter) Write(b []byte) (int, error) {
	fmt.Println(string(b))
	return len(b), nil
}

func needWriter(w io.Writer) {
	w.Write([]byte("Hello"))
}

mw := myWriter{}
needWriter(mw)
// Output:
//   Hello

Channel

Go言語の大きな特徴として並行処理が簡単に実装できることが挙げられます。Go言語では[goroutine]を用いることで並行処理を実装します。Channelは並行処理実装の上で、複数のgoroutine間での値の送受信を行うための型です。CSを勉強した方には、「Queueの様なモノ」と説明すればわかりやすいでしょうか。送受信したい型をTとすると、Channelの型はchan Tと表現します。初期化されていないChannelはnilで、そのままだと使用できませんので、組み込み関数make()でChannelを作成します。

ch := make(chan int)

Channelは「送る(Send)」と「受け取る(Receive)」の二つの操作をすることができます。これらの操作には<-演算子を使用します。Channel変数の左側に<-を書いた場合は受け取り、右側に書いたときは送信です。play

// https://play.golang.org/p/HHZD7xKjtnE

ch := make(chan int)

go func() {
	ch <- 10
}()

fmt.Println(<-ch)
// Output:
//   10

送信側は値が受け取られるまで、受信側は値が来るまで、ブロック(待機)します。そのため、受け取り側しかない、同じgoroutineで送信と受け取りの両方を行おうとしている、などの場合、deadlockすることもあるので注意しましょう。play

// https://play.golang.org/p/qeRi4FuenwC

ch := make(chan int)
ch <- 10

Channelにはバッファを用意することもできますが、ほとんどの用途ではバッファを使用しないことの方が多いでしょう。

Channelを閉じる: close()関数

送信側から、「これ以上値を送りませんよ」ということを伝えるため、Channelは「閉じる」ことができます。その際に使用するのが組み込み関数であるclose()です。受信側がChannelに対してfor-range(後述)を使用して読み込みしている場合などには、Channelきちんと閉じることで適切に処理を続行することができます。play

// https://play.golang.org/p/qjsB8MK9QrG

ch := make(chan int)
go func() {
	ch <- 10
	ch <- 20
	ch <-30
	close(ch)
}()

for v := range ch {
	fmt.Println(v)
}
fmt.Println("finished")

一方、閉じた後のChannelに対して値を送信しようとしたり、閉じたChannelをさらに閉じようとするとプログラムが異常終了しますので、注意が必要です。なお、閉じた後のChannelから読み出しをした場合はブロックせずにゼロ値が返ります。play

// https://play.golang.org/p/0KS_K0yUmvh

ch := make(chan int)
close(ch)
fmt.Println(<-ch)
// Output:
//   0

Go言語の制御構文

if/else

何らかの条件によって処理を変えたい場合、if文を使用します。書き方は次の通りです。

if [初期化文; ]条件式  {
  // 処理
}

条件式は真偽値を返すようななにかを書きます。例えば、真偽値を返すような関数呼び出しでもいいですし、 a == bのような等号・不等号を使用した比較式でもかまいません。 初期化文は他の言語ではあまり見られない記法ですが、条件判定の前に何かの前処理を行うことができます。 何らかのエラーを返すような関数呼び出しを初期化文で行い、そのエラーチェックを条件式として記述する、という様な形で使用します。Foo()func() errorとすると、

err := Foo()
if err != nil {
  // error handling
}

を、

if err := Foo(); err != nil {
  // error handling
}

のように書くことができ、非常に見た目がすっきりします。条件式がfalseだった場合の処理も書きたい場合、else節を続けることができます。

if [初期化文; ]条件式 {
  // 処理
} else {
  // 処理
}

複数の条件式すべてを満たす時に処理をしたい場合(AND)には、演算子&&を、いずれかの条件を満たしているときに処理をしたい場合(OR)には、演算子||を使用します。play

// https://play.golang.org/p/_iJta1Dg4CM

if foo == 1 && bar == 2 {
  // 変数fooが1で変数barが2の時に実行される
  fmt.Println("foo is 1 and bar is 2")
}

if baz < 100 || 200 < baz {
  // 変数bazが100未満か200より大きいときに実行される
  fmt.Println("baz is under 100 or over 200")
}

また、複数の条件式で分岐したい場合には、ifとelseの間にelse if節を追加することで実現できます。

if [初期化文; ]条件式 {
  // 処理
} else if 条件式 {
  // 処理
  // else ifはいくつでも
} else { // else節は省略可能
  // 処理
}

条件は上から順番に評価されますので、上のほうにあるifまたはif elseで条件が満たされた場合、下の方にあるif elseで条件が合うものがあったとしても実行されませんので、注意しましょう。

switch/case

switch/case文は主に、ある変数の値によって条件を変えたいときに使用します。

switch [初期化文; ][] {
case [, ]...:
  // 処理
  // caseはいくつでも
default: // defaultは省略可能
  // どの条件に当てはまらなかったときの処理
}

switch/case文は上から順番に式を評価していき、最初にswitchの式 == caseの式trueのものを実行します。play

// https://play.golang.org/p/GbOtt0GibQs
switch version {
case "1.0.0":
  // version == "1.0.0"の時の処理
  fmt.Println("version 1.0.0")
case "2.0.0":
  // version == "2.0.0"の時の処理
  fmt.Println("version 2.0.0")
}

caseの式には複数の値をカンマ区切りで指定することができます。play

// https://play.golang.org/p/SvoGCz1O6Ii

switch userType {
case "admin", "manager":
  // userTypeが"admin"か"manager"の時の処理
  fmt.Println("you're super user")
case "user":
  // userTypeが"user"の時の処理
  fmt.Println("you're regular user")
}

switchの式が省略された場合はswitchの式 = trueとして扱われます。つまり、

switch true {
  // ...
}

switch {
  // ...
}

は等しいということです。これを利用して、if-if else文を次のように書き換えることができます。

if foo == bar {
  // ...
} else if baz == qux {
  // ...
} else if quux == corge {
  // ...
}
switch {
case foo == bar:
  // ...
case baz == qux:
  // ...
case quux == corge:
  // ...
}

複数のif-if eleseをつなげるより、こちらの方が簡潔でかつ読みやすく記述できる場合があります。

for

何らかの処理を繰り返し実行したい時には、for文を使用します。他の言語ではfor文の他にwhile文やdo-while文といった文が用意されていることもありますが、Go言語ではすべてfor文で行います。 代わりに、Go言語のfor文には三つの書き方があります。

一つ目はいわゆる最も一般的なfor文です。forの後に初期化文、条件式、再初期化文を;(セミコロン)で区切って記述します。

for [初期化文]; [条件式]; [再初期化文] {
  // 処理
}

実行の流れは次のようになっています。

  1. 初期化文が実行される。通常は変数定義・代入などを行う。
  2. 条件式が評価される。初期化文で初期化した変数を使用することが多い。
  3. 2.の評価結果がtrueなら処理を行う。falseならfor文を抜ける。
  4. 処理終了後、再初期化分を実行する。通常は初期化文で初期化した変数に再代入するなどして状況を更新する。
  5. 2.にもどる

例として次のプログラムは1から10までの数字を表示します。play

// https://play.golang.org/p/hv-JoJMyt8z

for i := 0; i < 10; i++ {
  fmt.Println(i + 1)
}
// Output:
//   1
//   2
//   3
//   4
//   5
//   6
//   7
//   8
//   9
//   10

初期化文、条件式、再初期化文はいずれも省略することができます。

二つ目の書き方は、条件式のみを書く書き方です。 これは他の言語で言うところのwhile文に相当します。

for [条件式] {
  // 処理
}

まず条件式が評価され、結果がtrueならば処理を行い、再度条件式を評価し・・・というのを、条件式の結果がfalseとなるまで繰り返します。次の例は先ほどと同様、1から10まで表示します。

// https://play.golang.org/p/xEryTwzfpWp

i := 0
for i < 10 {
	fmt.Println(i + 1)
	i++
}
// Output:
//   1
//   2
//   3
//   4
//   5
//   6
//   7
//   8
//   9
//   10

for-range構文

for-range構文は配列、Slice、文字列、Mapの要素や、Channelから受け取る各要素に対して繰り返し処理をするための構文です。他の言語では「拡張for」や「for each」などに相当する構文です。

for [変数リスト [:]=] range コレクション {
  // 処理
}

ループ一回あたり、対象によって一つか二つの値を受け取ることができます。二つの値を受け取る対象の場合、二つ目を使用しない時には省略することもできます。

arr := []string{"foo", "bar", "baz"}

// これを
for i, _ := range arr {
  // 処理
}

// こう書くことができる
for i := range arr {
  // 処理
}

配列/Sliceに対してfor-rangeを使用する場合、値は二つ返ってきます。一つ目は配列/Sliceの添え字(int)で、二つ目はそのインデックスで得られる値です。play

// https://play.golang.org/p/_fkQhEMzEfc

arr := []string{"foo", "bar", "baz"}
for i, elem := range arr {
  fmt.Println(i, elem)
}
// Output:
//   0 foo
//   1 bar
//   2 baz

文字列は[]byteとほぼ等しいですが、[]byteに対してfor-rangeを使用すると二つ目の値としてbyteが得られる(つまり、文字単位のループにはならない)一方、文字列に対してfor-rangeを使用すると二つ目の値としてruneが得られます。これはUTF-8の文字単位でループすることが可能と言うことです。ただし、一つ目の値はバイト数なので注意してください。play

// https://play.golang.org/p/uBBa_qV40q4

s := "こんにちは"
for i, elem := range s {
	fmt.Printf("%d %c\n",i, elem)  // %dは10進数の整数を、%cは文字(rune)を表示するためのフォーマット
}
// Output:
//   0 こ
//   3 ん
//   6 に
//   9 ち
//   12 は

Mapに対してfor-rangeを使用する場合、一つ目の値としてキーが、二つ目の値として対応する値が得られます。play

// https://play.golang.org/p/AIqb4C1Bc2a

m := map[string]string{
  "foo": "hoge",
  "bar": "fuga",
  "baz": "piyo",
}
for k, v := range m {
  fmt.Println(k, v)
}
// Output:
//   foo hoge
//   bar fuga
//   baz piyo

Channelに対してfor-rangeを使用した場合、値は一つで、Channelから送られてきた値が得られます。Channelがcloseされるまでループを抜けないため、注意が必要です。play

// https://play.golang.org/p/l9hzUXfxyaH

ch := chMaker()
for v := range ch {
  fmt.Println(v)
}

defer

deferを付けて関数の呼び出しをすると、その場では実行されず、それが呼ばれた関数の終了時(最後まで実行されたときか、returnが呼ばれたとき)に実行されます。ちょっとわかりにくいと思いますので、例を見てみましょう。

func 関数名() {
  // 処理1
  defer 後処理()
  // 処理2
}

上記のように書いた場合、実行順は

  1. 処理1
  2. 処理2
  3. 後処理()

となります。複数deferを使った場合は、下から順に実行されます。関数内の処理自体は上から下ですから、上から順に実行されていき、returnまたは関数の最後に到達したらまた上に戻っていく、という流れだと思えばいいでしょう。play

// https://play.golang.org/p/i1ckJXs1zBp

fmt.Println("first")
defer fmt.Println("fifth")
fmt.Println("second")
defer fmt.Println("fourth")
fmt.Println("third")

エラー処理

Go言語でのエラー処理方法は大きく分けて二つあります。

FizzBuzz

Goを用いたプログラミングの練習として、FizzBuzzを実装してみましょう。FizzBuzzはプログラミングの練習でよく使用される課題の一つで、次のようななものです。

  • 順番に数字を表示していく(入力した数字を対象とする場合もある)
  • 対象の数字が3の倍数なら Fizz を、対象の数字が5の倍数なら Buzz を、3と5両方の倍数なら FizzBuzz を、それ以外の数字なら数字をそのまま、表示する

1から20まで表示する例:

1
2
Fizz
4
Buzz
Fizz
7
8
Fizz
Buzz
11
Fizz
13
14
FizBuzz
16
17
Fizz
19
Buzz

今回は次の様な仕様で実装してみましょう。

  • 1からNまでについて数字を順に表示する
  • Nはコマンドライン引数からとる
  • 表示する数字が3の倍数なら、数字を表示する代わりに"Fizz"を表示する
  • 表示する数字が5の倍数なら、数字を表示する代わりに"Buzz"を表示する
  • 表示する数字が3の倍数で、かつ5の倍数でもあるなら、数字を表示する代わりに"FizzBuzz"を表示する

Hint:

  • コマンドライン引数はflag.Arg()またはflag.Args()を使用して得ることができます。
  • 文字列から数値への変換はstrconv.Atoi()を使用します。

追加課題

上記仕様でFizzBuzzが実装できた人は、次の仕様で実装してみましょう。

  • 1からNまでについて数字を順に表示する
  • Nはコマンドライン引数からとる
  • 表示する数字が3の倍数か、3を含む数字なら、数字を表示する代わりに"Fizz"を表示する

goroutineを用いた並行処理

Go言語の非常に大きな特徴として、言語の仕様レベルで並行処理をサポートしていることが挙げられます。他の言語では使うこと自体が少々面倒なことも多い並行処理が、Go言語ではとても簡単に並行処理を使ったプログラムを書くことができます。

goroutine

Go言語で並行処理をするには、goroutine(ごるーちん/ごーるーちん)を使用します。使用方法は至って簡単で、次のように関数呼び出しの前にgoと付けるだけです。

go 関数()

たったこれだけで、任意の関数を並行起動することができます。

注意しなければならないのは、 main関数が終了したらプログラムの実行自体が終わる、ということ です。goroutineがいくつ起動されていようとも、main関数の終了とともにすべてが終了します。そのため、通常は何らかの方法で(後述します)goroutineとの待ち合わせ(同期)を行います。次の例では、goroutineの中で文字列の出力が行われていますが、main関数のほうが先に終わってしまうため、"this is goroutine"が出力されません。play

// https://play.golang.org/p/dzGSP5Pbilb

fmt. Println("hello")
go func() {
    time.Sleep(5 * time.Second)  // 5秒休む: 長時間かかる処理の代わり
    fmt.Println("this is goroutine")
}()
fmt.Println("hi")

注意点として、今回紹介した様なコード片や、簡単なコマンドラインツール等ではあまり気にしなくても問題ありませんが、goroutineは leakします。HTTPサーバ等の継続的に動き続けるアプリケーションでは、goroutineを作成したら適切に終了させないと、自動で削除されることはありません。特に後述するようなfor文と組み合わせて使う場合や、ブロックし続けるような処理を含んでいる場合には、必ず適切な処理を行ってください(例えば、後述するcontext.Contextなどでキャンセルする、などが有効です)。

chan

前述の様に、goroutineはgo 関数()の形で呼び出します。これは(呼び出した関数が返値を持っていても)値を返さないため、何らかの値を得るためには返値ではない、別の方法を使用する必要があります。そのときに利用できるのが、Channelの節で紹介したChannelです。Channelはgoroutine間(main()が実行されているのも実はgoroutineの一つです)で値をやりとりするための仕組みです。

Channelは送信側/受信側ともに準備ができた状態になるまではブロック(処理が進まなくなる)します。そのため、Channelはgoroutine間の処理の同期に使用することができます。play

// https://play.golang.org/p/nPFfRivvkJr

ch := make(chan string)

go func() {
	time.Sleep(5 * time.Second)  // 5秒休む
	ch <- "Hello"
}()

s := <- ch  // chから値が送られてくるまで待つ
fmt.Println(s)

select/case

select文はswitch文に非常によく似ていますが、複数のChannelで一番最初に処理されたものを選択するための構文です。まずは例を見てみましょう。play

// https://play.golang.org/p/1ygDWPQ91Gt

ch := make(chan string)

go func() {
  time.Sleep(5 * time.Second) // 長い処理
  ch <- "Hello"
}()

select {
case s := <-ch:
  fmt.Println(s)
case <-time.After(3 * time.Second): // time.Afterはchan time.Timeを返し、指定した時間の後に値が送られてくる
  fmt.Println("3 seconds passed")
}

上記の例では、二つ目のcaseの方が先に受信成功するため、"3 seconds passed"がプリントされます。select文ではすべてのcaseは同時に待ち受けられます。

また、上記は例のためselect分単体で使用しましたが、多くのケースでは条件を指定していない(=無限ループする)for文と併せて利用されます。play

// https://play.golang.org/p/Iab77qnSTjy

done := make(chan struct{})
go func() {
  time.Sleep(3 * time.Second)
  done <- struct{}{}
  close(done)
}()

for {
  select {
    case <-time.After(1 * time.Second):
      fmt.Println("1 second passed")
    case <-done:
      fmt.Println("done!")
      return
    }
}

また、if文におけるelse処理のようにどのケースも当てはまらない場合にはdefaultというケースを作成することで、「その他」の処理を記述することもできます。

syncパッケージ

syncパッケージは非同期処理における同期処理の仕組みをいくつか提供している標準パッケージです。ここではよく使われるものをいくつか紹介します。

sync.WaitGroup

sync.WaitGroupはその名の通り、goroutineをグループ化して、そのすべてのgoroutineが終了するのを待つために使用する構造体です。特に初期化することなく使用可能です。WaitGroup.Add(1)でgoroutineの数を追加し、待ちたいgoroutineが処理終了する際にWaitGroup.Done()を呼びます。WaitGroup.Wait()Add()した回数Done()が呼ばれるまでブロックします。注意点として、goroutineは 呼び出したその場で処理が開始するとは限らない ため、Add()はgoroutineの外で呼び、Done()はgoroutineの中で呼ぶようにします。play

// https://play.golang.org/p/EfpiGlkLOvx

var wg sync.WaitGroup  // 初期化なしで使用できる
for _, name := range []string{"Alice", "Bob", "Chris"} {
  wg.Add(1)  // goroutineの前で呼ぶ
  
  go func(name string) {
    defer wg.Done()  // goroutine終了。deferが便利です
    fmt.Println("hello, " + name)
  }(name)
}

wg.Wait()  // すべてのgoroutineが終わるまで待つ
fmt.Println("all goroutine have finished")

sync.Mutex

sync.Mutexは排他制御のために使用される構造体です。Lock()Unlock()の二つのメソッドを持っています。Lock()を呼んだとき、他の場所ですでにLock()が呼ばれている場合、Unlock()が呼ばれるまでブロックします。そのため、sync.Mutexを使用することである変数などが複数のgoroutineから変更されることを防ぐことができます。ゼロ値のsync.Mutexはそのまま(特に初期化等することなく)使用することができます。play

// https://play.golang.org/p/jyDti7pQYLT

var wg sync.WaitGroup

// sync.Mutexなしのばあい
for i := 0; i < 3; i++ {
  wg.Add(1)
  go func() {
      fmt.Println("A")
      time.Sleep(1 * time.Millisecond)
      fmt.Println("B")
      time.Sleep(1 * time.Millisecond)
      fmt.Println("C")
      wg.Done()
  }()
}

wg.Wait()

fmt.Println("----")

// sync.Mutexありのばあい
var mu sync.Mutex  // 初期化なしで使える
for i := 0; i < 3; i++ {
  wg.Add(1)
  go func() {
    mu.Lock()
    defer mu.Unlock()
    fmt.Println("A")
    time.Sleep(1 * time.Millisecond)
    fmt.Println("B")
    time.Sleep(1 * time.Millisecond)
    fmt.Println("C")
    wg.Done()
  }()
}
wg.Wait()

context.Context

context.Contextは主としてgoroutineのキャンセル処理などに使用される構造体です。上位のgoroutineがキャンセルされた場合(例えば、signalを受けてアプリケーションを終了する、HTTPのリクエストがキャンセルされた、など)、下位のgoroutineにキャンセルを伝搬する機能も持っています。通常、context.Contextを得るには、context.Background()を使用します。そのほか、context.WithCancel()context.WithDeadline()context.WithTimeout()を使用することでキャンセル処理、タイムアウト処理を簡単に実装することができます。

コンテキストがキャンセルされた場合、Context.Done()で帰ってくるチャンネルが閉じられるため、select文でコンテキストの終了を検知することができます。次の例では、goroutineで1秒ごとにカウントアップし、10秒後にmain()からキャンセルします。play

// https://play.golang.org/p/T3L5QZkV4NB

ctx, cancel := context.WithCancel(context.Background())

go func(ctx context.Context) {
  var i int
  for {
    select {
    case <-ctx.Done():  // cancel()が呼ばれるとここに入る
    	fmt.Println("canceled")
	return
    case <-time.After(1 * time.Second):
        fmt.Println(i)
	i++
    }
  }
}(ctx)

time.Sleep(10 * time.Second)
cancel()

実際には上記のキャンセル処理はHTTPリクエストのキャンセルや、エラーが発生した時などに行われます。このようなケースではcontext.WithTimeout()を使用して実装する方がよいでしょう。play

// https://play.golang.org/p/IztyGR0PTPP

ctx, cancel := context.WithTimeout(context.Background(), 10 * time.Second)
defer cancel()

go func(ctx context.Context) {
  var i int
  for {
    select {
    case <-ctx.Done():  // タイムアウトでここに入る
      fmt.Println("timeout")
      return
    case <-time.After(1 * time.Second):
      fmt.Println(i)
      i++
    }
  }
}(ctx)

goroutineを用いたgenerator

比較的簡単な例として、goroutineを用いたgeneratorを作ってみましょう。次の様な仕様で実装してみてください。

  • New関数でChannelを得る
  • New関数の引数は自由
  • 得られたChannelからはintの値が1から順番に得られる
  • 任意のタイミングでgeneratorの利用を終了できる
  • 使用例(キャンセル処理を含んでいません):
generator := New(/* 引数は自由に決めてください */)

for i := range generator {
  fmt.Println(i)
}
// Output:
//   1
//   2
//   3
//        ...と永遠に表示され続ける

Hint:

  • close()した後のChannelに値を入れようとするとpanic(プログラムの異常終了)するので、注意しましょう
  • 任意のタイミングで終了 = goroutineをキャンセル

回答例

FizzBuzzと組み合わせる

  • NewFizzBuzzGenerator関数
  • 1, 2, 3, ...の代わりに1, 2, Fizz, ...

回答例

簡単なアプリケーションの実装

さて、いくつか端折ってきた部分もありますが、これで一通りGo言語の仕様については学んできました。ここで、簡単なWebアプリケーションを一つ実装してみましょう。

Go言語では、HTTPに関連した標準パッケージとしてnet/httpが用意されています。そのうち、今回のように簡単なWebアプリケーションの実装をするにあたり重要なものとして、次のものが挙げられます:

  • http.HandleFunc(): パスとhttp.HandlerFuncの組を渡して、httpパッケージのデフォルトHandlerにHTTPのエンドポイントを設定する関数です。
  • http.HandlerFunc: 次に説明するhttp.ResponseWriter*http.Requestを受け取り、処理をする関数です。http.HandleFuncで設定したパスにアクセスがあった場合に呼び出されます。
  • http.ResponseWriter: サーバがクライアントに対してレスポンスを返すための関数をまとめたinterfaceです。WriteHeader(int)でステータスコードを設定し、Write([]byte)でbodyをクライアントに送ります。
  • http.Request: サーバがクライアントから受けたリクエストに関する情報を含んだ構造体です。MethodやURL、HeaderやBodyなどが含まれます。
  • http.ListenAndServe(): 実際にサーバを実行し、第一引数で与えたアドレスを第二引数で与えたHandlerで待ち受けます。第二引数がnilなら、httpパッケージのデフォルトHandlerが使われます。
package main

import (
	"fmt"
	"net/http"
)
// Hello はhttp.HandlerFuncを実装している関数
func Hello(w http.ResponseWriter, r *http.Request) {
  w.WriteHeader(http.StatusOK)  // 200 OKを設定
  w.Write([]byte("Hello, world"))  // bodyとしてメッセージを送信
}

func main() {
  http.HandleFunc("/hello", Hello) // /hello というパスにリクエストが来たらHelloを呼ぶ

  if err := http.ListenAndServe(":8080", nil); err != nil {  // :8080で待ち受ける
    fmt.Println(err.Error())
  }
}

上記のコードを実行すると、:8080でHTTPサーバが待ち受けます。上記コードを実行したのとは別の端末を起動し、次の様に動作を確認してみましょう。

$ curl http://localhost:8080/hello

Hello, worldと表示されれば成功です!

いくつか案を用意しましたので、次の中から一つ選んで実装してみましょう(もちろん、お好みのネタで実装してみてもOKです)。

Website

  • 自分の自己紹介など、何らかのHTMLページを表示するようなサーバ
  • 適当なディレクトリにおいたファイルを返す

Hint: net/httpパッケージ内にもファイルをサーブするために使える関数がいろいろ用意されています。

簡易電卓

  • 数式を渡すと答えが返ってくる
  • POST /calc(bodyに数式を入れる)で計算をする

TODO List

  • TODOリストを管理するアプリケーション
  • GET /itemsでTODOの一覧を返す
  • POST /item/createでアイテムを追加する
  • DELETE /item(ヘッダにitem IDを入れる)でアイテムを削除する

Hint: 今回はデータベースの使用方法については学習していませんので、簡単のためMapを使用するとよいでしょう(database/sqlなどを使用して、SQLデータベースに値を格納してももちろんかまいません!)。

その他

参考

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment