简单聊聊Go语言中空结构体和空字符串的特殊之处

 更新时间:2024年03月25日 16:29:30   作者:胡译胡说  
在日常的编程过程中,大家应该经常能遇到各种”空“吧,比如空指针、空结构体、空字符串等,本文就以 Go 语言为例,一起来看看空结构体和空字符串在 Go 语言中的特殊之处吧

在日常的编程过程中,大家应该经常能遇到各种”空“吧,比如空指针、空结构体、空字符串……代码中的这些”空“往往是特例,都有特殊的性质。

本文就以 Go 语言为例,一起来看看空结构体空字符串在 Go 语言中的特殊之处。

首先是空结构体。

Go 语言中的空结构体

我们先来运行这样一段代码。

// https://go.dev/play/p/L2YxOr8k6Qq
package main

type U struct{}
type V struct{}

func main() {
    var i = 10

    var u = U{}
    var v = V{}

    println("i address =", &i)
    println("u address =", &u)
    println("v address =", &v)
}

// 运行结果
// i address = 0xc000046730
// u address = 0xc000046730
// v address = 0xc000046730

iuv 这 3 个变量的内存地址竟然完全一样!

uv 的内存地址相同就已经有点出乎意料了,毕竟它们的类型不同,一个是 struct U 的实例(值),一个是 struct V 的实例。但更出乎意料的是,这个内存地址竟然还是变量 i 的地址(如下图)。

这是因为 struct Ustruct V 都是空结构体这种特殊的结构体,而空结构体的实例,即 struct{}{}不占用任何存储空间,图中自然也就找不到存储着 struct{}{} 的空间。

不占用存储空间且内存地址相同,这就是空结构体这种“空”的特点。

更有意思的是,既然 u (或 v)的地址就是变量 i 的地址,那通过 u 应该也能读出存储在 0xc000046730 这个位置的整数 10吧。 让我们来试一试。

println(*(*int)(unsafe.Pointer(&u)))

果然可以!

下面我们再来看看另一种“空”——空字符串。

Go 语言中的空字符串

下面这段代码会输出什么呢?交替出现的 Sora 和空行吗?

// https://go.dev/play/p/c1ZfChdH0rT
package main

import "fmt"

func main() {
    title := ""
    go func() {
        for {
            fmt.Println(title)
        }
    }()

    for {
        go func() {
            title = ""
        }()

        go func() {
            title = "Sora"
        }()
    }
}

竟然 painc 了,意不意外?

panic: runtime error: invalid memory address or nil pointer dereference
[signal SIGSEGV: segmentation violation code=0x1 addr=0x0 pc=0x462cca]

goroutine 18 [running]:
fmt.(*buffer).writeString(...)
    /usr/local/go-faketime/src/fmt/print.go:108
fmt.(*fmt).padString(0x425000000041f01c?, {0x0, 0x4})
    /usr/local/go-faketime/src/fmt/format.go:110 +0x24a
...
fmt.Println(...)
    /usr/local/go-faketime/src/fmt/print.go:314
main.main.func1()
    /tmp/sandbox3517788398/prog.go:9 +0x5a
created by main.main in goroutine 1
    /tmp/sandbox3517788398/prog.go:7 +0x66

下面就来分析一下背后的原因(从本节的标题也能猜出吧,八成和title = ""这里的空字符串有关)。

首先,由倒数第 3 行的 /tmp/sandbox3517788398/prog.go:9 +0x5a 可见,导致 panic 的代码是第 9 行的 fmt.Println(title)

而 “破案”的线索就在报错信息的这一行(第 7 行):

fmt.(*fmt).padString(0x425000000041f01c?, {0x0, 0x4})

接下来我们先找出 fmt.padString() 函数的定义。

// https://github.com/golang/go/blob/master/src/fmt/format.go#L110

