”工欲善其事,必先利其器。“—孔子《论语.录灵公》
首页 > 编程 > Go 数组如何工作以及如何使用 For-Range

Go 数组如何工作以及如何使用 For-Range

发布于2024-08-22
浏览:164

这是帖子的摘录;完整的帖子可以在这里找到:Go 数组如何工作并使用 For-Range 进行技巧。

经典的 Golang 数组和切片非常简单。数组是固定大小的,而切片是动态的。但我必须告诉你,Go 表面上看起来很简单,但它背后却发生了很多事情。

一如既往,我们将从基础知识开始,然后进行更深入的研究。别担心,当你从不同的角度观察数组时,它们会变得非常有趣。

我们将在下一部分中介绍切片,一旦准备好我就会把它放在这里。

什么是数组?

Go 中的数组与其他编程语言中的数组非常相似。它们有固定的大小,并将相同类型的元素存储在连续的内存位置中。

这意味着Go可以快速访问每个元素,因为它们的地址是根据数组的起始地址和元素的索引计算的。

func main() {
    arr := [5]byte{0, 1, 2, 3, 4}
    println("arr", &arr)

    for i := range arr {
        println(i, &arr[i])
    }
}

// Output:
// arr 0x1400005072b
// 0 0x1400005072b
// 1 0x1400005072c
// 2 0x1400005072d
// 3 0x1400005072e
// 4 0x1400005072f

这里有几件事需要注意:

  • 数组arr的地址与第一个元素的地址相同。
  • 每个元素的地址彼此相距1字节,因为我们的元素类型是byte。

How Go Arrays Work and Get Tricky with For-Range

内存中数组[5]byte{0, 1, 2, 3, 4}

仔细看图。

我们的堆栈是从较高的地址向下增长到较低的地址,对吧?这张图准确地展示了数组在栈中的样子,从 arr[4] 到 arr[0].

那么,这是否意味着我们可以通过知道第一个元素(或数组)的地址和元素的大小来访问数组的任何元素?让我们用 int 数组和不安全的包来尝试一下:

func main() {
    a := [3]int{99, 100, 101}

    p := unsafe.Pointer(&a[0])

    a1 := unsafe.Pointer(uintptr(p)   8)
    a2 := unsafe.Pointer(uintptr(p)   16)

    fmt.Println(*(*int)(p))
    fmt.Println(*(*int)(a1))
    fmt.Println(*(*int)(a2))
}

// Output:
// 99
// 100
// 101

好吧,我们获取指向第一个元素的指针,然后通过将 int 大小的倍数相加来计算指向下一个元素的指针,在 64 位体系结构中,int 大小为 8 个字节。然后我们使用这些指针来访问并将它们转换回 int 值。

How Go Arrays Work and Get Tricky with For-Range

内存中的数组 [3]int{99, 100, 101}

该示例只是为了教育目的而使用 unsafe 包直接访问内存。在不了解后果的情况下,不要在生产中这样做。

现在,类型 T 的数组本身并不是类型,但具有 特定大小和类型 T 的数组被视为类型。这就是我的意思:

func main() {
    a := [5]byte{}
    b := [4]byte{}

    fmt.Printf("%T\n", a) // [5]uint8
    fmt.Printf("%T\n", b) // [4]uint8

    // cannot use b (variable of type [4]byte) as [5]byte value in assignment
    a = b 
}

尽管 a 和 b 都是字节数组,Go 编译器将它们视为完全不同的类型,%T 格式清楚地表明了这一点。

Go编译器内部是这样看待它的(src/cmd/compile/internal/types2/array.go):

// An Array represents an array type.
type Array struct {
    len  int64
    elem Type
}

// NewArray returns a new array type for the given element type and length.
// A negative length indicates an unknown length.
func NewArray(elem Type, len int64) *Array { return &Array{len: len, elem: elem} }

数组的长度在类型本身中“编码”,因此编译器从其类型知道数组的长度。尝试将一种大小的数组分配给另一种大小的数组或比较它们,将导致类型不匹配错误。

数组文字

Go中初始化数组的方法有很多种,有些在实际项目中可能很少用到:

var arr1 [10]int // [0 0 0 0 0 0 0 0 0 0]

// With value, infer-length
arr2 := [...]int{1, 2, 3, 4, 5} // [1 2 3 4 5]

// With index, infer-length
arr3 := [...]int{11: 3} // [0 0 0 0 0 0 0 0 0 0 0 3]

// Combined index and value
arr4 := [5]int{1, 4: 5} // [1 0 0 0 5]
arr5 := [5]int{2: 3, 4, 4: 5} // [0 0 3 4 5]

我们上面所做的(除了第一个)是定义和初始化它们的值,这称为“复合文字”。该术语也用于切片、映射和结构。

