データ

newによる割り当て

Goには2つの割り当てプリミティブ、組み込み関数newmakeがあります。これらは異なることを行い、異なる型に適用され、混乱を招く可能性がありますが、ルールは単純です。まずnewについて説明しましょう。これは、メモリを割り当てる組み込み関数ですが、他の言語の同名の関数とは異なり、メモリを初期化しません。メモリをゼロ化するだけです。つまり、new(T)は型Tの新しいアイテムのためのゼロ化されたストレージを割り当て、そのアドレス、つまり*T型の値を返します。Go の用語では、新しく割り当てられた型Tのゼロ値へのポインタを返します。

newによって返されるメモリはゼロ化されているため、データ構造を設計するときに各型のゼロ値がさらなる初期化なしに使用できるように配置することが役立ちます。これは、データ構造のユーザーがnewでデータ構造を作成して、すぐに作業を開始できることを意味します。たとえば、bytes.Bufferのドキュメントは「Bufferのゼロ値は使用可能な空のバッファです」と述べています。同様に、sync.Mutexには明示的なコンストラクタやInitメソッドがありません。代わりに、sync.Mutexのゼロ値はロックされていないミューテックスとして定義されています。

ゼロ値が有用であるという性質は推移的に働きます。この型宣言を考えてみてください。

type SyncedBuffer struct {
    lock    sync.Mutex
    buffer  bytes.Buffer
}

SyncedBuffer型の値も、割り当てまたは単に宣言の直後に使用する準備ができています。次のスニペットでは、pvの両方が、さらなる配置なしに正しく動作します。

p := new(SyncedBuffer)  // type *SyncedBuffer
var v SyncedBuffer      // type  SyncedBuffer

コンストラクタと複合リテラル

時にはゼロ値では不十分で、初期化コンストラクタが必要です。パッケージosから派生したこの例のように。

func NewFile(fd int, name string) *File {
    if fd < 0 {
        return nil
    }
    f := new(File)
    f.fd = fd
    f.name = name
    f.dirinfo = nil
    f.nepipe = 0
    return f
}

ここには多くの定型文があります。複合リテラルを使用して簡略化できます。これは、評価されるたびに新しいインスタンスを作成する式です。

func NewFile(fd int, name string) *File {
    if fd < 0 {
        return nil
    }
    f := File{fd, name, nil, 0}
    return &f
}

Cとは異なり、ローカル変数のアドレスを返すのは完全に問題ありません。変数に関連付けられたストレージは、関数が戻った後も生き残ります。実際、複合リテラルのアドレスを取ることは、評価されるたびに新しいインスタンスを割り当てるため、これらの最後の2行を組み合わせることができます。

return &File{fd, name, nil, 0}

複合リテラルのフィールドは順番に配置され、すべてが存在する必要があります。ただし、要素をフィールド:ペアとして明示的にラベル付けすることで、初期化子は任意の順序で現れることができ、欠落しているものはそれぞれのゼロ値として残されます。したがって、次のように言うことができます:

return &File{fd: fd, name: name}

限定的なケースとして、複合リテラルにフィールドが全く含まれていない場合、その型のゼロ値が作成されます。式new(File)&File{}は等価です。

複合リテラルは配列、スライス、マップに対しても作成でき、フィールドラベルは適切なインデックスまたはマップキーです。これらの例では、EnoneEioEinvalの値に関係なく、それらが異なる限り、初期化は機能します。

a := [...]string   {Enone: "no error", Eio: "Eio", Einval: "invalid argument"}
s := []string      {Enone: "no error", Eio: "Eio", Einval: "invalid argument"}
m := map[int]string{Enone: "no error", Eio: "Eio", Einval: "invalid argument"}

makeによる割り当て

割り当てに戻ります。組み込み関数make(T, args)new(T)とは異なる目的を果たします。スライス、マップ、チャネルのみを作成し、(ゼロ化されていない)初期化されたT型の値(*Tではない)を返します。区別の理由は、これらの3つの型が、使用前に初期化されなければならないデータ構造への参照を内部的に表すためです。たとえば、スライスは、データ(配列内部)へのポインタ、長さ、容量を含む3項目記述子であり、これらの項目が初期化されるまで、スライスはnilです。スライス、マップ、チャネルの場合、makeは内部データ構造を初期化し、使用する値を準備します。たとえば、

make([]int, 10, 100)

