首页  >  文章  >  后端开发  >  在 Golang 中安全使用 Map:声明和初始化的差异

在 Golang 中安全使用 Map:声明和初始化的差异

王林
王林原创
2024-08-31 22:31:081201浏览

Safely using Maps in Golang: Differences in declaration and initialization

介绍

本周,我正在为 golang 开发一个 API 包装器包,它处理发送带有 URL 编码值的 post 请求、设置 cookie 以及所有有趣的东西。但是,当我构建主体时,我使用 url.Value 类型来构建主体,并使用它来添加和设置键值对。然而,我在某些部分遇到了有线零指针引用错误,我认为这是因为我手动设置的一些变量造成的。然而,通过仔细调试,我发现了一个常见的陷阱或不好的做法,即仅声明类型但初始化它,从而导致零引用错误。

在这篇文章中,我将介绍什么是映射、如何创建映射,特别是如何正确声明和初始化它们。在 golang 中映射或任何类似数据类型的声明和初始化之间建立适当的区别。

Golang 中的地图是什么?

golang中的map或hashmap是一种基本数据类型,允许我们存储键值对。在底层,它是一个类似标头映射的数据结构,用于保存存储桶,这些存储桶基本上是指向存储桶数组(连续内存)的指针。它具有存储实际键值对的哈希码,以及在当前键数量溢出时指向新存储桶的指针。这是一个非常智能的数据结构,提供几乎恒定的时间访问。

如何在 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。为此,您需要检查该密钥是否存在于地图中。

更多详情,你可以查看我的Golang Maps帖子详细内容。

声明和初始化之间的区别

在编程语言中声明和初始化任何变量都是不同的,并且必须在底层类型的实现上做更多的事情。对于 int、string、float 等主要数据类型,有默认值/零值,因此这与变量的声明和初始化相同。然而,在映射和切片的情况下,声明只是确保变量可用于程序的范围,但是对于初始化来说,是将其设置为其默认值/零值或应分配的实际值。

因此,声明只是使变量在程序范围内可用。对于映射和切片,声明变量而不初始化会将其设置为 nil,这意味着它指向没有分配的内存,并且不能直接使用。

而初始化会分配内存并将变量设置为可用状态。对于映射和切片,您需要使用 myMap = make(map[keyType]valueType) 或 slice = []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 中安全使用 Map:声明和初始化的差异的详细内容。更多信息请关注PHP中文网其他相关文章!

声明:
本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系admin@php.cn