Go语言积累的笔记

二维切片

2023.09.07

在创建二维数组时,数组的大小必须是常量,而不能是变量,如:

1
2
3
n := 10
var arr01 [10][10]int   // 正确
var arr02 [n][n]int   // 报错:invalid array length n

那么,当我们在函数中需要将二维数组的长度设置为传入的参数n时,就不能使用数组了,只能使用二维切片

但是二维切片的定义不是简单的:slice01 := make([][]int, n),这样得到的切片不能直接使用slice01[i][j]进行赋值。

因为slice01[i] 的类型为[]int,现在是不知道每个slice01[i]的长度的,也没有分配空间。

我们需要遍历外层的切片,为每一个内层切片分配空间,然后才能像数组一样直接对切片元素slice01[i][j]进行赋值。

具体如下:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
package main

import "fmt"

func main() {

    n := 3
    
    // 创建n*n的切片
    a := make([][]int, n)
    for i := range a {
       a[i] = make([]int, n)
    }
    fmt.Println(a)  // [[0 0 0] [0 0 0] [0 0 0]]
    a[0][0] = 1
    fmt.Println(a)  // [[1 0 0] [0 0 0] [0 0 0]]
}

判断两个切片的值是否相等

2023.09.08

在Go语言中,切片是引用类型,因此比较两个切片是否相等需要考虑切片中元素的个数和元素值是否相等。

以下是一种比较两个切片是否相等的方法:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
package main  
  
import (  
 "fmt"  
 "reflect"  
)  
  
func main() {  
 slice1 := []int{1, 2, 3}  
 slice2 := []int{1, 2, 3}  
  
 // 使用reflect.DeepEqual函数比较切片是否相等  
 equal := reflect.DeepEqual(slice1, slice2)  
 fmt.Println("切片相等:", equal)  
}

在上面的示例中,我们使用reflect.DeepEqual函数来比较两个切片slice1slice2是否相等。reflect.DeepEqual函数在比较两个切片时会检查切片的长度和每个对应位置的元素值是否相等。

如果两个切片相等,它将返回true,否则返回false

请注意,reflect.DeepEqual函数在比较切片时会进行递归比较,可以处理嵌套切片的情况。如果切片的元素是复杂类型(例如,切片、映射或结构体),则可以使用reflect.DeepEqual来比较它们的相等性。

complex64

2023.09.17

在Go语言中,complex64表示一个复数类型,其中实部和虚部的浮点数都是32位(即float32类型)。复数在数学中表示形式为a+bi,其中a是实部,b是虚部,而i是虚数单位(满足i^2=-1)。complex64类型在Go语言中主要用于处理涉及复数的计算。

常量表达式

2023.09.17

Golang的常量表达式中,函数必须是内置函数,否则编译不过。(√)

这个表述是正确的。

在Go语言中,常量表达式是在编译时就可以计算出结果的表达式,它们用于定义常量值。常量表达式必须使用内置类型的常量,例如整数、浮点数、字符串或布尔值,并且可以使用基本的运算符来组合这些常量来创建更复杂的常量表达式。以下是一些关于常量表达式的重要规则:

  1. 常量表达式必须在编译时可确定其值,不能包含运行时才能计算的表达式。

  2. 常量可以是整数、浮点数、复数、字符串、字符、布尔值或枚举类型。

  3. 常量表达式可以包括基本的运算符,如加法、减法、乘法、除法和位运算等。

  4. 常量表达式可以使用内置的函数,如len()、cap()、make()等,但这些函数只能作用于常量参数

  5. 常量表达式的结果可以用于类型转换,但必须满足类型转换的规则。

以下是一些常量表达式的示例:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
const (
    a = 10
    b = 3.14
    c = "Hello"
    d = true
    e = a + 20
    f = len(c)
    g = iota // 特殊常量,用于枚举
)

const (
    // 枚举
    Sunday = iota
    Monday
    Tuesday
    Wednesday
    Thursday
    Friday
    Saturday
)

上面的示例中,a、b、c、d等都是常量表达式,它们在编译时就可以确定其值。常量表达式可以用于很多方面,如定义枚举值、数组长度、切片容量等,以及在一些需要在编译时就确定的上下文中使用常量值。

获取多行输入

2023.09.21

在Go语言中,您可以使用标准库中的bufio包来获取多行输入并将每行的输入存入切片中。以下是一个示例代码,演示了如何实现这个功能:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
package main

import (
    "bufio"
    "fmt"
    "os"
    "strings"
)