は100の整数の配列を割り当て、その配列の最初の10要素を指す長さ10と容量100のスライス構造を作成します。(スライスを作成するとき、容量は省略できます。詳細については、スライスに関するセクションを参照してください)対照的に、new([]int)は、新しく割り当てられ、ゼロ化されたスライス構造へのポインタ、つまりnilスライス値へのポインタを返します。

これらの例は、newmakeの違いを示しています。

var p *[]int = new([]int)       // スライス構造を割り当てます。*p == nil、めったに有用ではありません
var v  []int = make([]int, 100) // スライス v は 100 の整数の新しい配列を参照します

// 不必要に複雑:
var p *[]int = new([]int)
*p = make([]int, 100, 100)

// 慣用的:
v := make([]int, 100)

makeはマップ、スライス、チャネルにのみ適用され、ポインタを返さないことを覚えておいてください。明示的なポインタを取得するには、newで割り当てるか、変数のアドレスを明示的に取得してください。

配列

配列は、メモリの詳細なレイアウトを計画するときに有用で、時にはアロケーションを避けるのに役立ちますが、主に次のセクションの主題であるスライスの構成要素です。そのトピックの基盤を築くため、配列について数語述べます。

GoとCで配列が機能する方法には大きな違いがあります。Goでは:

  • 配列は値です。1つの配列を別の配列に割り当てると、すべての要素がコピーされます。
  • 特に、配列を関数に渡すと、関数はそれへのポインタではなく、配列のコピーを受け取ります。
  • 配列のサイズは型の一部です。型[10]int[20]intは異なります。

値の性質は有用ですが、同時に高価でもあります。C のような動作と効率性が必要な場合は、配列へのポインタを渡すことができます。

func Sum(a *[3]float64) (sum float64) {
    for _, v := range *a {
        sum += v
    }
    return
}

array := [...]float64{7.0, 8.5, 9.1}
x := Sum(&array)  // 明示的なアドレス演算子に注意

しかし、このスタイルでさえ慣用的なGoではありません。代わりにスライスを使用してください。

スライス

スライスは配列をラップして、データシーケンスに対してより一般的で、強力で、便利なインターフェースを提供します。変換行列などの明示的な次元を持つアイテムを除いて、Goでの配列プログラミングのほとんどは、単純な配列ではなくスライスで行われます。

スライスは基になる配列への参照を保持し、1つのスライスを別のスライスに割り当てると、両方とも同じ配列を参照します。関数がスライス引数を取る場合、スライスの要素に対して行う変更は、基になる配列へのポインタを渡すのと同様に、呼び出し元に見えます。したがって、Read関数はポインタとカウントではなく、スライス引数を受け入れることができます。スライス内の長さは、読み取るデータの上限を設定します。パッケージosFile型のReadメソッドのシグネチャは次のとおりです:

func (f *File) Read(buf []byte) (n int, err error)

メソッドは、読み取られたバイト数とエラー値(もしあれば)を返します。より大きなバッファbufの最初の32バイトに読み取るには、バッファをスライス(ここでは動詞として使用)します。

n, err := f.Read(buf[0:32])

このようなスライスは一般的で効率的です。実際、効率性は一時的に置いておいて、次のスニペットもバッファの最初の32バイトを読み取ります。

var n int
var err error
for i := 0; i < 32; i++ {
    nbytes, e := f.Read(buf[i:i+1])  // 1バイト読み取ります
    n += nbytes
    if nbytes == 0 || e != nil {
        err = e
        break
    }
}

基になる配列の制限内に収まる限り、スライスの長さは変更できます。それ自体のスライスに割り当てるだけです。スライスの容量は、組み込み関数capでアクセスでき、スライスが想定できる最大長を報告します。スライスにデータを追加する関数は次のとおりです。データが容量を超えた場合、スライスは再割り当てされます。結果のスライスが返されます。この関数は、nilスライスに適用されるとき、lencapが合法であり、0を返すという事実を使用します。

func Append(slice, data []byte) []byte {
    l := len(slice)
    if l + len(data) > cap(slice) {  // 再割り当て
        // 将来の成長のため、必要な分の2倍を割り当てます
        newSlice := make([]byte, (l+len(data))*2)
        // copy関数は事前に宣言され、任意のスライス型で動作します
        copy(newSlice, slice)
        slice = newSlice
    }
    slice = slice[0:l+len(data)]
    copy(slice[l:], data)
    return slice
}

