Golang实现Singleton模式时遇到的空指针异常问题

Golang实现Singleton模式时遇到的空指针异常问题 大家好,感谢各位抽出时间。在我的以下代码中,我遇到了一个空指针异常,我不确定为什么会发生这种情况:

package config

type Config struct {
	DbStr  string `mapstructure:"DB_STRING"`
	DbName string `mapstructure:"DB_NAME"`
	Port   uint32 `mapstructure:"PORT"`
}

var LoadedConfig Config
var isConfigLoaded bool

func loadConfig(path string) (err error) {
	viper.SetDefault("PORT", 3000)
	viper.AddConfigPath(path)
	viper.SetConfigName("app")
	viper.SetConfigType("env")
	viper.AutomaticEnv()

	if err = viper.ReadInConfig(); err != nil {
		return
	}

	err = viper.Unmarshal(&LoadedConfig)

	if err == nil {
		isConfigLoaded = true
	}

	return
}

func GetConfig(path string) (config *Config, err error) {
	if isConfigLoaded {
		return &LoadedConfig, nil
	}

	err = loadConfig(path)

	return
}

main.go 文件中,我运行了 config, err := config.GetConfig("."),但随后得到了一个 panic: runtime error: invalid memory address or nil pointer dereference 错误。

我认为原因是我尝试对包级别的 Config 变量进行反序列化,这个变量被认为是 nil(尽管我认为它不是 nil,因为它是一个结构体的零值变量)。

有趣的是,当我尝试将 loadedConfig 改为 var configInstance *Config 时,代码就正常工作了(当然代码也做了相应修改)。

供参考,这是修改后的成功代码:

package config

var configInstance *Config

func loadConfig(path string) (err error) {
	config := Config{}
	viper.SetDefault("PORT", 3000)
	viper.AddConfigPath(path)
	viper.SetConfigName("app")
	viper.SetConfigType("env")
	viper.AutomaticEnv()

	if err = viper.ReadInConfig(); err != nil {
		return
	}

	err = viper.Unmarshal(&config)

	if err == nil {
		configInstance = &config
	}

	return
}

func GetConfig(path string) (*Config, error) {
	if configInstance != nil {
		return configInstance, nil
	}

	err := loadConfig(path)

	return configInstance, err
}

能否请您帮我理解一下,为什么包级别的零值结构体不能用于反序列化?谢谢!


更多关于Golang实现Singleton模式时遇到的空指针异常问题的实战教程也可以访问 https://www.itying.com/category-94-b0.html

3 回复

谢谢!我犯了个多么愚蠢的错误 :smiley:

更多关于Golang实现Singleton模式时遇到的空指针异常问题的实战系列教程也可以访问 https://www.itying.com/category-94-b0.html


GetConfig

您在 GetConfig 函数中没有初始化名为 config 的返回值。不带参数的 return 语句会返回已命名的返回值。请参阅 Go 语言之旅

你的问题在于对Go语言中值类型和指针类型的理解。在第一个版本中,LoadedConfig 是一个值类型的结构体变量,但你在 GetConfig 函数中返回的是它的地址。问题出现在 loadConfig 函数失败时,你返回了 nil 指针。

让我们分析一下第一个版本的代码:

func GetConfig(path string) (config *Config, err error) {
    if isConfigLoaded {
        return &LoadedConfig, nil  // 这里返回的是 LoadedConfig 的地址
    }

    err = loadConfig(path)  // 如果这里出错,err != nil

    return  // 这里返回的是零值:config = nil, err = 错误信息
}

loadConfig 失败时,函数返回 (nil, error)。调用者得到的是一个 nil 指针,然后尝试访问这个指针的字段时就会发生空指针异常。

第二个版本能正常工作的原因是:

func GetConfig(path string) (*Config, error) {
    if configInstance != nil {
        return configInstance, nil
    }

    err := loadConfig(path)

    return configInstance, err  // 总是返回 configInstance,即使它是 nil
}

这里的关键区别是:即使 loadConfig 失败,configInstance 仍然会被返回(可能是 nil),调用者需要检查错误并处理 nil 指针的情况。

要修复第一个版本,你可以这样做:

func GetConfig(path string) (*Config, error) {
    if isConfigLoaded {
        return &LoadedConfig, nil
    }

    err := loadConfig(path)
    if err != nil {
        return nil, err  // 明确返回 nil
    }

    return &LoadedConfig, nil
}

或者更简洁的版本:

func GetConfig(path string) (config *Config, err error) {
    if isConfigLoaded {
        return &LoadedConfig, nil
    }

    if err = loadConfig(path); err != nil {
        return nil, err
    }
    
    return &LoadedConfig, nil
}

关于你的疑问:“为什么包级别的零值结构体不能用于反序列化?” - 实际上是可以的。问题不在于反序列化,而在于你的函数在错误情况下返回了 nil 指针。viper.Unmarshal(&LoadedConfig) 是完全有效的,因为 &LoadedConfig 获取的是有效结构体变量的地址。

根本问题是你混淆了两种单例模式的实现方式:

  1. 使用值类型:var instance Config
  2. 使用指针类型:var instance *Config

两种方式都可以工作,但需要正确处理错误情况。你的第二个版本更符合Go语言的惯用法,因为它明确地使用了指针,并且调用者可以清楚地看到可能返回 nil

这里是一个完整的、线程安全的单例模式实现:

package config

import (
    "sync"
    "github.com/spf13/viper"
)

type Config struct {
    DbStr  string `mapstructure:"DB_STRING"`
    DbName string `mapstructure:"DB_NAME"`
    Port   uint32 `mapstructure:"PORT"`
}

var (
    instance *Config
    once     sync.Once
    initErr  error
)

func GetConfig(path string) (*Config, error) {
    once.Do(func() {
        instance, initErr = loadConfig(path)
    })
    return instance, initErr
}

func loadConfig(path string) (*Config, error) {
    config := &Config{}
    
    viper.SetDefault("PORT", 3000)
    viper.AddConfigPath(path)
    viper.SetConfigName("app")
    viper.SetConfigType("env")
    viper.AutomaticEnv()

    if err := viper.ReadInConfig(); err != nil {
        return nil, err
    }

    if err := viper.Unmarshal(config); err != nil {
        return nil, err
    }

    return config, nil
}

这个版本使用 sync.Once 确保线程安全,并且清晰地分离了成功和错误的情况。

回到顶部