※ この辺の知識が特別な説明なく登場します
- 構造体
- ポインタ
- 抽象データ型(interface)
- ジェネリクス
etc.
ざっくりとした概要
- 静的型付け言語
- クロスコンパイルが可能
- C と Java を足して2で割った感じ(個人的主観)
- プリプロセッサはない
- GCによるメモリ管理
- コンパイラはGo言語の公式コンパイラとGCC版がある(llgo というLLVMベースのものもあるらしい)
- 名前的型付け(≠構造的型付け)
- キャメルケースを使った命名
- 各命名における頭文字の大文字/小文字でスコープの制御を行う
- Goアセンブリというちょっと特殊なアセンブリを使うことが可能
- 構造体や配列などのオブジェクトはミュータブル
- 変数などの宣言は名前が先、型が後ろ
- 関数型スタイルのコードは書きづらい(最近は変わりつつもある)
- オブジェクトが存在しないことを示す値は
nil
Hello World
package main
import "fmt"
func main() {
fmt.Println("Hello, World!")
}
実行方法
※Go言語の公式コンパイラのサンプルです
- スクリプトスタイル
$ go run main.go
- コンパイルしてから実行
$ go build main.go
$ main
基本的ルール
- 必ずパッケージングする
package main
- エントリポイントは
main
パッケージのmain
関数(ファイル名はなんでもOK)
func main() {
}
- コメントの書き方はCやJavaと同じ
// 1行コメント
/*
複数行
コメント
*/
- 行末のセミコロンは不要(付けることも可能だが、付けないのがスタンダード)
fmt.Println("Hello, World!")
基礎文法
変数と定数
- 宣言と代入
var v int
v = 1
- 型推論
v := 1 // int型
- 定数
// 型の明示
const One int = 1
// untyped な書き方(型推論とはちょっと違うが、似たようなことができるという解釈でOK)
const Two = 2
-
iota
を活用した連番の定数定義
// 最初の一つに `iota` を指定すると、以降の定数には連番の値が生成されて割り当てられる
const (
Zero = iota // 0
One // 1
Two // 2
Three // 3
)
プリミティブ型
※代表的なものだけ書いています
// 整数
var i int
i = -10
// 符号なし整数
var u uint
u = 10
// 浮動小数
var f float64
f = 3.3
// 文字列
var s string
s = "foo"
// 真偽値
var b bool
b = true
// バイト
var b byte
b = 'a' // 97
リスト
配列
- Goの配列は固定長
var ary [3]int
ary[0] = 1
ary[1] = 2
ary[2] = 3 // ここまではOK
ary[3] = 4 // これはコンパイルエラー
スライス(可変長配列)
- いわゆる可変長配列を扱いたい場合はこっちを使う
var s []int
// append関数を使ってメモリを拡張しつつ要素を追加
s = append(s, 1)
s = append(s, 2) // ここまではOK
s[2] = 3 // ここで実行時エラーが発生する
-
make
関数を使ったアロケーション
s := make([]int, 3)
s[0] = 1
s[1] = 2
s[2] = 3 // ここまではOK
s[3] = 4 // ここで実行時エラーが発生する
- 可変長配列を初期化しつつ宣言
s := []int{1, 2, 3}
マップ(ハッシュ・辞書・連想配列・オブジェクト)
-
map
型を使う
m := make(map[string]int)
s["foo"] = 1
s["bar"] = 2
- 初期化しつつ宣言
m := map[string]int{
"foo": 1,
"bar": 2,
}
標準入出力
-
fmt
パッケージをimport
して使う
package main
import "fmt"
func main() {
var name string
fmt.Scan(&name)
fmt.Println("Hello, " + name + "!")
}
条件分岐
-
条件演算子
-
==
: Equal -
!=
: Not equal -
<
: Less than -
>
: Greater than -
<=
: Less than or equal to -
>=
: Greater than or equal to -
&&
: And -
||
: Or -
!
: Not
-
-
if
文
x := 10
if x > 5 {
fmt.Println("x is greater than 5")
} else {
fmt.Println("x is less than 5")
}
-
switch
文
i := 2
switch i {
case 1:
fmt.Println("1")
case 2:
fmt.Println("2")
default:
fmt.Println("default")
}
- 真/偽として評価可能な値は
true
かfalse
のみ(bool
型のみ)
// 文字列や数値、nil などを真偽値として評価することはできない
// コンパイルエラー
if 1 {
// ...
}
// コンパイルエラー
if !nil {
// ...
}
// コンパイルエラー
if "True" {
// ...
}
// ok
if true {
// ...
}
// ok
if !false {
// ...
}
繰り返し
Goでの繰り返し構文は for
文のみ。
ただし、Goのfor
文は柔軟性が高く、色々な書き方ができる。
- クラシックスタイル
for i := 0; i < 10; i++ {
fmt.Println(i)
}
- while 文的な書き方
j := 0
for j < 10 {
fmt.Println(j)
j++
}
- 無限ループ
k := 0
for {
fmt.Println(k)
k++
}
range
を使ったイテレーション
- 指定回数繰り返し
for range 3 {
fmt.Println("Hello")
}
// Hello
// Hello
// Hello
- for each 的な書き方
s := []int{2, 3, 5}
for i, v := range s {
fmt.Println(i, v)
}
// 0 2
// 1 3
// 2 5
関数
Goにはデフォルト引数は無い。
- 関数宣言
func addOne(n int) int {
return n + 1
}
- 無名関数(アロー関数のような記法は無い)
var square = func(x int) int {
return x * x
}
- 複数の戻り値を返すこともできる
func swap(x, y string) (string, string) {
return y, x
}
構造体
Goにはクラスは無い。
ただし、構造体とメソッドを活用することで、オブジェクト指向ライクな実装が可能。
- 型定義
type Point struct {
X int
Y int
}
- 初期化
p := Point{X: 10, Y: 20}
- メソッド
// 関数名の前にレシーバを書くと、その構造体のメソッドになる
// レシーバ変数は、慣習的に型名の頭文字を取ることが多く、self や this は使わない
func (p Point) Print() {
fmt.Printf(p.X, p.Y)
}
- メソッド呼び出し
p := Point{X: 10, Y: 20}
p.Print() // 10 20
- 破壊的メソッド(パラメータの上書き)
// レシーバをポインタ型にすると、パラメータを上書きすることができる
func (p *Point) Set(x, y int) {
p.X = x
p.Y = y
}
- コンストラクタ
// New〇〇 という命名を使って、ポインタ型を返すという慣習がある
func NewPoint(x, y int) *Point {
return &Point{X: x, Y: y}
}
- フィールド参照
p1 := Point{X: 1, Y: 2} // 非ポインタ型
fmt.Println(p1.X, p1.Y) // 1 2
p2 := NewPoint(3, 4) // ポインタ型
// C言語のようにアロー演算子などを使う必要は無い
fmt.Println(p2.X, p2.Y) // 3 4
エラーハンドリング(例外処理)
Goには Either モナドや Result 型、 try~catch 文などはない。
代わりに error
型を使い、明示的にハンドリングする。
error型
error
型はinterface(抽象型については後述)として定義されている。
// The error built-in interface type is the conventional interface for
// representing an error condition, with the nil value representing no error.
type error interface {
Error() string
}
新しいエラーの生成
-
errors
パッケージを活用する
import "errors"
func f() error {
return errors.New("example error")
}
-
fmt
パッケージを活用する
import "fmt"
func f() error {
return fmt.Errorf("example error")
}
エラーハンドリング例
func divide(a, b int) (int, error) {
if b == 0 {
return 0, errors.New("division by zero")
}
return a / b, nil
}
func main() {
result, err := divide(3, 0)
if err != nil {
panic(err) // スタックトレースを表示してプログラムを終了する
}
fmt.Println("3/0 =", result)
}
Defined Types
type
キーワードを使うことで、既存の型を元とした別の型を定義することができる。
Defined Types は元の方のエイリアスというわけではなく、明確に異なる型として区別される。
TypeScriptでいうところのBranded Typesみたいなもの。
- 定義
type MyInt int
-
MyInt
型 とint
型は明確に区別される
type MyInt int
func double(i MyInt) int {
return int(i * 2)
}
func main() {
var i int = 10
// これはコンパイルエラーになる
double(i) // cannot use i (variable of type int) as MyInt value in argument to double
myInt := MyInt(i)
doubled := double(myInt)
fmt.Println(doubled) // 20
}
- メソッド定義
func (i MyInt) Double() int {
return int(i * 2)
}
抽象型
Javaなどのように明示的な実装は行わず、インターフェースを満たしていればそれはインターフェースが実装されているものとして扱われる。
インターフェースには実装を期待するメソッドを書く。
// Greet() というメソッドを所持する値の抽象型
type Greeter interface {
Greet()
}
type Foo struct{}
func (f Foo) Greet() {
println("Hello!")
}
func main() {
var greeter Greeter
greeter = Foo{}
greeter.Greet() // Hello!
}
独自のエラー型
前述したように、 error
型はinterfaceとして定義されているため、独自のエラー型を定義することもできる
type MyError struct{
msg string
}
func (e MyError) Error() string {
return e.msg
}
func f() error {
return MyError{"my error"}
}
空のインターフェースと any
空のインターフェースは満たすべき条件が存在しない。
すなわち「全ての値を受け入れられる型」となる。
type I interface {}
var v1 I = 2
var v2 I = "foo"
この空のインターフェースには、エイリアスとしてany
が標準で定義されており、毎回 interface{}
と書かずとも、代わりに any
と書くことができる。
var v1 any = 2
var v2 any = "foo"
タイプアサーションと Type Switch
- タイプアサーション
func toInt(v any) int {
i, ok := v.(int)
if ok {
return i
}
return 0
}
- Type Switch
func printTypeAndValue(v any) {
switch tv := v.(type) {
case int:
fmt.Printf("type is an int, value: %d\n", tv)
case string:
fmt.Printf("type is a string, value: %s\n", tv)
case bool:
fmt.Printf("type is a bool, value: %t\n", tv)
default:
fmt.Printf("type is unknown value: %v\n", tv)
}
}
ジェネリクス
ジェネリクスは型と関数両方で扱うことができる。
命名の末尾に []
をつけると、その括弧内でタイプパラメータを定義することができる。
例えば、[T any]
は、 T
は全ての型を取り得るタイプパラメータとなる。
type List[T any] []T
func (l List[T]) Append(v T) List[T] {
return append(l, v)
}
func main() {
var intList List[int]
intList = intList.Append(1)
intList = intList.Append(2)
intList = intList.Append(3)
fmt.Println(intList)
var stringList List[string]
stringList = stringList.Append("a")
stringList = stringList.Append("b")
stringList = stringList.Append("c")
fmt.Println(stringList)
}
直和型
Goではタイプパラメータに限り、直和型を扱うことができる。
- 定義
type Number interface {
int | float32 | float64
}
- 使い方
func square[T Number](x T) T {
return x * x
}
非同期処理
goroutine と呼ばれる軽量なスレッドを活用した非同期処理実装することができる。
関数呼び出し時に、go キーワードを先頭につけることで、その処理が非同期実行される。
func printNumbers() {
for i := range 4 {
time.Sleep(500 * time.Millisecond)
fmt.Println(i)
}
}
func main() {
go printNumbers()
fmt.Println("Main function running")
time.Sleep(3 * time.Second)
fmt.Println("Main function finished")
}
出力例
Main function running
0
1
2
3
Main function finished
チャンネルを使ったスレッド間通信
チャンネル型という特殊な型を利用することで、goroutine間でデータの送受信を行うことができる。
func printNumbers(done chan bool) {
for i := 1; i <= 5; i++ {
fmt.Println(i)
time.Sleep(500 * time.Millisecond)
}
done <- true // 処理完了を通知
}
func main() {
done := make(chan bool)
go printNumbers(done)
// チャネルで完了通知を待機
<-done
fmt.Println("All goroutines finished")
}
出力例
1
2
3
4
5
All goroutines finished
ビット演算
-
演算子
-
|
: Or -
^
: Exclusive or -
&
: And -
<<
: Left shift -
>>
: Right shift
-
-
論理和演算
x, y := 2, 4
fmt.Println(x | y)
// 6
- 論理積演算
x, y := 6, 4
fmt.Println(x & y)
// 4
- 排他的論理和を使った暗号化
x, y := 6, 4
fmt.Println(x ^ y)
// 2
- 左ビットシフト
x, y := 4, 2
fmt.Println(x << y)
// 16
- 右ビットシフト
x, y := 4, 2
fmt.Println(x >> y)
// 1
文字列をXOR暗号化する例
文字列にインデックスでアクセスすると、byteを取り出すことができる。
byte
型は内部的には8bit符号なし整数型(uint8)なので、前述の例と同様にビット演算が可能。
※ golangの具体的な文字の取り扱いと、その表現方法などについては触れません
func xorEncryptDecrypt(input, key string) string {
output := make([]byte, len(input))
keyLen := len(key)
for i := 0; i < len(input); i++ {
output[i] = input[i] ^ key[i % keyLen]
}
return string(output)
}
func main() {
plaintext := "Hello, World!"
key := "mysecretkey"
// 暗号化
encrypted := xorEncryptDecrypt(plaintext, key)
fmt.Println("Encrypted:", encrypted)
// 復号化
decrypted := xorEncryptDecrypt(encrypted, key)
fmt.Println("Decrypted:", decrypted)
}
// Encrypted: %
// ^E# X
// Decrypted: Hello, World!