Appendsliceの要素を変更できますが、スライス自体(ポインタ、長さ、容量を保持するランタイム データ 構造)は値によって渡されるため、その後スライスを返さなければなりません。

スライスへの追加のアイデアは非常に有用で、append組み込み関数によって捕獲されています。その関数の設計を理解するには、もう少し情報が必要ですので、後で戻ってきます。

2次元スライス

Go の配列とスライスは1次元です。2D配列またはスライスに相当するものを作成するには、次のような配列の配列またはスライスのスライスを定義する必要があります:

type Transform [3][3]float64  // 3x3配列、実際には配列の配列
type LinesOfText [][]byte     // バイト スライスのスライス

スライスは可変長であるため、各内部スライスは異なる長さを持つことができます。これは、LinesOfTextの例のような一般的な状況になる可能性があります:各行は独立した長さを持ちます。

text := LinesOfText{
    []byte("Now is the time"),
    []byte("for all good gophers"),
    []byte("to bring some fun to the party."),
}

時々、2D スライスを割り当てる必要があり、これは、たとえば、ピクセルのスキャンラインを処理するときに発生する可能性があります。これを実現するには2つの方法があります。1つは各スライスを独立して割り当てることです。もう1つは、単一の配列を割り当てて、個々のスライスをそれに向けることです。どちらを使用するかは、アプリケーションによって異なります。スライスが成長または縮小する可能性がある場合は、次の行を上書きしないように独立して割り当てる必要があります。そうでない場合は、単一の割り当てでオブジェクトを構築する方が効率的です。参考のため、2つの方法の概要を次に示します。最初に、一度に1行ずつ:

// トップレベル スライスを割り当てます
picture := make([][]uint8, YSize) // y単位ごとに1行
// 行をループして、各行のスライスを割り当てます
for i := range picture {
    picture[i] = make([]uint8, XSize)
}

そして今、1つの割り当てとして、行にスライスされます:

// トップレベル スライスを割り当てます。前と同じです
picture := make([][]uint8, YSize) // y単位ごとに1行
// すべてのピクセルを保持する1つの大きなスライスを割り当てます
pixels := make([]uint8, XSize*YSize) // []uint8型を持ちます(pictureは[][]uint8ですが)
// 行をループして、残りのピクセルスライスの前面から各行をスライスします
for i := range picture {
    picture[i], pixels = pixels[:XSize], pixels[XSize:]
}

マップ

マップは、1つの型(キー)の値を別の型(要素または)の値に関連付ける便利で強力な組み込みデータ構造です。キーは、等価演算子が定義されている任意の型、たとえば整数、浮動小数点数、複素数、文字列、ポインタ、インターフェース(動的型が等価性をサポートしている限り)、構造体、配列にすることができます。スライスは、等価性が定義されていないため、マップキーとして使用できません。スライスと同様に、マップは基になるデータ構造への参照を保持します。マップの内容を変更する関数にマップを渡すと、変更は呼び出し元に見えます。

マップは、通常の複合リテラル構文でコロンで区切られたキーと値のペアを使用して構築できるため、初期化中に簡単に構築できます。

var timeZone = map[string]int{
    "UTC":  0*60*60,
    "EST": -5*60*60,
    "CST": -6*60*60,
    "MST": -7*60*60,
    "PST": -8*60*60,
}

マップ値の割り当てと取得は、インデックスが整数である必要がないことを除いて、配列やスライスと同様に構文的に見えます。

offset := timeZone["EST"]

マップに存在しないキーでマップ値を取得しようとすると、マップ内のエントリの型のゼロ値が返されます。たとえば、マップに整数が含まれている場合、存在しないキーを検索すると0が返されます。セットは、値の型がboolのマップとして実装できます。マップエントリをtrueに設定してセットに値を入れ、単純なインデックスでテストします。

attended := map[string]bool{
    "Ann": true,
    "Joe": true,
    ...
}

if attended[person] { // person がマップにない場合は false になります
    fmt.Println(person, "was at the meeting")
}

時々、欠落エントリをゼロ値から区別する必要があります。"UTC"のエントリがあるのか、それともマップにまったくないため 0 なのでしょうか?複数の割り当ての形式で識別できます。

var seconds int
var ok bool
seconds, ok = timeZone[tz]

明らかな理由により、これは「comma ok」慣用句と呼ばれます。この例では、tzが存在する場合、secondsは適切に設定され、okは真になります。そうでない場合、secondsはゼロに設定され、okは偽になります。これを素晴らしいエラー レポートと組み合わせる関数は次のとおりです:

