banner
cos

cos

愿热情永存,愿热爱不灭,愿生活无憾
github
tg_channel
bilibili

Go言語初上手(一) 環境設定と基本文法 | 青訓営

字節第三回青訓キャンプはバックエンド専用のセッションで、授業が始まり、楽しくノートを書きます。
授業では Go の基本文法について詳しく説明され、自分で Go 言語の聖典を読んだまとめも加え、この文章を書きました。JS や C/C++ との共通点が多いと感じています。

内容はGo 言語聖典および第三回青訓キャンプのコースからのものです。
コースのソースコードはwangkechun/go-by-exampleです。

Go 言語の紹介とインストール#

Go 言語とは#

  • 高性能、高並行性
  • 豊富な標準ライブラリ
  • 完全なツールチェーン
  • 静的リンク
  • 高速コンパイル
  • クロスプラットフォーム
  • ガーベジコレクション

要するに、C/C++ の性能を兼ね備え、Python などの言語の簡潔さと充実した標準ライブラリを持っています。

インストール#

  1. https://go.dev/ にアクセスし、Download をクリックして、対応するプラットフォームのインストーラをダウンロードしてインストールします。
  2. 上記の URL にアクセスできない場合は、https://studygolang.com/dl にアクセスしてダウンロードしてください。
  3. GitHub のアクセス速度が遅い場合は、go mod proxy を設定することをお勧めします。https://goproxy.cn/ の説明を参考に設定すると、サードパーティの依存パッケージのダウンロード速度が大幅に向上します。

IDE の推奨#

  • vscode に Go プラグインをインストール
  • GoLand JetBrains シリーズの新しい IDE、dddd
    image.png

GitHub を通じてこのコースのサンプルプロジェクトを簡単に体験できます Dashboard — Gitpod (素晴らしい、泣きそうです)

基本データ型#

整数型#

C++ と似ており、整数型は符号付きと符号なしのタイプに分かれています。符号付き整数は

  • int8、int16、int32、int64
  • それぞれ 8 ビット、16 ビット、32 ビット、64 ビットの符号付き整数
  • uint8、uint16、uint32、uint64 は符号なし整数に対応します。
  • さらに、特定の CPU プラットフォームのマシンワードサイズに対応する符号付きと符号なしの整数intuintもあります。intは最も広く使用される数値型で、これらの 2 つの型は同じサイズ: 32 または 64 ビットです。
    • 異なるコンパイラは、同じハードウェアプラットフォーム上でも異なるサイズを生成する可能性があります。
  • Unicode 文字 rune 型は int32と等価な型で、通常はUnicode コードポイントを表すために使用されます。これらの 2 つの名前は互換的に使用できます。
  • byteuint8 型の等価型で、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を使用して出力する際に00x、または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 の浮動小数点型には float32float64 があります。

その範囲の限界値は math パッケージで見つけることができます。

  • 定数 math.MaxFloat32float32 が表すことができる最大数値を示し、約 3.4e38 です。対応する math.MaxFloat64 定数は約 1.8e308 です。これらが表すことができる最小値はそれぞれ約 1.4e-454.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 言語は、complex64complex128 の 2 種類の精度の複素数型を提供します。これらはそれぞれ float32float64 の 2 つの浮動小数点数精度に対応します。組み込みの complex 関数は複素数を構築するために使用され、組み込みの realimag 関数はそれぞれ複素数の実部虚部を返します。

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.141592i2iのように、複素数の虚部が形成され、複素数の実部は 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 バイトを含みます。

  • ij は省略可能で、省略された場合は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 の略です。ParseIntParseFloatを使用して文字列を解析できます。また、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 宣言文の最初の宣言された定数の行で、iota0 に設定され、その後、各定数宣言の行で 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 に変換され、そのメモリサイズは不確定です。無型浮動小数点数と複素数定数は、メモリサイズが明確float64complex128 に変換されます。

プログラム構造#

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 += 1i = i + 1 と等価です。対応するデクリメントは i-- で、i から 1 を引きます。これらはであり、C 系の他の言語のような式ではありません。

  • したがって、j = i++無効であり、++-- は変数名の後ろにのみ置くことができます。したがって、--i も無効です。

type#

C++ の typeof の強化版に似ており、形式は次のとおりです。

type 型名 基礎型

以下のように、CelsiusFahrenheit の 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 言語聖典

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 つの型、keyvalue の型が必要です。
    • map[string]intkey 型が stringvalue 型が 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#

slicemap に対しては、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 つのフィールド、namepassword を含んでいます。

  • 構造体の名前を使用して構造体変数を初期化し、各フィールドの初期値を渡す必要があります
  • また、キーと値のペアの形式で初期値を指定することもでき、これにより一部のフィールドのみを初期化できます。
  • 同様の構造体はポインタもサポートしており、これにより構造体の直接変更が可能になり、特定の状況では大きな構造体のコピーコストを回避できます。
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)
	}
}

ツールの推奨#

授業中に紹介されたコード生成ツールのいくつか

課後練習#

  1. 最初の例の謎解きゲームの最終コードを修正し、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
		}
	}
}
  1. 2 番目の例のコマンドライン辞書の最終コードを修正し、別の翻訳エンジンのサポートを追加します。
  1. 前のステップの基礎の上に、コードを修正して 2 つの翻訳エンジンに並行リクエストを行い、応答速度を向上させます。

まとめと感想#

授業では Go の基本文法について詳しく説明され、自分で Go 言語の聖典を読んだまとめも加え、この文章を書きました。JS や C/C++ との共通点が多いと感じています。

内容はGo 言語聖典および第三回青訓キャンプのコースからのものです。

読み込み中...
文章は、創作者によって署名され、ブロックチェーンに安全に保存されています。