ホームページ >バックエンド開発 >Golang >Golang でマップを安全に使用する: 宣言と初期化の違い

Golang でマップを安全に使用する: 宣言と初期化の違い

王林
王林オリジナル
2024-08-31 22:31:081274ブラウズ

Safely using Maps in Golang: Differences in declaration and initialization

導入

今週、私は golang の API ラッパー パッケージの 1 つに取り組んでいました。これは、URL エンコードされた値を含む投稿リクエストの送信、Cookie の設定、その他すべての楽しいことを処理していました。ただし、本文を構築している間、url.Value 型を使用して本文を構築し、それを使用してキーと値のペアを追加および設定していました。しかし、一部の部分で有線 nil ポインタ参照エラーが発生しました。手動で設定した変数のせいだと思いました。しかし、さらに詳しくデバッグしてみると、型を宣言しただけで初期化し、nil 参照エラーが発生するというよくある落とし穴や悪い習慣が見つかりました。

この投稿では、マップとは何か、マップの作成方法、特にマップを適切に宣言して初期化する方法について説明します。 golang でマップまたは同様のデータ型の宣言と初期化を適切に区別します。

Golang の地図とは何ですか?

golang のマップまたはハッシュマップは、キーと値のペアを保存できる基本的なデータ型です。内部的には、バケットを保持するヘッダー マップのようなデータ構造であり、バケットは基本的にバケット配列 (連続メモリ) へのポインターです。これには、実際のキーと値のペアを保存するハッシュ コードと、現在のバケットがキーの数でオーバーフローした場合の新しいバケットへのポインターが含まれています。これは、ほぼ一定時間のアクセスを提供する非常にスマートなデータ構造です。

Golang でマップを作成する方法

golang で簡単なマップを作成するには、文字列と整数のマップを使用した文字頻度カウンターの例を取り上げます。マップは文字をキーとして保存し、その頻度を値として保存します。

package main

import "fmt"

func main() {
    words := "hello how are you"
    letters := map[string]int{}

    for _, word := range words {
        wordCount[word]++
    }

    fmt.Println("Word counts:")
    for word, count := range wordCount {
        fmt.Printf("%s: %d\n", word, count)
    }
}
$ go run main.go

Word counts:
e: 2
 : 3
w: 1
r: 1
y: 1
u: 1
h: 2
l: 2
o: 3
a: 1

したがって、マップを map[string]int{} として初期化すると、空のマップが得られます。これをキーと値の入力に使用できます。文字列を反復処理し、文字 (ルーン) ごとにそのバイトの文字を文字列にキャストして値をインクリメントします。int のゼロ値は 0 なので、デフォルトではキーが存在しない場合、キーは 0 になります。これは少し両刃の剣ですが、キーが値 0 でマップ内に存在するか、キーが存在しないがデフォルト値が 0 であるかはわかりません。そのためには、キーがマップ内に存在するかどうかを確認する必要があります。

さらに詳しくは、私の Golang マップの投稿をご覧ください。

宣言と初期化の違い

プログラミング言語での変数の宣言と初期化には違いがあり、基礎となる型の実装でさらに多くのことを行う必要があります。 int、string、float などのプライマリ データ型の場合は、デフォルト/ゼロ値があるため、変数の宣言と初期化と同じになります。ただし、マップとスライスの場合、宣言は変数がプログラムのスコープで使用できることを確認するだけですが、初期化では変数をデフォルト/ゼロ値、または割り当てる必要がある実際の値に設定します。

つまり、宣言は単にプログラムのスコープ内で変数を使用できるようにするだけです。マップとスライスの場合、初期化せずに変数を宣言すると、その変数が nil に設定されます。これは、変数が割り当てられたメモリを指さず、直接使用できないことを意味します。

一方、初期化ではメモリが割り当てられ、変数が使用可能な状態に設定されます。マップとスライスの場合は、myMap = make(map[keyType]valueType) またはスライス = []type{} のような構文を使用して明示的に初期化する必要があります。この初期化を行わないと、マップまたはスライスを使用しようとすると、nil マップまたはスライスへのアクセスまたは変更によるパニックなどのランタイム エラーが発生します。

マップが宣言されたとき、初期化されたとき、または初期化されていないときのマップの値を見てみましょう。

マップから設定を読み取る構成マネージャーを構築していると想像してください。マップはグローバルに宣言されますが、設定がロードされたときにのみ初期化されます。

  1. 宣言されていますが初期化されていません

以下のコードは、初期化されていないマップ アクセスを示しています。

package main

import (
    "fmt"
    "log"
)

// Global map to store configuration settings
var configSettings map[string]string // Declared but not initialized

func main() {
    // Attempt to get a configuration setting before initializing the map
    serverPort := getConfigSetting("server_port")
    fmt.Printf("Server port: %s\n", serverPort)
}