func offset(tz string) int {
    if seconds, ok := timeZone[tz]; ok {
        return seconds
    }
    log.Println("unknown time zone:", tz)
    return 0
}

実際の値を気にすることなく、マップ内の存在をテストするには、通常の値の変数の代わりにブランク識別子(_)を使用できます。

_, present := timeZone[tz]

マップエントリを削除するには、delete組み込み関数を使用します。この関数の引数は、マップと削除するキーです。キーがマップにすでに存在しない場合でも、これを行うのは安全です。

delete(timeZone, "PDT")  // 標準時間になりました

印刷

Goでのフォーマット済み印刷は、Cのprintfファミリーに似たスタイルを使用しますが、より豊富で一般的です。関数はfmtパッケージにあり、大文字の名前を持ちます:fmt.Printffmt.Fprintffmt.Sprintfなど。文字列関数(Sprintfなど)は、提供されたバッファを埋めるのではなく、文字列を返します。

フォーマット文字列を提供する必要はありません。PrintfFprintfSprintfのそれぞれについて、たとえばPrintPrintlnの別のペアの関数があります。これらの関数は、フォーマット文字列を取りませんが、代わりに各引数のデフォルト フォーマットを生成します。Printlnバージョンも引数間に空白を挿入し、出力に改行を追加しますが、Printバージョンは、どちらの側のオペランドも文字列でない場合にのみ空白を追加します。この例では、各行が同じ出力を生成します。

fmt.Printf("Hello %d\n", 23)
fmt.Fprint(os.Stdout, "Hello ", 23, "\n")
fmt.Println("Hello", 23)
fmt.Println(fmt.Sprint("Hello ", 23))

フォーマットされた印刷関数fmt.Fprintとその仲間は、第1引数としてio.Writerインターフェースを実装する任意のオブジェクトを取ります。変数os.Stdoutos.Stderrは馴染みのあるインスタンスです。

ここでCから逸脱し始めます。まず、%dなどの数値フォーマットは、符号や サイズのフラグを取りません。代わりに、印刷ルーチンは引数の型を使用してこれらの特性を決定します。

var x uint64 = 1<<64 - 1
fmt.Printf("%d %x; %d %x\n", x, x, int64(x), int64(x))

印刷します:

18446744073709551615 ffffffffffffffff; -1 -1

整数の10進数などのデフォルト変換が必要な場合は、包括的フォーマット%v(「値」の意味)を使用できます。結果は、PrintPrintlnが生成するものとまったく同じです。さらに、そのフォーマットは、配列、スライス、構造体、マップでさえ、任意の値を印刷できます。前のセクションで定義されたタイム ゾーン マップの印刷ステートメントは次のとおりです。

fmt.Printf("%v\n", timeZone)  // または単に fmt.Println(timeZone)

これは次の出力を提供します:

map[CST:-21600 EST:-18000 MST:-25200 PST:-28800 UTC:0]

マップの場合、Printfと仲間はキーによって出力を辞書順にソートします。

構造体を印刷するとき、修正されたフォーマット%+vは構造体のフィールドに名前で注釈を付け、任意の値の代替フォーマット%#vは完全なGo構文で値を印刷します。

type T struct {
    a int
    b float64
    c string
}
t := &T{ 7, -2.35, "abc\tdef" }
fmt.Printf("%v\n", t)
fmt.Printf("%+v\n", t)
fmt.Printf("%#v\n", t)
fmt.Printf("%#v\n", timeZone)

印刷します:

&{7 -2.35 abc	def}
&{a:7 b:-2.35 c:abc	def}
&main.T{a:7, b:-2.35, c:"abc\tdef"}
map[string]int{"CST":-21600, "EST":-18000, "MST":-25200, "PST":-28800, "UTC":0}

(アンパサンドに注意してください。)引用符で囲まれた文字列フォーマットは、string型または[]byte型の値に適用されるときに%qを通じても利用できます。可能な場合、代替フォーマット%#qは代わりにバッククォートを使用します。(%qフォーマットは整数とルーンにも適用され、単一引用符のルーン定数を生成します。)また、%xは文字列、バイト配列、バイト スライス、および整数で動作し、長い16進文字列を生成し、フォーマットにスペース(% x)があると、バイト間にスペースを入れます。

