字節第三回青訓キャンプはバックエンド専用のセッションで、授業が始まり、楽しくノートを書きます。
授業では Go の基本文法について詳しく説明され、自分で Go 言語の聖典を読んだまとめも加え、この文章を書きました。JS や C/C++ との共通点が多いと感じています。
内容はGo 言語聖典および第三回青訓キャンプのコースからのものです。
コースのソースコードはwangkechun/go-by-exampleです。
Go 言語の紹介とインストール#
Go 言語とは#
- 高性能、高並行性
- 豊富な標準ライブラリ
- 完全なツールチェーン
- 静的リンク
- 高速コンパイル
- クロスプラットフォーム
- ガーベジコレクション
要するに、C/C++ の性能を兼ね備え、Python などの言語の簡潔さと充実した標準ライブラリを持っています。
インストール#
- https://go.dev/ にアクセスし、Download をクリックして、対応するプラットフォームのインストーラをダウンロードしてインストールします。
- 上記の URL にアクセスできない場合は、https://studygolang.com/dl にアクセスしてダウンロードしてください。
- GitHub のアクセス速度が遅い場合は、go mod proxy を設定することをお勧めします。https://goproxy.cn/ の説明を参考に設定すると、サードパーティの依存パッケージのダウンロード速度が大幅に向上します。
IDE の推奨#
- vscode に Go プラグインをインストール
- GoLand JetBrains シリーズの新しい IDE、dddd
GitHub を通じてこのコースのサンプルプロジェクトを簡単に体験できます Dashboard — Gitpod (素晴らしい、泣きそうです)
基本データ型#
整数型#
C++ と似ており、整数型は符号付きと符号なしのタイプに分かれています。符号付き整数は
- int8、int16、int32、int64
- それぞれ 8 ビット、16 ビット、32 ビット、64 ビットの符号付き整数
- uint8、uint16、uint32、uint64 は符号なし整数に対応します。
- さらに、特定の CPU プラットフォームのマシンワードサイズに対応する符号付きと符号なしの整数
int
とuint
もあります。int
は最も広く使用される数値型で、これらの 2 つの型は同じサイズ: 32 または 64 ビットです。- 異なるコンパイラは、同じハードウェアプラットフォーム上でも異なるサイズを生成する可能性があります。
- Unicode 文字
rune
型はint32
と等価な型で、通常はUnicode コードポイントを表すために使用されます。これらの 2 つの名前は互換的に使用できます。 byte
はuint8
型の等価型で、byte
型は一般に数値が生データであることを強調するために使用されます。uintptr
型は、具体的なビットサイズを指定せずにポインタを格納するのに十分なサイズです。これは、特に Go 言語と C 言語の関数ライブラリやオペレーティングシステムインターフェースが交差する場所で、低レベルのプログラミング時にのみ必要です。第 13 章の unsafe パッケージの関連部分で類似の例を見ることができます。
Printf
関数の %b
パラメータを使用してバイナリ形式の数字を印刷できます。%d
、%o
、または %x
パラメータを使用して出力の基数形式を制御します。この部分は C のフォーマット出力に似ています。
var x uint8 = 1<<1 | 1<<5
fmt.Printf("%08b\n", x) // "00100010", the set {1, 5}
o := 0666
fmt.Printf("%d %[1]o %#[1]o\n", o) // "438 666 0666"
x := int64(0xdeadbeef)
fmt.Printf("%d %[1]x %#[1]x %#[1]X\n", x)
// Output:
// 3735928559 deadbeef 0xdeadbeef 0XDEADBEEF
ascii := 'a'
unicode := '国'
newline := '\n'
fmt.Printf("%d %[1]c %[1]q\n", ascii) // "97 a 'a'"
fmt.Printf("%d %[1]c %[1]q\n", unicode) // "22269 国 '国'"
上記の例では、一般的に Printf フォーマット文字列に複数の%
パラメータが含まれている場合、対応する同じ数の追加オペランドが含まれますが、%
の後の[1]
副詞はPrintf
関数に最初のオペランドを再度使用するように指示します。
%
の後の#
副詞は、%o
、%x
、または%X
を使用して出力する際に0
、0x
、または0X
のプレフィックスを生成するようにPrintf
に指示します。- 文字は
%c
パラメータを使用して印刷するか、または%q
パラメータを使用してシングルクォート付きの文字を印刷します。
組み込みの len
関数は符号付きのint
を返し、以下の例のように逆順ループを処理できます。
medals := []string{"gold", "silver", "bronze"}
for i := len(medals) - 1; i >= 0; i-- {
fmt.Println(medals[i]) // "bronze", "silver", "gold"
}
浮動小数点数#
Go の浮動小数点型には float32
と float64
があります。
その範囲の限界値は math パッケージで見つけることができます。
- 定数
math.MaxFloat32
はfloat32
が表すことができる最大数値を示し、約3.4e38
です。対応するmath.MaxFloat64
定数は約1.8e308
です。これらが表すことができる最小値はそれぞれ約1.4e-45
と4.9e-324
です。 Printf
関数の%g
パラメータを使用して浮動小数点数を印刷すると、よりコンパクトな表現形式で印刷され、十分な精度が提供されますが、表形式のデータに対しては、%e
(指数付き)または%f
の形式で印刷する方が適切かもしれません。これらの 3 つの印刷形式はすべて印刷の幅を指定し、印刷精度を制御できます。
for x := 0; x < 8; x++ {
fmt.Printf("x = %d e^x = %8.3f\n", x, math.Exp(float64(x)))
}
// x = 0 e^x = 1.000
// x = 1 e^x = 2.718
// x = 2 e^x = 7.389
// x = 3 e^x = 20.086
// x = 4 e^x = 54.598
// x = 5 e^x = 148.413
// x = 6 e^x = 403.429
// x = 7 e^x = 1096.633
math パッケージは、多くの一般的な数学関数を提供するだけでなく、IEEE754 浮動小数点数標準で定義された特殊値の作成とテストも提供します:正の無限大と負の無限大Inf -Inf
は、それぞれ非常に大きなオーバーフローした数値とゼロ除算の結果を表します。また、NaN
(非数)は、一般に無効な除算操作の結果を表すために使用されます。例えば 0/0 や Sqrt (-1) などです。
var z float64
fmt.Println(z, -z, 1/z, -1/z, z/z) // "0 -0 +Inf -Inf NaN"
- Go の
NaN
は JS のものと似ており、どんな数とも等しくなく、自身とも等しくありません。math.IsNaN
を使用して数が非数NaN
であるかどうかをテストできます。
nan := math.NaN()
fmt.Println(nan == nan, nan < nan, nan > nan) // "false false false"
複素数#
Go 言語は、complex64
と complex128
の 2 種類の精度の複素数型を提供します。これらはそれぞれ float32
と float64
の 2 つの浮動小数点数精度に対応します。組み込みの complex
関数は複素数を構築するために使用され、組み込みの real
と imag
関数はそれぞれ複素数の実部と虚部を返します。
var x complex128 = complex(1, 2) // 1+2i
var y complex128 = complex(3, 4) // 3+4i
fmt.Println(x*y) // "(-5+10i)"
fmt.Println(real(x*y)) // "-5"
fmt.Println(imag(x*y)) // "10"
浮動小数点数リテラルまたは十進整数リテラルの後に i が付くと、例えば3.141592i
や2i
のように、複素数の虚部が形成され、複素数の実部は 0になります:
fmt.Println(1i * 1i) // "(-1+0i)", i^2 = -1
複素数定数は通常の数値定数に加算できます。
fmt.Println(1i * 1i) // "(-1+0i)", i^2 = -1
math/cmplx パッケージは、複素数処理のための多くの関数を提供します。例えば、複素数の平方根関数や累乗関数などです。
fmt.Println(cmplx.Sqrt(-1)) // "(0+1i)"
ブール型#
true
または false
、特に言うことはありません。
文字列#
Go の文字列型string
は 不変の文字列 で、JS と同様で、C++ とは異なります。
不変性は、2 つの文字列が同じ基底データを共有している場合でも安全であることを意味します。これにより、任意の長さの文字列をコピーするコストが低くなります。同様に、文字列 s と対応する部分文字列スライス s [7:] の操作も安全に同じメモリを共有できます。したがって、文字列スライス操作のコストも低くなります。この 2 つのケースでは、新しいメモリを割り当てる必要はありません。
文字列の第 i
バイトは、必ずしも文字列の第 i
文字であるとは限りません。非 ASCII 文字の UTF8 エンコーディングは 2 バイトまたはそれ以上を必要とするためです。
s[i:j]
は、元の s
文字列の第 i
バイトから第 j
バイト(j 自体を含まない)までの新しい文字列を生成します。生成された新しい文字列は j-i
バイトを含みます。
i
とj
は省略可能で、省略された場合は0
が開始位置として、len(s)
が終了位置として使用されます。
fmt.Println(s[0:5]) // "hello"
fmt.Println(s[:5]) // "hello"
fmt.Println(s[7:]) // "world"
fmt.Println(s[:]) // "hello, world"
+
演算子は 2 つの文字列を結合して新しい文字列を構築します:
fmt.Println("goodbye" + s[5:]) // "goodbye, world"
文字列の比較は、バイトごとに行われ、比較結果は文字列の自然なエンコーディング順序になります。
Go 言語のソースファイルは常に UTF8 でエンコードされ、Go 言語のテキスト文字列も UTF8 で処理されるため、Unicode コードポイントを文字列リテラルに書き込むことができます。
生の文字列リテラルの形式は、ダブルクォーテーションの代わりにバッククォートを使用します。
const GoUsage = `Go is a tool for managing Go source code.
Usage:
go command [arguments]
...`
生の文字列リテラル内では、エスケープ操作はありません。すべての内容は文字通りの意味を持ち、タブや改行を含むため、プログラム内の生の文字列リテラルは複数行にわたることができます。
- 生の文字列リテラル内では、バッククォートを直接書くことはできません。八進法または十六進法でエスケープするか、+"`" で文字列定数を結合することで実現できます。
- 唯一の特別な処理は、すべてのプラットフォームで値が同じであることを保証するために改行を削除することです。改行をテキストファイルに含めるシステムも含まれます。
Windows システムは、改行と行送りを一緒にテキストファイルに含めます。
以下は文字列メソッドのいくつかです。
package main
import (
"fmt"
"strings"
)
func main() {
a := "hello"
fmt.Println(strings.Contains(a, "ll")) // true
fmt.Println(strings.Count(a, "l")) // 2
fmt.Println(strings.HasPrefix(a, "he")) // true
fmt.Println(strings.HasSuffix(a, "llo")) // true
fmt.Println(strings.Index(a, "ll")) // 2
fmt.Println(strings.Join([]string{"he", "llo"}, "-")) // he-llo
fmt.Println(strings.Repeat(a, 2)) // hellohello
fmt.Println(strings.Replace(a, "e", "E", -1)) // hEllo
fmt.Println(strings.Split("a-b-c", "-")) // [a b c]
fmt.Println(strings.ToLower(a)) // hello
fmt.Println(strings.ToUpper(a)) // HELLO
fmt.Println(len(a)) // 5
b := "你好"
fmt.Println(len(b)) // 6
}
Go 言語では、
%v
を使用して任意の型の変数を簡単に印刷でき、数値文字列を区別する必要はありません。また、%+v
を使用して詳細な結果を印刷し、%#v
はさらに詳細です。
package main
import "fmt"
type point struct {
x, y int
}
func main() {
s := "hello"
n := 123
p := point{1, 2}
fmt.Println(s, n) // hello 123
fmt.Println(p) // {1 2}
fmt.Printf("s=%v\n", s) // s=hello
fmt.Printf("n=%v\n", n) // n=123
fmt.Printf("p=%v\n", p) // p={1 2}
fmt.Printf("p=%+v\n", p) // p={x:1 y:2}
fmt.Printf("p=%#v\n", p) // p=main.point{x:1, y:2}
f := 3.141592653
fmt.Println(f) // 3.141592653
fmt.Printf("%.2f\n", f) // 3.14
}
文字列と数字の変換#
Go 言語では、文字列と数字型の間の変換はすべて strconv
パッケージにあります。このパッケージは、string convert の略です。ParseInt
やParseFloat
を使用して文字列を解析できます。また、Atoi を使用して十進数の文字列を数字に変換できます。Itoa
を使用して数字を文字列に変換できます。
- 入力が不正な場合、これらの関数はすべてエラーを返しますが、Itoa を除きます。
package main
import (
"fmt"
"strconv"
)
func main() {
f, _ := strconv.ParseFloat("1.234", 64)
fmt.Println(f) // 1.234
n, _ := strconv.ParseInt("111", 10, 64)
fmt.Println(n) // 111
n, _ = strconv.ParseInt("0x1000", 0, 64)
fmt.Println(n) // 4096
n2, _ := strconv.Atoi("123")
fmt.Println(n2) // 123
n2, err := strconv.Atoi("AAA")
fmt.Println(n2, err) // 0 strconv.Atoi: parsing "AAA": invalid syntax
n3 := strconv.Itoa(123) // これはエラーを返しません
fmt.Println(n3) // 123
}
定数#
他の言語の定数と同様に、定数の値は変更できず、初期化される必要があります。定数を一括で宣言する場合、最初の定数以外の初期化式は省略可能で、省略した場合は前の定数の初期化式が使用されます。以下のようになります:
const pi = 3.14159 // approximately; math.Pi is a better approximation
const (
e = 2.71828182845904523536028747135266249775724709369995957496696763
pi = 3.14159265358979323846264338327950288419716939937510582097494459
)
const (
a = 1
b
c = 2
d
)
iota
定数生成器#
C/C++ の列挙型
Enum
に似ています!!
定数宣言では、iota
定数生成器を使用して初期化できます。これは、類似のルールで初期化された一連の定数を生成するために使用されますが、各行に初期化式を書く必要はありません。const
宣言文の最初の宣言された定数の行で、iota
は 0
に設定され、その後、各定数宣言の行で 1 ずつ増加します。
type Weekday int
const (
Sunday Weekday = iota
Monday
Tuesday
Wednesday
Thursday
Friday
Saturday
)
// Sunday は 0 に対応
// Monday は 1 に対応
// ....
// Saturday は 6 に対応
複雑な式と組み合わせて iota
を使用することもできます。以下の例では、各定数は式 1 << iota
に対応し、連続する 2 の累乗です。
type Flags uint
const (
FlagUp Flags = 1 << iota // is up
FlagBroadcast // supports broadcast access capability
FlagLoopback // is a loopback interface
FlagPointToPoint // belongs to a point-to-point link
FlagMulticast // supports multicast access capability
)
fmt.Println(FlagUp, FlagBroadcast, FlagLoopback, FlagPointToPoint, FlagMulticast)
// 1 2 4 8 16
無型定数#
多くの定数には明確な基礎型がありません。Go のコンパイラは、これらの明確な基礎型を持たない数値定数に対して、基礎型よりも高い精度の算術演算を提供します。少なくとも 256 ビットの演算精度を持つと考えることができます。ここには、無型のブール型、無型の整数、無型の文字、無型の浮動小数点数、無型の複素数、無型の文字列の 6 種類の未定義型の定数があります。
無型であるのは定数のみです。無型の定数が変数に割り当てられると、無型の定数は暗黙的に対応する型に変換されます。変換が合法である場合に限ります。
- 明示的な型の変数宣言がない場合(短い変数宣言を含む)、定数の形式は暗黙的に変数のデフォルト型を決定します。
- 無型整数定数は
int
に変換され、そのメモリサイズは不確定です。無型浮動小数点数と複素数定数は、メモリサイズが明確なfloat64
とcomplex128
に変換されます。
- 無型整数定数は
プログラム構造#
https://books.studygolang.com/gopl-zh/ch2/ch2.html
宣言と変数#
var#
一般的な構文は次のとおりです。
var 変数名 型 = 式
型を省略すると、式に基づいて自動的に推論されます。式が空の場合は、ゼロ値で変数が初期化されます(したがって、Go 言語には未初期化の変数は存在しません)。
型 | ゼロ値 |
---|---|
数値 | 0 |
ブール | false |
文字列 | "" |
配列や構造体などの集約型 | nil |
1 つの宣言文で複数の変数を同時に宣言することができます。また、一連の初期化式を使用して複数の変数を宣言し、初期化することもできます。
var i, j, k int // int int int
var b, f, s = true, 2.3, "hello" // bool float64 string
一連の変数は、関数を呼び出すことによって、関数が返す複数の戻り値で初期化することもできます:
var f, err = os.Open(name) // os.Openはファイルとエラーを返します
短い変数宣言 :=
#
名前 := 式
の形式で変数を宣言し、変数の型は式に基づいて自動的に推論されます。
- 簡潔で柔軟な特性のため、短い変数宣言はほとんどのローカル変数の宣言と初期化に広く使用されます。
- var 形式の宣言文は、型を明示的に指定する必要がある場所や、変数が後で再割り当てされるため初期値が重要でない場所でよく使用されます。
i := 100 // int
i, j := 0, 1 // int int
var boiling float64 = 100 // a float64
var names []string
var err error
- 短い変数宣言文は、同じレベルのレキシカルスコープで既に宣言された変数に対しては代入のみを行います。
- 変数が外部のレキシカルスコープで宣言されている場合、短い変数宣言文は現在のレキシカルスコープで新しい変数を再宣言します。
ポインタ#
C 言語と同様に、&
演算子を使用してアドレスを取得し、*
を使用して値を取得します。
x := 1
p := &x // pは型 *int で、xを指します
fmt.Println(*p) // "1"
*p = 2 // x = 2 と同等
fmt.Println(x) // "2"
任意の型のポインタのゼロ値は nil
です。
p
が有効な変数を指している場合、p != nil
のテストは真になります。- 2 つのポインタが同じ変数を指しているか、すべてが
nil
の場合にのみ等しいです。
new
関数#
new(T)
は、T
型の匿名変数を作成し、T
型のゼロ値で初期化し、その変数のアドレスを返します。返されるポインタ型は *T
です。
- Go 言語の
new
は、キーワードではなく関数です!したがって、再定義できます。
p := new(int) // pは*int型で、匿名のint変数を指します
fmt.Println(*p) // "0"
*p = 2 // int匿名変数の値を2に設定
fmt.Println(*p) // "2"
インクリメント / デクリメント演算#
インクリメント文 i++
は i
に 1 を加えます。これは i += 1
や i = i + 1
と等価です。対応するデクリメントは i--
で、i
から 1 を引きます。これらは文であり、C 系の他の言語のような式ではありません。
- したがって、
j = i++
は 無効であり、++
と--
は変数名の後ろにのみ置くことができます。したがって、--i
も無効です。
型 type
#
C++ の typeof の強化版に似ており、形式は次のとおりです。
type 型名 基礎型
以下のように、Celsius
と Fahrenheit
の 2 つの型を宣言し、それぞれ異なる温度単位に対応させます。
- 基礎データ型はその内部構造と表現方法を決定します。
- これらは同じ基礎型
float64
を持っていますが、異なるデータ型であるため、相互に比較したり、同じ式で混在させたりすることはできません。 - 型変換は値自体を変更することはありませんが、それらの意味を変えることになります。
import "fmt"
type Celsius float64 // 摂氏温度
type Fahrenheit float64 // 華氏温度
const (
AbsoluteZeroC Celsius = -273.15 // 絶対零度
FreezingC Celsius = 0 // 凍結点温度
BoilingC Celsius = 100 // 沸騰点温度
)
func CToF(c Celsius) Fahrenheit { return Fahrenheit(c*9/5 + 32) }
func FToC(f Fahrenheit) Celsius { return Celsius((f - 32) * 5 / 9) }
比較演算子 ==
と <
は、命名型の変数と他の同じ型の変数、または同じ基礎型の未命名型の値の間で比較するために使用できます。しかし、2 つの値が異なる型を持つ場合、直接比較することはできません:
var c Celsius
var f Fahrenheit
fmt.Println(c == 0) // "true"
fmt.Println(f >= 0) // "true"
fmt.Println(c == f) // compile error: type mismatch
fmt.Println(c == Celsius(f)) // "true"! 型変換操作は値を変更しません
命名型は、その型の値に新しい動作を定義することもできます。これらの動作は、その型に関連付けられた関数の集合として表され、これを型のメソッドセットと呼びます(第 6 章で詳しく説明します)。
以下の宣言文では、Celsius 型のパラメータ c が関数名の前に現れ、Celsius 型の値を持つString
というメソッドを宣言しています。このメソッドは、温度単位 °C を持つ文字列を返します:
func (c Celsius) String() string { return fmt.Sprintf("%g°C", c) }
多くの型はString
メソッドを定義します。これは、fmt パッケージの印刷メソッドを使用する際に、その型に対応するString
メソッドが返す結果が優先的に印刷されるためです。
c := FToC(212.0)
fmt.Println(c.String()) // "100°C"
fmt.Printf("%v\n", c) // "100°C"; Stringを明示的に呼び出す必要はありません
fmt.Printf("%s\n", c) // "100°C"
fmt.Println(c) // "100°C"
fmt.Printf("%g\n", c) // "100"; Stringを呼び出しません
fmt.Println(float64(c)) // "100"; Stringを呼び出しません
ループ for
#
Go のループには while や do while はなく、1 種類の for
ループのみがあります。
書き方は次の通りです:
for 初期化; 条件; 後処理 {
// 0個以上の文
}
for ループの 3 つの部分は括弧で囲む必要はありません。波括弧は必須で、左波括弧は後処理文と同じ行に置かなければなりません。
初期化
文は省略可能で、ループ開始前に実行されます。初期化
が存在する場合、単純文(simple statement)でなければなりません(短い変数宣言、自増文、代入文、または関数呼び出し)。条件
はブール式(boolean expression)であり、各ループの反復が始まる前にその値が計算されます。true
の場合、ループ本体の文が実行されます。後処理
文は、各ループ本体の実行が終了した後に実行され、その後再度条件
が評価されます。条件
の値がfalse
の場合、ループは終了します。
for ループのこれら 3 つの部分はすべて省略可能で、初期化
と 後処理
を省略すると、while ループになります。セミコロンも省略可能で、3 つの部分をすべて省略すると、無限ループになります。break
を使用してループを抜けることができます:
i := 1
for {
fmt.Println("loop")
break
}
for j := 7; j < 9; j++ {
fmt.Println(j)
}
for n := 0; n < 5; n++ {
if n%2 == 0 {
continue
}
fmt.Println(n)
}
for i <= 3 {
fmt.Println(i)
i = i + 1
}
分岐構造#
if else#
Go の if
は Python に似ており、括弧は不要ですが、後に波括弧が必要です。
if 7%2 == 0 {
fmt.Println("7 is even")
} else {
fmt.Println("7 is odd")
}
if 8%4 == 0 {
fmt.Println("8 is divisible by 4")
}
if num := 9; num < 0 {
fmt.Println(num, "is negative")
} else if num < 10 {
fmt.Println(num, "has 1 digit")
} else {
fmt.Println(num, "has multiple digits")
}
switch#
Go 言語の switch
分岐構造は C++ に似ていますが、多くの違いもあります:
- switch の後の変数名には括弧を付けないでください。
- C++ の switch case では
break
を加えないとすべての case を実行しますが、Go 言語では **break
を加える必要はありません **。 - Go 言語の switch はより強力で、任意の変数型を使用でき、任意の if else 文を置き換えることができます。
switch の後に何も加えず、case 内に条件分岐を書くことができます。これにより、複数の if else 文よりもコードの論理が明確になります。
package main
import (
"fmt"
"time"
)
func main() {
a := 2
switch a {
case 1:
fmt.Println("one")
case 2:
fmt.Println("two")
case 3:
fmt.Println("three")
case 4, 5:
fmt.Println("four or five")
default:
fmt.Println("other")
}
t := time.Now()
switch {
case t.Hour() < 12:
fmt.Println("It's before noon")
default:
fmt.Println("It's after noon")
}
}
プロセス情報#
Go では、os.argv
を使用してプログラム実行時に指定されたコマンドライン引数を取得できます。例えば、コンパイルされたバイナリファイルcommand
の後にabcd
を付けて起動すると、os.argv
は長さ 5 のslice
になり、最初のメンバーはバイナリ自身の名前を示します。so.getenv
を使用して環境変数を読み取ることができます。exec
package main
import (
"fmt"
"os"
"os/exec"
)
func main() {
// go run example/20-env/main.go a b c d
fmt.Println(os.Args) // [/var/folders/8p/n34xxfnx38dg8bv_x8l62t_m0000gn/T/go-build3406981276/b001/exe/main a b c d]
fmt.Println(os.Getenv("PATH")) // /usr/local/go/bin...
fmt.Println(os.Setenv("AA", "BB"))
buf, err := exec.Command("grep", "127.0.0.1", "/etc/hosts").CombinedOutput()
if err != nil {
panic(err)
}
fmt.Println(string(buf)) // 127.0.0.1 localhost
}
複合データ型#
配列#
配列は固定長の特定型要素のシーケンスです。配列はゼロ個または複数の要素を持つことができます。配列の長さが固定されているため、Go 言語では配列を直接使用することはほとんどありません。配列に対応する型はSlice
(スライス)で、これは成長と収縮が可能な動的シーケンスです。スライスの機能はより柔軟ですが、スライスの動作原理を理解するにはまず配列を理解する必要があります。
package main
import "fmt"
func main() {
var a [5]int
a[4] = 100
fmt.Println("get:", a[2])
fmt.Println("len:", len(a))
b := [5]int{1, 2, 3, 4, 5}
fmt.Println(b)
var twoD [2][3]int
for i := 0; i < 2; i++ {
for j := 0; j < 3; j++ {
twoD[i][j] = i + j
}
}
fmt.Println("2d: ", twoD)
}
スライス Slice
#
スライスは配列とは異なり、任意の長さに変更でき、より多くの操作が可能です。
make
を使用してスライスを作成し、配列のように値を取得できます。append
を使用して要素を追加します。注意:append の使い方は JS のconcat
に似ており、新しい配列を返し、append の結果を元の配列に割り当てます。- スライスの初期化時に動的に長さを指定することもできます。
len(s)
- スライスは Python のようなスライス操作を持っており、例えば
s[2:5]
は第 2 から第 5 の位置の要素を取得しますが、第 5 の要素は含まれません。ただし、Python とは異なり、ここでは負のインデックスはサポートされていません。
package main
import "fmt"
func main() {
s := make([]string, 3)
s[0] = "a"
s[1] = "b"
s[2] = "c"
fmt.Println("get:", s[2]) // c
fmt.Println("len:", len(s)) // 3
s = append(s, "d")
s = append(s, "e", "f")
fmt.Println(s) // [a b c d e f]
c := make([]string, len(s))
copy(c, s)
fmt.Println(c) // [a b c d e f]
fmt.Println(s[2:5]) // [c d e]
fmt.Println(s[:5]) // [a b c d e]
fmt.Println(s[2:]) // [c d e f]
good := []string{"g", "o", "o", "d"}
fmt.Println(good) // [g o o d]
}
マップ#
map
は実際の使用で最も頻繁に使用されるデータ構造です。
make
を使用して空のmap
を作成できます。ここでは 2 つの型、key
とvalue
の型が必要です。map[string]int
はkey
型がstring
、value
型がint
であることを示します。
map
の値の取得と挿入は、C++ の STL の map と同様に直接行えます。m[key]
m[key] = value
delete
を使用してキーと値のペアを削除できます。- Go の
map
は完全に無秩序であり、反復時にはアルファベット順や挿入順に出力されず、ランダム順序で出力されます。
package main
import "fmt"
func main() {
m := make(map[string]int)
m["one"] = 1
m["two"] = 2
fmt.Println(m) // map[one:1 two:2]
fmt.Println(len(m)) // 2
fmt.Println(m["one"]) // 1
fmt.Println(m["unknow"]) // 0
r, ok := m["unknow"]
fmt.Println(r, ok) // 0 false
delete(m, "one")
m2 := map[string]int{"one": 1, "two": 2}
var m3 = map[string]int{"one": 1, "two": 2}
fmt.Println(m2, m3)
}
range#
slice
や map
に対しては、range
を使用して迅速に反復処理できます。これにより、コードがより簡潔になります。range を使用して反復処理を行うと、配列の場合、最初の値はインデックス、2 番目の値は対応する位置の値になります。インデックスが不要な場合は、アンダースコア _
を使用して無視できます。
Go 言語では無駄なローカル変数(local variables)を使用することは許可されていないため、コンパイルエラーが発生します。
空識別子
(blank identifier)、つまり_
(アンダースコア)を使用します。空識別子は、変数名が必要だがプログラムロジックでは必要ない場合に使用され、ループ内で不要なループインデックスを破棄し、要素値を保持します。
package main
import "fmt"
func main() {
nums := []int{2, 3, 4}
sum := 0
for i, num := range nums {
sum += num
if num == 2 {
fmt.Println("index:", i, "num:", num) // index: 0 num: 2
}
}
fmt.Println(sum) // 9
m := map[string]string{"a": "A", "b": "B"}
for k, v := range m {
fmt.Println(k, v) // b 8; a A
}
for k := range m {
fmt.Println("key: ", k) // key: a; key: b
}
for _, v := range m {
fmt.Println("value:", v) // value: A; value: B
}
}
構造体#
構造体は、型付きフィールドの集合です。例えば、ここで user
構造体は 2 つのフィールド、name
と password
を含んでいます。
- 構造体の名前を使用して構造体変数を初期化し、各フィールドの初期値を渡す必要があります。
- また、キーと値のペアの形式で初期値を指定することもでき、これにより一部のフィールドのみを初期化できます。
- 同様の構造体はポインタもサポートしており、これにより構造体の直接変更が可能になり、特定の状況では大きな構造体のコピーコストを回避できます。
package main
import "fmt"
type user struct {
name string
password string
}
func main() {
a := user{name: "wang", password: "1024"}
b := user{"wang", "1024"}
c := user{name: "wang"}
c.password = "1024"
var d user
d.name = "wang"
d.password = "1024"
fmt.Println(a, b, c, d) // {wang 1024} {wang 1024} {wang 1024} {wang 1024}
fmt.Println(checkPassword(a, "haha")) // false
fmt.Println(checkPassword2(&a, "haha")) // false
}
func checkPassword(u user, password string) bool {
return u.password == password
}
func checkPassword2(u *user, password string) bool {
return u.password == password
}
JSON#
Go 言語の JSON 操作は非常に簡単です。
- 既存の構造体に対して、各フィールドの最初の文字が大文字であることを保証すれば、つまり公開フィールドであれば、この構造体は
JSON.marshaler
を使用してシリアライズされ、JSON 文字列に変換できます。
JSON.marshaler
はシリアライズされた値とエラーを返します。以下の例では、デフォルトでシリアライズされた文字列は大文字で始まります。後で json タグなどの構文を使用して、出力 JSON 結果内のフィールド名を変更できます。
- シリアライズされた文字列は、
JSON.unmarshaler
を使用して空の変数にデシリアライズできます。
package main
import (
"encoding/json"
"fmt"
)
type userInfo struct {
Name string
Age int `json:"age"`
Hobby []string
}
func main() {
a := userInfo{Name: "wang", Age: 18, Hobby: []string{"Golang", "TypeScript"}}
buf, err := json.Marshal(a)
if err != nil {
panic(err)
}
fmt.Println(buf) // [123 34 78 97...]
fmt.Println(string(buf)) // {"Name":"wang","age":18,"Hobby":["Golang","TypeScript"]}
buf, err = json.MarshalIndent(a, "", "\t")
if err != nil {
panic(err)
}
fmt.Println(string(buf))
var b userInfo
err = json.Unmarshal(buf, &b)
if err != nil {
panic(err)
}
fmt.Printf("%#v\n", b) // main.userInfo{Name:"wang", Age:18, Hobby:[]string{"Golang", "TypeScript"}}
}
時間処理#
Go 言語で最も一般的なのは time.now()
を使用して現在の時間を取得することです。また、time.date
を使用してタイムゾーン付きの時間を構築することもできます。これにより、この時間点の年、月、日、時、分を取得するための多くの方法があります。
Sub
メソッドを使用して 2 つの時間を減算し、時間の間隔を得ることができます。- 時間の間隔から、何時間、何分、何秒であるかを得ることができます。
- 特定のシステムと相互作用する際に、タイムスタンプを使用することがよくあります。
.UNIX
を使用してタイムスタンプを取得できます。time.format
time.parse
package main
import (
"fmt"
"time"
)
func main() {
now := time.Now()
fmt.Println(now) // 2022-05-07 13:12:03.7190528 +0800 CST m=+0.004990401
t := time.Date(2022, 5, 7, 13, 25, 36, 0, time.UTC)
t2 := time.Date(2022, 8, 12, 12, 30, 36, 0, time.UTC)
fmt.Println(t) // 2022-05-07 13:25:36 +0000 UTC
fmt.Println(t.Year(), t.Month(), t.Day(), t.Hour(), t.Minute()) // 2022 March 27 1 25
fmt.Println(t.Format("2006-01-02 15:04:05")) // 2022-05-07 13:25:36
diff := t2.Sub(t)
fmt.Println(diff) // 2327h5m0s
fmt.Println(diff.Minutes(), diff.Seconds()) // 139625 8.3775e+06
t3, err := time.Parse("2006-01-02 15:04:05", "2022-05-07 13:25:36")
if err != nil {
panic(err)
}
fmt.Println(t3 == t) // true
fmt.Println(now.Unix()) // 1651900531
}
関数#
Go は他の多くの言語とは異なり、関数の引数の変数型は後置です。Go では、関数は複数の値を返すことを原生的にサポートしています。
- 実際のビジネスロジックコードでは、ほぼすべての関数が 2 つの値を返します。最初の値は戻り値、2 番目の値はエラーメッセージです。以下の例の
exists
のように。
package main
import "fmt"
func add(a int, b int) int {
return a + b
}
func add2(a, b int) int {
return a + b
}
func exists(m map[string]string, k string) (v string, ok bool) {
v, ok = m[k]
return v, ok
}
func main() {
res := add(1, 2)
fmt.Println(res) // 3
v, ok := exists(map[string]string{"a": "A"}, "a")
fmt.Println(v, ok) // A True
}
エラーハンドリング#
Go のエラーハンドリングは、エラーメッセージを伝えるために別の戻り値を使用することです。
- 関数の戻り値の型の後に
error
を加えると、その関数がエラーを返す可能性があることを示します。したがって、関数の実装時には、return
で 2 つの値を同時に返す必要があります。 - エラーが発生した場合、
return nil
とエラーを返すことができます。エラーがない場合は、元の結果とnil
を返します。
package main
import (
"errors"
"fmt"
)
type user struct {
name string
password string
}
func findUser(users []user, name string) (v *user, err error) {
for _, u := range users {
if u.name == name {
return &u, nil
}
}
return nil, errors.New("not found")
}
func main() {
u, err := findUser([]user{{"wang", "1024"}}, "wang")
if err != nil {
fmt.Println(err)
return
}
fmt.Println(u.name) // wang
if u, err := findUser([]user{{"wang", "1024"}}, "li"); err != nil {
fmt.Println(err) // not found
return
} else {
fmt.Println(u.name)
}
}
ツールの推奨#
授業中に紹介されたコード生成ツールのいくつか
課後練習#
- 最初の例の謎解きゲームの最終コードを修正し、fmt.Scanf を使用してコードの実装を簡素化します。
package main
import (
"fmt"
"math/rand"
"time"
)
func main() {
maxNum := 100
rand.Seed(time.Now().UnixNano())
secretNumber := rand.Intn(maxNum)
// fmt.Println("The secret number is ", secretNumber)
fmt.Println("Please input your guess")
//reader := bufio.NewReader(os.Stdin)
for {
//input, err := reader.ReadString('\n')
var guess int
_, err := fmt.Scanf("%d", &guess)
fmt.Scanf("%*c") // 改行を食べる
if err != nil {
fmt.Println("An error occured while reading input. Please try again", err)
continue
}
//input = strings.TrimSuffix(input, "\n")
if err != nil {
fmt.Println("Invalid input. Please enter an integer value")
continue
}
fmt.Println("You guess is", guess)
if guess > secretNumber {
fmt.Println("Your guess is bigger than the secret number. Please try again")
} else if guess < secretNumber {
fmt.Println("Your guess is smaller than the secret number. Please try again")
} else {
fmt.Println("Correct, you Legend!")
break
}
}
}
- 2 番目の例のコマンドライン辞書の最終コードを修正し、別の翻訳エンジンのサポートを追加します。
- 前のステップの基礎の上に、コードを修正して 2 つの翻訳エンジンに並行リクエストを行い、応答速度を向上させます。
まとめと感想#
授業では Go の基本文法について詳しく説明され、自分で Go 言語の聖典を読んだまとめも加え、この文章を書きました。JS や C/C++ との共通点が多いと感じています。
内容はGo 言語聖典および第三回青訓キャンプのコースからのものです。