func getConfigSetting(key string) string {
    if configSettings == nil {
        log.Fatal("Configuration settings map is not initialized")
    }
    value, exists := configSettings[key]
    if !exists {
        return "Setting not found"
    }
    return value
}
$ go run main.go
Server port: Setting not found
  1. 宣言と初期化を同時に行う

以下のコードは、同時に初期化されるマップ アクセスを示しています。

package main

import (
    "fmt"
    "log"
)

// Global map to store configuration settings
var configSettings = map[string]string{
    "server_port":  "8080",
    "database_url": "localhost:5432",
}

func main() {
    serverPort := getConfigSetting("server_port")
    fmt.Printf("Server port: %s\n", serverPort)
}

func getConfigSetting(key string) string {
    value, exists := configSettings[key]
    if !exists {
        return "Setting not found"
    }
    return value
}
$ go run main.go
Server port: 8080
  1. 宣言され、後で初期化される

以下のコードは、後で初期化されるマップ アクセスを示しています。

package main

import (
    "fmt"
    "log"
)

// Global map to store configuration settings
var configSettings map[string]string // declared but not initialized

func main() {
    // Initialize configuration settings
    initializeConfigSettings()
    // if the function is not called, the map will be nil

    // Get a configuration setting safely
    serverPort := getConfigSetting("server_port")
    fmt.Printf("Server port: %s\n", serverPort)
}

func initializeConfigSettings() {
    if configSettings == nil {
        configSettings = make(map[string]string) // Properly initialize the map
        configSettings["server_port"] = "8080"
        configSettings["database_url"] = "localhost:5432"
        fmt.Println("Configuration settings initialized")
    }
}

func getConfigSetting(key string) string {
    if configSettings == nil {
        log.Fatal("Configuration settings map is not initialized")
    }
    value, exists := configSettings[key]
    if !exists {
        return "Setting not found"
    }
    return value
}
$ go run main.go
Configuration settings initialized
Server port: 8080

In the above code, we declared the global map configSettings but didn't initialize it at that point, until we wanted to access the map. We initialize the map in the main function, this main function could be other specific parts of the code, and the global variable configSettings a map from another part of the code, by initializing it in the required scope, we prevent it from causing nil pointer access errors. We only initialize the map if it is nil i.e. it has not been initialized elsewhere in the code. This prevents overriding the map/flushing out the config set from other parts of the scope.

Pitfalls in access of un-initialized maps

But since it deals with pointers, it comes with its own pitfalls like nil pointers access when the map is not initialized.

Let's take a look at an example, a real case where this might happen.

package main

import (
    "fmt"
    "net/url"
)

func main() {
        var vals url.Values
        vals.Add("foo", "bar")
        fmt.Println(vals)
}

This will result in a runtime panic.

$ go run main.go
panic: assignment to entry in nil map

goroutine 1 [running]:
net/url.Values.Add(...)
        /usr/local/go/src/net/url/url.go:902
main.main()
        /home/meet/code/playground/go/main.go:10 +0x2d
exit status 2

This is because the url.Values is a map of string and a list of string values. Since the underlying type is a map for Values, and in the example, we only have declared the variable vals with the type url.Values, it will point to a nil reference, hence the message on adding the value to the type. So, it is a good practice to use make while declaring or initializing a map data type. If you are not sure the underlying type is map then you could use Type{} to initialize an empty value of that type.

package main

import (
    "fmt"
    "net/url"
)

func main() {
        vals := make(url.Values)
        // OR
        // vals := url.Values{}
        vals.Add("foo", "bar")
        fmt.Println(vals)
}
$ go run urlvals.go
map[foo:[bar]]
foo=bar

It is also recommended by the golang team to use the make function while initializing a map. So, either use make for maps, slices, and channels, or initialize the empty value variable with Type{}. Both of them work similarly, but the latter is more generally applicable to structs as well.

Conclusion

Understanding the difference between declaring and initializing maps in Golang is essential for any developer, not just in golang, but in general. As we've explored, simply declaring a map variable without initializing it can lead to runtime errors, such as panics when attempting to access or modify a nil map. Initializing a map ensures that it is properly allocated in memory and ready for use, thereby avoiding these pitfalls.

By following best practices—such as using the make function or initializing with Type{}—you can prevent common issues related to uninitialized maps. Always ensure that maps and slices are explicitly initialized before use to safeguard against unexpected nil pointer dereferences

Thank you for reading this post, If you have any questions, feedback, and suggestions, feel free to drop them in the comments.

Happy Coding :)

以上がGolang でマップを安全に使用する: 宣言と初期化の違いの詳細内容です。詳細については、PHP 中国語 Web サイトの他の関連記事を参照してください。

声明:
この記事の内容はネチズンが自主的に寄稿したものであり、著作権は原著者に帰属します。このサイトは、それに相当する法的責任を負いません。盗作または侵害の疑いのあるコンテンツを見つけた場合は、admin@php.cn までご連絡ください。