もう1つの便利なフォーマットは%Tで、値のを印刷します。

fmt.Printf("%T\n", timeZone)

印刷します:

map[string]int

カスタム型のデフォルト フォーマットを制御したい場合は、その型でシグネチャString() stringを持つメソッドを定義するだけです。単純な型Tの場合、それは次のようになります。

func (t *T) String() string {
    return fmt.Sprintf("%d/%g/%q", t.a, t.b, t.c)
}
fmt.Printf("%v\n", t)

フォーマットで印刷するには:

7/-2.35/"abc\tdef"

TのポインタだけでなくT型のを印刷する必要がある場合は、Stringのレシーバーは値型でなければなりません。この例では、構造体型にとってより効率的で慣用的なため、ポインタを使用しました。詳細については、ポインタ vs 値レシーバーのセクションを参照してください。)

私たちのStringメソッドはSprintfを呼び出すことができます。印刷ルーチンは完全に再帰的であり、この方法でラップできるためです。ただし、この方法について理解すべき重要な詳細が1つあります:Stringメソッドを無限に再帰するSprintf呼び出しを構築しないでください。これは、Sprintf呼び出しが文字列としてレシーバーを直接印刷しようとし、それが再びメソッドを呼び出すとき発生する可能性があります。この例が示すように、よくある間違いです。

type MyString string

func (m MyString) String() string {
    return fmt.Sprintf("MyString=%s", m) // エラー:永久に再帰します
}

修正するのも簡単です:引数を基本的な文字列型に変換します。これには、メソッドがありません。

type MyString string
func (m MyString) String() string {
    return fmt.Sprintf("MyString=%s", string(m)) // OK:変換に注意
}

初期化セクションでは、この再帰を回避する別の手法を見ることができます。

別の印刷手法は、印刷ルーチンの引数を別のそのようなルーチンに直接渡すことです。Printfのシグネチャは、フォーマット後に(任意の型の)任意の数のパラメータが現れることを指定するために、最終引数に...interface{}型を使用します。

func Printf(format string, v ...interface{}) (n int, err error) {

関数Printf内では、v[]interface{}型の変数のように動作しますが、別の可変引数関数に渡されると、通常の引数リストのように動作します。上で使用した関数log.Printlnの実装は次のとおりです。実際のフォーマットのために、引数を直接fmt.Sprintlnに渡します。

// Println は fmt.Println の方法で標準ロガーに印刷します
func Println(v ...interface{}) {
    std.Output(2, fmt.Sprintln(v...))  // Output は parameters (int, string) を取ります
}

Sprintlnへの入れ子呼び出しでvの後に...を書いて、コンパイラーにvを引数のリストとして扱うように指示します。そうしないと、vを単一のスライス引数として渡すだけです。

印刷についてはここで説明したもの以上にもっとあります。fmtパッケージのgodocドキュメントで詳細を参照してください。

ちなみに、...パラメータは特定の型にできます。たとえば、整数のリストの最小値を選択する min 関数の...int

func Min(a ...int) int {
    min := int(^uint(0) >> 1)  // 最大のint
    for _, i := range a {
        if i < min {
            min = i
        }
    }
    return min
}

Append

今、append組み込み関数の設計を説明するために必要な欠落した部分があります。appendのシグネチャは、上記のカスタムAppend関数とは異なります。概略的には、次のようになります:

func append(slice []T, elements ...T) []T

ここで、Tは任意の型のプレースホルダーです。型Tが呼び出し元によって決定されるGoで関数を実際に記述することはできません。そのため、appendは組み込まれています:コンパイラからのサポートが必要です。

appendの動作は、要素をスライスの最後に追加して結果を返すことです。手書きのAppendと同様に、基になる配列が変更される可能性があるため、結果を返す必要があります。この単純な例:

x := []int{1,2,3}
x = append(x, 4, 5, 6)
fmt.Println(x)

[1 2 3 4 5 6]を印刷します。したがって、appendPrintfのように機能し、任意の数の引数を収集します。

しかし、Appendの動作を実行し、スライスをスライスに追加したい場合はどうでしょうか?簡単:上記のOutput呼び出しで行ったように、呼び出しサイトで...を使用します。このスニペットは、上記のものと同じ出力を生成します。

x := []int{1,2,3}
y := []int{4,5,6}
x = append(x, y...)
fmt.Println(x)

その...がなければ、型が間違っているためコンパイルされません。yint型ではありません。