// padString appends s to f.buf, ...
func (f *fmt) padString(s string) {

对照着定义,可以猜出 {0x0, 0x4} 对应的正是参数 s string

那再结合字符串类型 string 在 Go 语言中的定义,

// https://github.com/golang/go/blob/master/src/internal/unsafeheader/unsafeheader.go#L34
type String struct {
    Data unsafe.Pointer
    Len  int
}

不难推测出,这里相当于我们将 String{Data: 0x0, Len: 0x4} 这样一个表示字符串的结构体传递给了 fmt.padString()。而这是一个长度为 4 的空字符串

这里没有写错,就是长度为 4 的空字符串

既然长度为 4,那别管空不空,fmt.Println() 就要通过存在于 Data 中的指针(地址)取出这“4个字符”——计算机就是这么“诚实”。但 Data == 0x0,是空指针,当然就空指针 panic 了,即报错信息中的“invalid memory address or nil pointer dereference”。

“案子”是破了,可“长度为 4 的空字符串”又是怎么产生的呢?

罪魁祸首就在这一对儿协程上,

    for {
        go func() {
            title = ""
        }()

        go func() {
            title = "Sora"
        }()
    }

看似通过 = 一下子就能把字符串赋给变量 title,但实际上不得不依次对 DataLen 赋值,比如,

go routine1: title.Data = <空字符串""的地址> = 0x0
go routine1: title.Len  = <空字符串""的长度> = 0

go routine2: title.Data = <字符串"Sora"的地址>
go routine2: title.Len  = <字符串"Sora"的长度> = 4

而当这一对儿协程并发执行时,以上 2 组“语句”的执行顺序是不确定的,完全有可能出现以下二者交替执行情况:

go routine2: title.Data = <字符串"Sora"的地址>
    go routine1: title.Data = <空字符串""的地址> = 0x0
    go routine1: title.Len  = <空字符串""的长度> = 0
go routine2: title.Len  = <字符串"Sora"的长度> = 4

于是导致了{Data: 0x0, Len: 0x4},即长度为 4 的空字符串

painc 的“案子”终于破了。

本文通过两个小例子简单介绍了 Go 语言中的“空”,诸位也可以测试测试其他语言中的“空”有什么特性。

到此这篇关于简单聊聊Go语言中空结构体和空字符串的特殊之处的文章就介绍到这了,更多相关Go空结构体和空字符串内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!

相关文章

  • 使用Go语言实现常见hash算法

    使用Go语言实现常见hash算法

    这篇文章主要为大家详细介绍了使语言实现各种常见hash算法的相关知识,文中的示例代码讲解详细,具有一定的借鉴价值,需要的小伙伴可以参考下
    2024-01-01
  • Go语言实现本地缓存的策略详解

    Go语言实现本地缓存的策略详解

    今天给大家分享的是Go语言本地缓存的一些内容,主要是结合bigcache和fastcache两个优秀的开源代码库,总结一些设计思路和感悟,文章通过代码示例介绍的非常详细,需要的朋友可以参考下
    2023-07-07
  • 浅谈Golang的方法传递值应该注意的地方

    浅谈Golang的方法传递值应该注意的地方

    这篇文章主要介绍了浅谈Golang的方法传递值应该注意的地方,具有很好的参考价值,希望对大家有所帮助。一起跟随小编过来看看吧
    2020-12-12
  • Go 热加载之fresh详解

    Go 热加载之fresh详解

    这篇文章主要为大家介绍了Go 热加载之fresh详解,有需要的朋友可以借鉴参考下,希望能够有所帮助,祝大家多多进步,早日升职加薪
    2022-08-08
  • Go 使用Unmarshal将json赋给struct出错的原因及解决

    Go 使用Unmarshal将json赋给struct出错的原因及解决

    这篇文章主要介绍了Go 使用Unmarshal将json赋给struct出错的原因及解决方案,具有很好的参考价值,希望对大家有所帮助。一起跟随小编过来看看吧
    2021-03-03
  • 一文深入探索Go语言中的循环结构

    一文深入探索Go语言中的循环结构

    在编程中,循环结构扮演着重要的角色,它使我们能够有效地重复执行特定的代码块,以实现各种任务和逻辑,在Go语言中,for 是 Go 中唯一的循环结构,本文将深入探讨Go语言中的for循环类型以及它们的用法
    2023-08-08
  • Golang学习笔记之安装Go1.15版本(win/linux/macos/docker安装)

    Golang学习笔记之安装Go1.15版本(win/linux/macos/docker安装)

    这篇文章主要介绍了Golang学习笔记之安装Go1.15版本(win/linux/macos/docker安装),本文给大家介绍的非常详细,对大家的学习或工作具有一定的参考借鉴价值,需要的朋友可以参考下
    2020-12-12
  • golang实现页面静态化操作的示例代码

    golang实现页面静态化操作的示例代码

    这篇文章主要介绍了golang实现页面静态化操作的示例代码,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一起学习学习吧
    2020-02-02
  • Go+Lua解决Redis秒杀中库存与超卖问题

    Go+Lua解决Redis秒杀中库存与超卖问题

    本文主要介绍了Go+Lua解决Redis秒杀中库存与超卖问题,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一起学习学习吧
    2023-03-03
  • 9个Golang中map常用示例分享

    9个Golang中map常用示例分享

    这篇文章主要和大家分享9个Golang中map可能是常用的使用案例,从1到9,越来越厉害。文中的示例代码讲解详细,希望对大家学习Golang有一定的帮助
    2023-02-02

最新评论