func main() {
    // 创建一个切片来存储输入的多行文本
    lines := []string{}

    // 创建一个 bufio.Scanner 来从标准输入读取多行文本
    scanner := bufio.NewScanner(os.Stdin)

    fmt.Println("请输入多行文本(输入空行结束):")

    // 使用循环逐行读取输入,并将每行添加到切片中
    for scanner.Scan() {
        text := scanner.Text()

        // 如果输入是空行,则退出循环
        if text == "" {
            break
        }

        lines = append(lines, text)
    }

    // 检查是否有扫描错误
    if err := scanner.Err(); err != nil {
        fmt.Println("读取输入时发生错误:", err)
        return
    }

    // 打印存储的文本行
    fmt.Println("您输入的文本行:")
    for _, line := range lines {
        fmt.Println(line)
    }
}

这个示例代码首先创建了一个切片 lines 来存储输入的多行文本。然后,它使用 bufio.NewScanner(os.Stdin) 创建了一个扫描器,该扫描器可以从标准输入读取文本。在一个循环中,它逐行读取输入,并将每行添加到 lines 切片中,直到输入一个空行为止。

最后,它检查是否有扫描错误并打印存储的文本行。

您可以在终端中运行此代码,输入多行文本,直到输入一个空行,然后它将显示您输入的文本行。

整数在底层的存储

2023.09.22

int8为例:

int8是一个8位有符号整数类型,它可以表示的范围是-128到127。

这是因为int8使用8个比特(或位)来存储数值,其中最高位(最左边的位)用于表示正负号,而剩下的7个位用于表示数值本身。因此,int8可以表示2^8(即256)个不同的数值。由于最高位用于表示正负号,所以int8可以表示的范围是从-128到127。

具体来说,当最高位为0时,表示的是正数,范围从0到127;当最高位为1时,表示的是负数,范围从-1到-128。因此,int8的范围是-128到127。

所以go语言中,各种整数的表示范围如下:

有符号类型 范围 位数
int8 -128 到 127 8位
int16 -32768 到 32767 16位
int32 -2,147,483,648 到 2,147,483,647 32位
int64 -9,223,372,036,854,775,808 到 9,223,372,036,854,775,807 64位
int (底层平台决定) -2,147,483,648 到 2,147,483,647 (32位系统)
-9,223,372,036,854,775,808 到 9,223,372,036,854,775,807(64位系统)
32/64位
无符号类型 范围 位数
uint8 0 到 255 8位
uint16 0 到65535 16位
uint32 0 到4294967295 32位
uint64 0 到18446744073709551615 64位
uint (底层平台决定) 0 到4294967295 (32位系统)
0 到18446744073709551615(64位系统)
32/64位

int:根据系统架构,可以是32位或64位整数。(32位系统占32位,64位系统占64位)

  • 对于有符号的整型而言,其最高位为符号位,真正表示存储数值的位数会少一位,而且存在正数和负数对半分的情况,所以表示正数的范围再次减半。
  • 对于无符号的整数而言,其最高位也是符号位,不过只能为0(表示正数)。由于其不存在负数的情况,所以可以表示的正数范围比有符号的整数大一倍。

len()

2023.10.01

1
func len(v Type) int

len 内建函数返回 v 的长度,这取决于具体类型:

1
2
3
4
5
数组:v 中元素的数量。
数组指针:*v 中元素的数量(即使 v 为 nil)。
切片或映射:v 中元素的数量;若 v 为 nil,len(v) 即为零。
字符串:v 中字节的数量。
信道:信道缓存中队列(未读取)元素的数量;若 v 为 nil,len(v) 即为零。

特别注意:对字符串使用len()函数,得到的是v的字节数而不是字符串长度。如:

1
2
3
4
a := "hello"
b := "Go语言"
fmt.Println(len(a))  // 5
fmt.Println(len(b))  // 8(字节数为8,中文占3个字节)

如果想获取字符串中Unicode字符的数量(字符数),而不是字节数,您可以使用utf8.RuneCountInString函数。这将返回字符串中的Unicode字符数。

1
2
3
str := "Hello, Go语言"
length := utf8.RuneCountInString(str)
fmt.Println(length) // 11(字符长度为11)

string的本质

2023.10.01

在编程语言中,字符串是一种重要的数据结构,通常由一系列字符组成。字符串一般有种类型,一种在编译时指定长度,不能修改。一种具有动态的长度,可以修改。但是在 Go言中,字符串不能被修改,只能被访问,不能采取如下方式对字符串进行修改。

1
2
var b = "hello world"
b[1] = "a"

字符串的终止有两种方式,一种是 C 语言中的隐式申明,以字符“\0”作为终止符。一种是 Go语言中的显式声明。Go 语言运行时字符串 string 的表示结构如下。