现在,有一件有趣的事情:当我们创建一个少于 4 个元素的数组时,Go 会生成指令将值逐个放入数组中。

所以当我们执行 arr := [3]int{1, 2, 3, 4} 时,实际发生的是:

arr := [4]int{}
arr[0] = 1
arr[1] = 2
arr[2] = 3
arr[3] = 4

这种策略称为本地代码初始化。这意味着初始化代码是在特定函数的范围内生成和执行的,而不是全局或静态初始化代码的一部分。

当你阅读下面的另一个初始化策略时,它会变得更清楚,其中值不是像那样一个一个地放入数组中。

“超过 4 个元素的数组怎么样?”

编译器在二进制文件中创建数组的静态表示,这称为“静态初始化”策略。

这意味着数组元素的值存储在二进制文件的只读部分中。该静态数据是在编译时创建的,因此这些值直接嵌入到二进制文件中。如果你好奇 [5]int{1,2,3,4,5} 在 Go 汇编中的样子:

main..stmp_1 SRODATA static size=40
    0x0000 01 00 00 00 00 00 00 00 02 00 00 00 00 00 00 00  ................
    0x0010 03 00 00 00 00 00 00 00 04 00 00 00 00 00 00 00  ................
    0x0020 05 00 00 00 00 00 00 00                          ........

要看到数组的值并不容易,但我们还是可以从中得到一些关键信息。

我们的数据存储在stmp_1中,它是只读静态数据,大小为40字节(每个元素8字节),并且该数据的地址被硬编码在二进制中。

编译器生成代码来引用此静态数据。当我们的应用程序运行时,它可以直接使用这个预先初始化的数据,而不需要额外的代码来设置数组。

const readonly = [5]int{1, 2, 3, 4, 5}

arr := readonly

“如果数组有 5 个元素,但只初始化了 3 个元素呢?”

好问题,这个字面量 [5]int{1,2,3} 属于第一类,Go 将值逐个放入数组中。

在讨论定义和初始化数组时,我们应该提到并非每个数组都在堆栈上分配。如果它太大,它就会被移动到堆中。

但是您可能会问,“太大”有多大。

从 Go 1.23 开始,如果变量(而不仅仅是数组)的大小超过常量值 MaxStackVarSize(当前为 10 MB),它将被认为对于堆栈分配来说太大,并将逃逸到堆。

func main() {
    a := [10 * 1024 * 1024]byte{}
    println(&a)

    b := [10*1024*1024   1]byte{}
    println(&b)
}

在这种情况下,b 将移动到堆,而 a 不会。

数组运算

数组的长度在类型本身中进行编码。即使数组没有 cap 属性,我们仍然可以获得它:

func main() {
    a := [5]int{1, 2, 3}
    println(len(a)) // 5
    println(cap(a)) // 5
}

毫无疑问,容量等于长度,但最重要的是我们在编译时就知道这一点,对吧?

因此 len(a) 对编译器没有意义,因为它不是运行时属性,Go 编译器在编译时就知道该值。

...

这是帖子的摘录;完整的帖子可以在这里找到:Go 数组如何工作并使用 For-Range 进行技巧。

