Golang指针使用问题求助

Golang指针使用问题求助 我之前从未在支持指针的语言中编写过程序。我写了这段代码,但不太确定它为什么能正常工作:

package main

import (
	"encoding/json"
	"fmt"
	"io/ioutil"
	"log"
	"os"
	"path"
)

type Gist struct {
	AccessToken string
	Files       []string
}

type Conf struct {
	Gist Gist
}

// 尝试读取用户主目录下的 .sinkerrc.json 文件
func ReadSinkerRc() ([]byte, error) {
	homdir, err := os.UserHomeDir()
	if err != nil {
		return nil, err
	}
	data, err := ioutil.ReadFile(path.Join(homdir, ".sinkerrc.json"))
	if err != nil {
		return nil, err
	}
	return data, nil
}

// 解析配置中的json。
func ParseJsonConfg(data []byte) (Conf, error) {
	var conf Conf
	err := json.Unmarshal(data, &conf)
	return conf, err
}

// 获取配置数据,负责读取并解析配置文件中的json
func Get() (*Conf, error) {
	data, err := ReadSinkerRc()
	if err != nil {
		return nil, err
	}
	conf, err := ParseJsonConfg(data)
	if err != nil {
		return nil, err
	}
	return &conf, nil
}

func main() {
	config, err := Get()
	if err != nil {
		log.Fatal(err.Error())
	}
	fmt.Println(config.Gist.AccessToken)

}

Get 函数返回一个指向 Conf 的指针和一个错误,因为否则我们无法表示一个为 nil 的 Conf,但我们可以表示一个 nil 指针,因为指针的默认零值是 nil。并且没有办法让一个结构体的值为 nil,对吗?如果是这样,为什么不行呢?

我不理解的是,如果 Get 返回一个 Conf 的内存地址和一个错误,为什么在 main 函数中我可以直接访问 Conf 的属性,而不需要获取它的底层值?为什么我不需要做 *conf 来获取底层值?


更多关于Golang指针使用问题求助的实战教程也可以访问 https://www.itying.com/category-94-b0.html

9 回复

非常感谢。我现在明白了。那确实让我很困惑。感谢您抽出时间。

更多关于Golang指针使用问题求助的实战系列教程也可以访问 https://www.itying.com/category-94-b0.html


很好的解释。那么一个例子可能是配置变更,你希望其他代码能在不重启应用的情况下感知到?

性能并不是使用指针的特定用例。使用指针的主要目的是为了实现值的共享可变性,以便所有引用该值的地方都能观察到其变化。当你需要这种可变性时,应该使用指针;如果不需要,则应使用非指针值。

根据程序的行为以及逃逸分析器的决定,使用指针可能会将对象分配到堆上而非栈上,从而导致更多的"Stop-the-World"垃圾回收,从而损害程序的性能。

skillian:

我不太确定你所说的在函数作用域之外修改那个字符串有什么用处。

我询问的是,通常我编写代码时都不使用显式指针,并且总是从函数作用域返回新值。在这篇文章中,关于“为什么使用指针”的一个答案是,这本身并不完全是为了性能,而是为了让我们能够观察系统范围内的状态变化并做出反应。我特别想知道,什么时候能有一个良好且常见的用例来说明这个观点。

另一个问题……指针的使用似乎违背了我在 JavaScript、Python 等语言中遵循多年的许多实践,在这些语言中,我尽量避免函数作用域之外的隐式更改。

从函数外部修改变量似乎很危险,因为这可能难以追踪是谁在更改什么。话虽如此,我想我理解指针的意义,正是为了允许这种情况,以避免将数据结构复制到函数的作用域中。对于大型数据结构,在更注重性能的语言中,这是有道理的。我的观察对吗?

除了性能原因之外,指针还有其他特定的使用场景吗?

你能帮我理解,除了性能之外,为什么在函数作用域之外改变那个字符串是有用的吗?