1
2
3
4
type stringStruct struct {
	str unsafe.Pointer
	len int
}

其中,Data 指向底层的字符数组,Len 代表字符串的长度。字符串在本质上是一串字符数组,每个字符在存储时都对应了一个或多个整数,这涉及字符集的编码方式。

string类型的数据占用空间的大小为16字节,unsafe.Pointerint类型的字段分别占用8个字节空间。

如下所示,在打印hello world这11个字符时,通过下标输出其十六进制表示的字节数组为 68 65 6c 6c 6f 20 77 6f 72 6c 64

1
2
3
4
str := "hello world"
for i := 0; i < len(str); i++ {
    fmt.Printf("%x ", str[i])  // %x:表示打印16进制的整数
}

Go语言中所有的文件都采用 UTF-8 的编码方式,同时字符常量使用 UTF-8 的字符编码集。 UFT-8是一种长度可变的编码方式,可包含世界上大部分的字符。上例中的字母都只占据 1 字节,但是特殊的字符(例如大部分中文) 会占据 3 字节。如下所示,变量 b 看起来只有 4个符,但是 len(b) 获取的长度为 8,字符串 b 中每个中文都占据了3 字节。

1
2
3
4
5
b := "Go语言"
fmt.Println(len(b))  // 8
for i := 0; i < len(b); i++ {
    fmt.Printf("%x ", b[i])  // 47 6f e8 af ad e8 a8 80 
}

Go 语言的设计者认为,用字符 (character) 表示字符串的组成元素可能产生歧义,因为有些字符非常相似,例如小写拉丁字母 a 与带重音符号的 à。这些相似的字符真正的区别在于其编码后的整数是不相同的,a 被表示为 0x61,a 被表示为 0xE0。因此在 Go 语言中使用符文(rune)类型来表示和区分字符串中的“字符”,rune 其实是 int32 的别称。 当用range 轮询字符串时,轮询的不再是单字节,而是具体的rune。如下所示,对字符串b进行轮询,其第一个参数 index 代表每个 rune 的字节偏移量,而runeValue 为 int32,代表符文数。

1
2
3
4
var b = "Go语言"
for index, runeValue := range b {
    fmt.Printf("%#U starts at byte position %d\n", runeValue, index)
}

fmt.Printf有一个特殊的格式化符#U可用于打印符文数十六进制的Unicode编码方式及字符形状。如上例打印出:

1
2
3
4
U+0047 'G' starts at byte position 0
U+006F 'o' starts at byte position 1 
U+8BED '语' starts at byte position 2
U+8A00 '言' starts at byte position 5

string类型的空间占用

2023.10.01

当查看string类型的变量所占的空间大小时,会发现是16字节(64位机器)。

1
2
str := "hello"
fmt.Println(unsafe.Sizeof(str)) // 16

也许你会好奇,为什么是16字节,它的底层存储模型是什么样子的。 在src/runtime/string.go中,定义了string的结构:

1
2
3
4
type stringStruct struct {
	str unsafe.Pointer
	len int
}

string底层结构是一个结构体,它有两个字段: str unsafe.Pointer:该字段指向了string底层的byte数组(切片)。 len int:该字段确定了string字符串的长度。 unsafe.Pointerint类型的字段分别占用8个字节空间,所以string类型的变量占用16字节(内存对齐后)空间。

参考:Go1.19.3 string原理简析

内存对齐

2023.10.01

介绍

参考:【Golang】这个内存对齐呀!?

Go语言中的内存对齐是一种用于优化内存访问和提高性能的技术。内存对齐是计算机体系结构中的一个重要概念,它确保数据结构中的字段在内存中按照一定的规则排列,以便CPU能够更有效地访问这些数据。在Go中,内存对齐通常是由编译器和运行时系统来处理的,而不需要手动控制。以下是关于Go语言中内存对齐的一些详细信息:

  1. 原始数据类型的内存对齐:

    • 在Go语言中,原始数据类型(如int、float、bool等)的内存对齐通常按照它们的大小进行,例如int32会按照4字节对齐,int64会按照8字节对齐。这意味着它们将始终从内存的4字节或8字节边界开始存储。
  2. 结构体的内存对齐:

    • 在Go语言中,结构体的字段也会根据其大小进行内存对齐。通常,结构体字段的对齐方式是根据字段中最大的对齐值来确定的。例如,如果一个结构体有一个int32(对齐4字节)和一个float64(对齐8字节)字段,那么它的对齐方式将按照8字节对齐,以适应float64的大小。
  3. 对齐规则:

    • Go语言的内存对齐规则通常是平台相关的,因为不同的操作系统和体系结构可能有不同的要求。编译器和运行时系统会根据目标平台的要求来确定数据结构的内存布局和对齐方式。
  4. 结构体字段的对齐控制:

    • 在Go语言中,可以使用struct标签来控制结构体字段的对齐方式。例如,可以使用struct标签中的align选项来指定字段的对齐方式,但这通常不是必要的,除非你有特定的对齐需求。
  5. 内存对齐的性能影响:

    • 内存对齐可以提高内存访问的性能,因为它允许CPU更有效地加载和存储数据。不正确的内存对齐可能会导致性能下降,因为它会增加数据访问的开销。