版本声明 本文转载于:https://dev.to/func25/how-go-arrays-work-and-get-tricky-with-for-range-3i9i?1如有侵犯,请联系[email protected]删除
最新教程 更多>
  • 解决MySQL插入Emoji时出现的\\"字符串值错误\\"异常
    解决MySQL插入Emoji时出现的\\"字符串值错误\\"异常
    Resolving Incorrect String Value Exception When Inserting EmojiWhen attempting to insert a string containing emoji characters into a MySQL database us...
    编程 发布于2025-05-18
  • 如何将来自三个MySQL表的数据组合到新表中?
    如何将来自三个MySQL表的数据组合到新表中?
    mysql:从三个表和列的新表创建新表 答案:为了实现这一目标,您可以利用一个3-way Join。 选择p。*,d.content作为年龄 来自人为p的人 加入d.person_id = p.id上的d的详细信息 加入T.Id = d.detail_id的分类法 其中t.taxonomy =...
    编程 发布于2025-05-18
  • Java的Map.Entry和SimpleEntry如何简化键值对管理?
    Java的Map.Entry和SimpleEntry如何简化键值对管理?
    A Comprehensive Collection for Value Pairs: Introducing Java's Map.Entry and SimpleEntryIn Java, when defining a collection where each element com...
    编程 发布于2025-05-18
  • 大批
    大批
    [2 数组是对象,因此它们在JS中也具有方法。 切片(开始):在新数组中提取部分数组,而无需突变原始数组。 令ARR = ['a','b','c','d','e']; // USECASE:提取直到索引作...
    编程 发布于2025-05-18
  • FastAPI自定义404页面创建指南
    FastAPI自定义404页面创建指南
    response = await call_next(request) if response.status_code == 404: return RedirectResponse("https://fastapi.tiangolo.com") else: ...
    编程 发布于2025-05-18
  • Go语言垃圾回收如何处理切片内存?
    Go语言垃圾回收如何处理切片内存?
    在Go Slices中的垃圾收集:详细的分析在GO中,Slice是一个动态数组,引用了基础阵列。使用切片时,了解垃圾收集行为至关重要,以避免潜在的内存泄漏。考虑使用slice使用slice的以下实现:字符串{ R:=(*Q)[0] *q =(*q)[1:len(*q)] 返回...
    编程 发布于2025-05-18
  • 如何实时捕获和流媒体以进行聊天机器人命令执行?
    如何实时捕获和流媒体以进行聊天机器人命令执行?
    在开发能够执行命令的chatbots的领域中,实时从命令执行实时捕获Stdout,一个常见的需求是能够检索和显示标准输出(stdout)在cath cath cant cant cant cant cant cant cant cant interfaces in Chate cant inter...
    编程 发布于2025-05-18
  • 如何将PANDAS DataFrame列转换为DateTime格式并按日期过滤?
    如何将PANDAS DataFrame列转换为DateTime格式并按日期过滤?
    Transform Pandas DataFrame Column to DateTime FormatScenario:Data within a Pandas DataFrame often exists in various formats, including strings.使用时间数据时...
    编程 发布于2025-05-18
  • 如何将MySQL数据库添加到Visual Studio 2012中的数据源对话框中?
    如何将MySQL数据库添加到Visual Studio 2012中的数据源对话框中?
    在Visual Studio 2012 尽管已安装了MySQL Connector v.6.5.4,但无法将MySQL数据库添加到实体框架的“ DataSource对话框”中。为了解决这一问题,至关重要的是要了解MySQL连接器v.6.5.5及以后的6.6.x版本将提供MySQL的官方Visual...
    编程 发布于2025-05-18
  • Java字符串非空且非null的有效检查方法
    Java字符串非空且非null的有效检查方法
    检查字符串是否不是null而不是空的 if(str!= null && str.isementy())二手: if(str!= null && str.length()== 0) option 3:trim()。isement(Isement() trim whitespace whitesp...
    编程 发布于2025-05-18
  • Async Void vs. Async Task在ASP.NET中:为什么Async Void方法有时会抛出异常?
    Async Void vs. Async Task在ASP.NET中:为什么Async Void方法有时会抛出异常?
    在ASP.NET async void void async void void void void void的设计无需返回asynchroncon而无需返回任务对象。他们在执行过程中增加未偿还操作的计数,并在完成后减少。在某些情况下,这种行为可能是有益的,例如未期望或明确预期操作结果的火灾和...
    编程 发布于2025-05-18
  • 如何在GO编译器中自定义编译优化?
    如何在GO编译器中自定义编译优化?
    在GO编译器中自定义编译优化 GO中的默认编译过程遵循特定的优化策略。 However, users may need to adjust these optimizations for specific requirements.Optimization Control in Go Compi...
    编程 发布于2025-05-18
  • 反射动态实现Go接口用于RPC方法探索
    反射动态实现Go接口用于RPC方法探索
    在GO 使用反射来实现定义RPC式方法的界面。例如,考虑一个接口,例如:键入myService接口{ 登录(用户名,密码字符串)(sessionId int,错误错误) helloworld(sessionid int)(hi String,错误错误) } 替代方案而不是依靠反射...
    编程 发布于2025-05-18
  • 在UTF8 MySQL表中正确将Latin1字符转换为UTF8的方法
    在UTF8 MySQL表中正确将Latin1字符转换为UTF8的方法
    在UTF8表中将latin1字符转换为utf8 ,您遇到了一个问题,其中含义的字符(例如,“jáuòiñe”)在utf8 table tabled tablesset中被extect(例如,“致电。The recommended approach to correct the data is t...
    编程 发布于2025-05-18
  • 如何避免Go语言切片时的内存泄漏?
    如何避免Go语言切片时的内存泄漏?
    ,a [j:] ...虽然通常有效,但如果使用指针,可能会导致内存泄漏。这是因为原始的备份阵列保持完整,这意味着新切片外部指针引用的任何对象仍然可能占据内存。 copy(a [i:] 对于k,n:= len(a)-j i,len(a); k
    编程 发布于2025-05-18

免责声明: 提供的所有资源部分来自互联网,如果有侵犯您的版权或其他权益,请说明详细缘由并提供版权或权益证明然后发到邮箱:[email protected] 我们会第一时间内为您处理。

Copyright© 2022 湘ICP备2022001581号-3