对于这个特定的例子,基于结构体字段中的 json 标签,看起来该字段是可变的,以便 json.Unmarshal(或其他兼容的实现)能够将数据反序列化到其中。在没有阅读他们代码的情况下,我不明白他们为什么使用 *string 而不是 string。这似乎暗示在 JSON 中这些字段可能为 null,并且在 Go 代码中,nil*string""string 之间存在有意义的区别。

我不太确定你所说的“在函数作用域之外改变那个字符串是有用的”是什么意思。

例如,在GitHub API的Go语言绑定中,一个Gist结构体包含一个嵌入的Files结构体,而该Files结构体又有一个Filename字段,这个字段不是字符串类型,而是一个指针。您能帮我理解,除了性能原因之外,为什么在函数作用域之外修改这个字符串是有用的吗?

type GistFile struct {
	Size     *int    `json:"size,omitempty"`
	Filename *string `json:"filename,omitempty"`
	Language *string `json:"language,omitempty"`
	Type     *string `json:"type,omitempty"`
	RawURL   *string `json:"raw_url,omitempty"`
	Content  *string `json:"content,omitempty"`
}

为什么我不需要执行 *conf 来获取底层的值?

我相信这个问题在某个地方被 Rus Cox 回答过,但我现在找不到了。Go 语言的设计者们认为,使用 . 来访问结构体或结构体指针的字段都是可以的,因为这没有歧义。我觉得这有点像这样:

var a, b float32

a = 1.2
b = 3.4

// 我们应该将 * 操作编译为 CPU 整数乘法还是浮点乘法?这很明显!a 和 b 是浮点数,所以使用浮点乘法!
c := a * b

var d struct{ e float32 }

// 这个 . 是针对指针还是值?d 是一个结构体值,所以显然是后者!
d.e = c

var f *struct{ g float32 } = new(struct{ float32 })

// 同样,类型让这一点显而易见。
f.g = c

可以fmt.Println((*config).Gist.AccessToken),但这是不必要的,因为对于 . 你还能合理地表示其他什么意思呢?

在Go语言中,当你通过指针访问结构体字段时,编译器会自动进行解引用。config.Gist.AccessToken 实际上等价于 (*config).Gist.AccessToken,这是Go语言提供的语法糖,让指针操作更加简洁。

示例说明:

package main

import "fmt"

type Person struct {
    Name string
    Age  int
}

func getPerson() (*Person, error) {
    p := Person{Name: "Alice", Age: 30}
    return &p, nil
}

func main() {
    // 方法1:直接访问(自动解引用)
    p1, _ := getPerson()
    fmt.Println(p1.Name)  // 自动解引用,等价于 (*p1).Name
    
    // 方法2:显式解引用
    p2, _ := getPerson()
    fmt.Println((*p2).Name)  // 显式解引用
    
    // 两种方式是等价的
    fmt.Println(p1.Name == (*p1).Name)  // true
}

关于结构体nil值的问题:你是正确的。结构体类型的零值是一个所有字段都为相应类型零值的结构体实例,而不是nil。只有指针、切片、映射、通道、函数和接口类型的零值才是nil。

示例对比:

package main

import "fmt"

type Config struct {
    Value string
}

func getConfigPtr() (*Config, error) {
    return nil, nil  // 可以返回nil指针
}

func getConfigValue() (Config, error) {
    var c Config
    return c, nil  // 返回的是零值Config,不是nil
}

func main() {
    // 指针可以检查nil
    c1, _ := getConfigPtr()
    if c1 == nil {
        fmt.Println("c1 is nil")
    }
    
    // 结构体值不能是nil
    c2, _ := getConfigValue()
    // if c2 == nil {  // 编译错误:cannot convert nil to type Config
    // }
    
    // 结构体零值
    fmt.Printf("c2: %+v\n", c2)  // c2: {Value:}
}

在你的代码中,Get() 返回 &conf 是正确的做法,这样可以在出错时返回 nil, err,调用方可以通过检查指针是否为nil来判断是否获取到了有效配置。

回到顶部