总之,Go语言中的内存对齐是一个重要的底层概念,但通常不需要手动干预。Go的编译器和运行时系统会负责处理内存对齐,以确保代码在不同平台上具有良好的性能和可移植性。如果你对特定平台的内存布局有特殊要求,可以考虑使用struct标签来控制对齐方式,但大多数情况下,Go会自动处理这些细节。

结构体中的使用

结构体中可以利用内存对齐,来减少空间的占用。

1.将字段按照大小顺序排列:将结构体字段按照它们的大小进行排序,这可以减少内存浪费。较小的字段放在前面,较大的字段放在后面。

1
2
3
4
5
type MyStruct struct {
    Field1 int32
    Field2 float64
    Field3 string
}

2.使用struct标签来调整字段对齐:虽然Go不提供直接的字段顺序控制,但你可以使用struct标签来调整字段的对齐方式,例如,使用align选项来指定对齐方式。

1
2
3
4
5
type MyStruct struct {
    Field1 int32  `struct:"align:4"`
    Field2 float64
    Field3 string
}

请注意,尽管你可以使用上述方法来尝试优化内存布局,但在大多数情况下,Go的编译器和运行时系统已经足够智能,能够自动执行内存对齐和布局的优化。因此,通常情况下不需要手动控制内存对齐,而应该专注于编写可读性和维护性更好的代码,让Go编译器来处理底层的内存管理细节。

示例

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
type MyStruct1 struct {
    a int8
    b string // 占用16字节
    c int8
}

type MyStruct2 struct {
    a int8
    c int8
    b string // 占用16字节
}

func main() {
    s11 := MyStruct1{
       a: 1,
       b: "hello",
       c: 1,
    }

    s21 := MyStruct2{
       a: 1,
       b: "hello",
       c: 1,
    }

    fmt.Println(unsafe.Sizeof(s11)) // 32
    fmt.Println(unsafe.Sizeof(s21)) // 24
}

在示例中,MyStruct1 结构体的字段顺序是 a int8, b string, c int8。对于大多数的平台,int8 的大小是1字节,而 string 类型包括一个指向底层数据的指针和一个长度字段,大小为16字节(64位系统)。

在大多数64位系统上,string 的内存对齐通常是8字节,因为这些系统的指针大小通常是8字节。在32位系统上,string 的内存对齐通常是4字节,因为32位系统的指针大小通常是4字节。 可以使用unsafe.Sizeof()来查看变量内存占用情况,使用unsafe.Alignof()来查看变量的内存对齐情况。

1
2
3
var s string
size := unsafe.Sizeof(s)  // 16
align := unsafe.Alignof(s)  // 8

首先,让我们计算每个字段的大小:

  1. a int8:1字节
  2. b string:通常8字节(指针) + 8字节(长度) = 16字节
  3. c int8:1字节

然后,考虑内存对齐的要求。在大多数平台上,内存对齐要求是按照字段的大小将其对齐到某个倍数。通常,int8 对齐到1字节,string 在64位系统上对齐到8字节。

现在,让我们计算 MyStruct1 结构体的总大小:

  1. a 需要1字节。
  2. b 需要16字节。
  3. c 需要1字节。

但由于string会对齐到8字节,所以变量a之后会空出7字节的内存,然后才能存放变量b

现在,将这些字段的大小相加:8 + 16 + 1 = 25 字节。但是,由于内存对齐的要求,Go 编译器会将结构体的大小舍入到最接近的8字节(因为结构体中字段中最大对齐值为8)的倍数。所以,MyStruct1 结构体的实际大小是32字节,而不是25字节。

因此,MyStruct1 结构体的占用内存为32字节

MyStruct2 结构体的字段顺序是 a int8, c int8, b string。 内存占用情况:变量a和变量c分别占用1个字节(共2字节),变量b内存对齐到8字节,因此变量c后面会空出6个字节的内存,然后才能存放变量b。 因此,MyStruct1 结构体的占用内存为:8+16=24 字节。(已满足结构体的内存对齐值8)

0%