Go语言学习笔记

2024/7/15·1 views

文档

官方文档

https://golang.google.cn/doc/

TopGoer

https://topgoer.com/

GoLang 标准库

https://studygolang.com/pkgdoc [中文]
https://pkg.go.dev/ [官方]

Go编程时光

https://golang.iswbm.com/index.html

跟煎鱼学Go

https://eddycjy.gitbook.io/golang/

C语言中文网

http://c.biancheng.net/golang/

其他:

Go 普通指针类型、unsafe.Pointer、uintptr之间的关系
https://www.cnblogs.com/-wenli/p/12682477.html

GoLang

内存逃逸

GC

Map

内置

Go 语言拥有一些不需要进行导入操作就可以使用的内置函数。它们有时可以针对不同的类型进行操作,例如:len、cap 和 append,或必须用于系统级的操作,例如:panic。因此,它们需要直接获得编译器的支持。

复制代码
  append          -- 用来追加元素到数组、slice中,返回修改后的数组、slice
  close           -- 主要用来关闭channel
  delete            -- 从map中删除key对应的value
  panic            -- 停止常规的goroutine  (panic和recover:用来做错误处理)
  recover         -- 允许程序定义goroutine的panic动作
  real            -- 返回complex的实部   (complex、real imag:用于创建和操作复数)
  imag            -- 返回complex的虚部
  make            -- 用来分配内存,返回Type本身(只能应用于slice, map, channel)
  new                -- 用来分配内存,主要用来分配值类型,比如int、struct。返回指向Type的指针
  cap                -- capacity是容量的意思,用于返回某个类型的最大容量(只能用于切片和 map)
  copy            -- 用于复制和连接slice,返回复制的数目
  len                -- 来求长度,比如string、array、slice、map、channel ,返回长度
  print、println     -- 底层打印函数,在部署环境中建议使用 fmt 包

go mod

  • go mod help 查看帮助

  • go mod init初始化模块,会在项目根目录下生成 go.mod 文件。

  • go mod tidy 根据 go.mod 文件来处理依赖关系。

  • go mod vendor 将依赖包复制到项目下的 vendor 目录。建议一些使用了被墙包的话可以这么处理,方便用户快速使用命令 go build -mod=vendor 编译

  • go list -m all 显示依赖关系。go list -m -json all 显示详细依赖关系。

  • go mod download 下载依赖。参数是非必写的,path 是包的路径,version 是包的版本。

数组切片

初始化数组的初始化有多种形式,

go 复制代码
[5] int {1,2,3,4,5}    //长度为5的数组,其元素值依次为:1,2,3,4,5
[5] int {1,2}          //长度为5的数组,其元素值依次为:1,2,0,0,0 。在初始化时没有指定初值的元素将会赋值为其元素类型int的默认值0,string的默认值是"" 
[...] int {1,2,3,4,5}  //长度为5的数组,其长度是根据初始化时指定的元素个数决定的 
[5] int { 2:1,3:2,4:3} //长度为5的数组,key:value,其元素值依次为:0,0,1,2,3。在初始化时指定了2,3,4索引中对应的值:1,2,3 
[...] int {2:1,4:3}    //长度为5的数组,起元素值依次为:0,0,1,0,3。由于指定了最大索引4对应的值3,根据初始化的元素个数确定其长度为5赋值与使用

数组的长度不可改变,在特定场景中这样的集合就不太适用,Go中提供了一种灵活,功能强悍的内置类型Slices切片(“动态数组"),与数组相比切片的长度是不固定的,可以追加元素,在追加时可能使切片的容量增大。切片中有两个概念:一是len长度,二是cap容量,长度是指已经被赋过值的最大下标+1,可通过内置函数len()获得。容量是指切片目前可容纳的最多元素个数,可通过内置函数cap()获得。切片是引用类型,因此在当传递切片时将引用同一指针,修改值将会影响其他的对象。

切片可以通过数组来初始化,也可以通过内置函数make()初始化 .初始化时len=cap,在追加元素时如果容量cap不足时将按len的2倍扩容

go 复制代码
s :=[] int {1,2,3 }           //直接初始化切片,[]表示是切片类型,{1,2,3}初始化值依次是1,2,3.其cap=len=3
s := arr[:]                   //初始化切片s,是数组arr的引用
s := arr[startIndex:endIndex] //将arr中从下标startIndex到endIndex-1 下的元素创建为一个新的切片
s := arr[startIndex:]         //缺省endIndex时将表示一直到arr的最后一个元素
s := arr[:endIndex]           //缺省startIndex时将表示从arr的第一个元素开始
s1 := s[startIndex:endIndex]  //通过切片s初始化切片s1
s :=make([]int,len,cap)       //通过内置函数make()初始化切片s,[]int 标识为其元素类型为int的切片

slice可以从一个数组或一个已经存在的slice中再次声明。slice通过array[i:j]来获取,其中i是数组的开始位置,j是结束位置,但不包含array[j],它的长度是j-i

注意slice和数组在声明时的区别:声明数组时,方括号内写明了数组的长度或使用...自动计算长度,而声明slice时,方括号内没有任何字符

切片是引用类型,在使用时需要注意其操作:

  • 切片可以通过内置函数append(slice []Type,elems ...Type)追加元素,elems可以是一排type类型的数据,也可以是slice,因为追加的一个一个的元素,因此如果将一个slice追加到另一个slice中需要带上"...",这样才能表示是将slice中的元素依次追加到另一个slice中。另外在通过下标访问元素时下标不能超过len大小,如同数组的下标不能超出len范围一样。

    复制代码
    s :=append(s,1,2,3,4)
    s :=append(s,s1...)
  • slice的默认开始位置是0,ar[:n]等价于ar[0:n]

  • slice的第二个序列默认是数组的长度,ar[n:]等价于ar[n:len(ar)]

  • 如果从一个数组里面直接获取slice,可以这样ar[:],因为默认第一个序列是0,第二个是数组的长度,即等价于ar[0:len(ar)]

slice是引用类型,所以当引用改变其中元素的值时,其它的所有引用都会改变该值,例如上面的aSlicebSlice,如果修改了aSlice中元素的值,那么bSlice相对应的值也会改变。

go get 和 go get -u 的区别

如题,区别如下:
加上它可以利用网络来更新已有的代码包及其依赖包。如果已经下载过一个代码包,但是这个代码包又有更新了,那么这时候可以直接用 -u 标记来更新本地的对应的代码包。如果不加这个 -u 标记,执行 go get 一个已有的代码包,会发现命令什么都不执行。只有加了 -u 标记,命令会去执行 git pull 命令拉取最新的代码包的最新版本,下载并安装。

其他

go 复制代码
func main() {
 x := 0x12345678
 p := unsafe.Pointer(&x) // *int -> Pointer
 n := (*[4]byte)(p) // Pointer -> *[4]byte
 for i := 0; i  Pointer -> uintptr
 p += unsafe.Offsetof(d.x) // uintptr + offset
  p2 := unsafe.Pointer(p) // uintptr -> Pointer
 px := (*int)(p2) // Pointer -> *int
 *px = 200 // d.x = 200
 fmt.Printf("%#v\n", d)
}

输出:

php 复制代码
struct { s string; x int }{s:"abc", x:200}

注意:GC 把 uintptr 当成普通整数对象,它⽆法阻⽌ "关联" 对象被回收。

自定义类型

命名类型:bool、int、string
未命名类型: array、slice、map
目标类型是未命名类型会发生隐式转换否则必须显式转换,例如

go 复制代码
type bigInt int64
var a bigInt = 12
var b int64 = a //报错 var b int64 = int64(a) 

type FooSlice []uint
var a FooSlice = FooSlice{1, 2, 3}
var b []uint = a  // 正常运行

运算符 ^取反

转换

go 复制代码
func main() {
   var v int64 = 12              //默认10进制
   s2 := strconv.FormatInt(v, 2) //10 转2进制
   fmt.Printf("%v\n", s2)
 
   s8 := strconv.FormatInt(v, 8)
   fmt.Printf("%v\n", s8)
 
   s10 := strconv.FormatInt(v, 10)
   fmt.Printf("%v\n", s10)
 
   s16 := strconv.FormatInt(v, 16) //10 yo 16
   fmt.Printf("%v\n", s16)
 
   var sv = "11"
   fmt.Println(strconv.ParseInt(sv, 16, 32)) // 16 to 10
   fmt.Println(strconv.ParseInt(sv, 10, 32)) // 10 to 10
   fmt.Println(strconv.ParseInt(sv, 8, 32))  // 8 to 10
   fmt.Println(strconv.ParseInt(sv, 2, 32))  // 2 to 10
 
}

编码url

为什么需要编码和解码

1.是因为当字符串数据以url的形式传递给web服务器时,字符串中是不允许出现空格和特殊字符的;
2.因为 url 对字符有限制,比如把一个邮箱放入 url,就需要使用 urlencode 函数,因为 url 中不能包含 @ 字符;
3.url转义其实也只是为了符合url的规范而已。因为在标准的url规范中中文和很多的字符是不允许出现在url中的。

哪些字符是需要转化的呢?

1. ASCII 的控制字符

这些字符都是不可打印的,自然需要进行转化。

2. 一些非ASCII字符

这些字符自然是非法的字符范围。转化也是理所当然的了。

3. 一些保留字符

很明显最常见的就是“&”了,这个如果出现在url中了,那你认为是url中的一个字符呢,还是特殊的参数分割用的呢?

4. 就是一些不安全的字符了。

例如:空格。为了防止引起歧义,需要被转化为“+”。
明白了这些,也就知道了为什么需要转化了,而转化的规则也是很简单的。

按照每个字符对应的字符编码,不是符合我们范围的,统统的转化为%的形式也就是了。自然也是16进制的形式。

5.和字符编码无关

通过urlencode的转化规则和目的,我们也很容易的看出,urleocode是基于字符编码的。同样的一个汉字,不同的编码类型,肯定对应不同的urleocode的串。gbk编码的有gbk的encode结果。
apache等服务器,接受到字符串后,可以进行decode,但是还是无法解决编码的问题。编码问题,还是需要靠约定或者字符编码的判断解决。
因此,urleocode只是为了url中一些非ascii字符,可以正确无误的被传输,至于使用哪种编码,就不是encode所关心和解决的问题了。
编码问题,不是urlencode所要解决的。

golang之UrlEncode编码/UrlDecode解码

golang 复制代码
package main

import(
    "fmt"
    "net/url"
)

func main()  {
    var urlStr string = "傻了吧:%:%@163& .html.html"
    escapeUrl := url.QueryEscape(urlStr)
    fmt.Println("编码:",escapeUrl)

    enEscapeUrl, _ := url.QueryUnescape(escapeUrl)
    fmt.Println("解码:",enEscapeUrl)
}

输出

编码: %E5%82%BB%E4%BA%86%E5%90%A7%3A%25%3A%25%40163%26+.html.html
解码: 傻了吧:%:%@163& .html.html

原文地址: golang之UrlEncode编码/UrlDecode解码

defer

Golang中的defer语句用于延迟函数的调用,每次 defer 都会把一个函数压入栈中,函数返回前再把延迟的函数取出并执行。Golang 中的 defer 可以帮助我们处理容易忽略的问题,如资源释放、连接关闭等。

关键字 return 不是一个原子操作,实际上 return 只代理汇编指令 ret,即将跳转程序执行。比如语句 return i,实际上分两步进行,即将 i 值存入栈中作为返回值,然后执行跳转,而 defer 的执行时机正是跳转前,所以说 defer 执行时还是有机会操作返回值的。

defer函数在return前执行

匿名返回值,返回字面量,不会被影响

复制代码
func foo() int {
	i := 0
	defer func() {
		i++
	}()
	return i  // 0 匿名返回值
}
func gee() int {
	i := 0
	defer func() {
		i++
	}
	return 0 // 0 字面量
}

具名返回值,会被影响

复制代码
func bar() (result int) {
	defer func() {
		result++
	}()
	return result // 1 这里return 0或者result都会返回1
}

判断文件是否存在

go 复制代码
func FileExist(path string) bool {
  _, err := os.Lstat(path)  // Stat
  return !os.IsNotExist(err)
}
go 复制代码
func FileExist(path string) bool {
  err := syscall.Access(path, syscall.F_OK)
  return !os.IsNotExist(err)
}

syscall.Access是针对posix平台的。不能在windows上使用

正则

  • (re) numbered capturing group

  • (?Pre) named & numbered capturing group

  • (?:re) non-capturing group

  • (?flags) set flags within current group; non-capturing

  • (?flags:re) set flags during re; non-capturing

Flag syntax is xyz (set) or -xyz (clear) or xy-z (set xy, clear z). The flags are:

  • i case-insensitive (default false)

  • m multi-line mode: ^ and $ match begin/end line in addition to begin/end text (default false)

  • s let . match \

  • (default false)

  • U ungreedy: swap meaning of x* and x*?, x+ and x+?, etc (default false)

例如不区分大小写

复制代码
r := regexp.MustCompile(`(?i)CaSe`)

交换顺序

复制代码
a := 1
b := 2
a ^= b
b ^= a
a ^= b
复制代码
a := 1
b := 2
a, b = b, a

获取正在运行的函数名

复制代码
func getFuncName() string {
	pc, _, _, _ := runtime.Caller(1)
	return runtime.FuncForPC(pc).Name()
}
复制代码
func getFuncName()string{
    pc := make([]uintptr,1)
    runtime.Callers(2,pc)
    f := runtime.FuncForPC(pc[0])
    return f.Name()
}

泛型

go >= 1.18

复制代码
type Number interface {
	uint | uint8 | uint16 | uint32 | uint64  # 联合类型
}

func sum[K comparable, V Number](s map[K]V) (sum uint64) {
	for _, value := range s {
		sum += uint64(value)
	}
	return
}

fmt.Println(sum[int, uint64](map[int]uint64{1: 1, 2: 3}))

为 K 类型参数指定类型约束 comparable。专门针对此类情况,comparable 在 Go 中预先声明了约束。它允许任何类型的值可以用作比较运算符 == 和的操作数 !=。Go 要求 map keys 具有可比性。所以声明 K as comparable 是必要的,这样你就可以 K 在 map 变量中用作键。它还确保调用代码对 map keys 使用允许的类型。

打印panic调用堆栈

go 复制代码
    defer func() {
        if err := recover(); err != nil {
            var buf [4096]byte
            n := runtime.Stack(buf[:], true)
            fmt.Println(string(buf[:n]))
        }
    }()

判断是否纯数字

提供了两个方法,一个逐字符判断,一个使用正则

复制代码
func IsNumeric(str string) bool {
	for _, v := range str {
		if !unicode.IsNumber(v) {    // IsNumber判断是否为一个数字字符 (类别 N)
		// if !unicode.IsDigit(v) { // IsDigit 判断 r 是否为一个十进制的数字字符
			return false
		}
	}
	return true
}

# https://www.coder.work/article/193576

func IsNumericP(str string) bool {
	// 使用正则判断是不是数字字符串
	return regexp.MustCompile(`^[0-9]+$`).MatchString(str)
}

示例

复制代码
func main() {
	str := "12345"
	str1 := "123rr"
	// fmt.Println(IsNumeric(str), IsNumeric(str1)) // true, false
	fmt.Println(IsNumericP(str), IsNumericP(str)) // true, false
}

终止协程

1.手动终止

调用 runtime.Goexit() 来手动终止协程

goroutine作为Golang并发的核心,我们不仅要关注它们的创建和管理,当然还要关注如何合理的退出这些协程,不(合理)退出不然可能会造成阻塞、panic、程序行为异常、数据结果不正确等问题。

2.1 使用for-range退出

for-range是使用频率很高的结构,常用它来遍历数据,range能够感知channel的关闭,当channel被发送数据的协程关闭时,range就会结束,接着退出for循环。
它在并发中的使用场景是:当协程只从1个channel读取数据,然后进行处理,处理后协程退出。下面这个示例程序,当in通道被关闭时,协程可自动退出。

算法

排序

冒泡

复制代码
package main

import "fmt"

func BubbleSort(list []int) []int {
	for i := 0; i  list[i] {
				list[i], list[j] = list[j], list[i]
			}
		}
	}
	return list
}

func main() {
	list := []int{2, 4, 2, 6, 1, 3, 9}
	fmt.Println(BubbleSort(list))
}

最大公约数 (GCD)

go 复制代码
func gcd(x, y int) int {
	for y != 0 {
		x, y = y, x%y
	}
	return x
}

一个数中二进制1bit的个数

go 复制代码
package main

import "fmt"

// pc[i] is the population count of i.
var pc [256]byte

func init() {
	for i := range pc {
		pc[i] = pc[i/2] + byte(i&1)
	}
}

// PopCount returns the population count (number of set bits) of x.
func PopCount(x uint64) int {
	return int(pc[byte(x>>(0*8))] +
		pc[byte(x>>(1*8))] +
		pc[byte(x>>(2*8))] +
		pc[byte(x>>(3*8))] +
		pc[byte(x>>(4*8))] +
		pc[byte(x>>(5*8))] +
		pc[byte(x>>(6*8))] +
		pc[byte(x>>(7*8))])
}

func main() {
	//fmt.Println(PopCount(12))
	fmt.Println([]byte("Hello"))
}

控制协程数量

go 复制代码
package main  
  
import (  
    "fmt"  
    "math"    "runtime"    "sync")  
  
var wg = sync.WaitGroup{}  
  
func main() {  
    goroutineNum := 5  
    taskNum := math.MaxInt32  
  
    ch := make(chan struct{})  
  
    // 先开辟好协程 等待处理数据  
    for i := 0; i < goroutineNum; i++ {  
       go func() {  
          for data := range ch {  
             fmt.Println(" i == ", data, "func == ", runtime.NumGoroutine())  
             wg.Done()  
          }  
       }()  
    }  
  
    // 分发事项  
    for i := 0; i < taskNum; i++ {  
       wg.Add(1)  
       ch <- struct{}{}  
    }  
  
    wg.Wait()  
}

笔记

作用域:下面代码两个a的地址是不同的

go 复制代码
a := 123
{
	a := 345
}

常量:可以是编译期可以确定的数字,字符串,布尔值,len,cap, unsafe.Sizeof 等编译期可确定的值

go 复制代码
const (
	a, b = 1, 2
	c, d
)

其中a = c = 1, b = d = 2

枚举(iota):定义常量组从0开始按行计数的自增枚举值,同一常量组中,可以提供多个iota,各自增长,如果被打断,则需要显式恢复

go 复制代码
const (
	// 多个iota
	A, B = iota, iota << 10 // 0, 0 << 10
	C, D                    // 1, 1 << 10
	// 被打断,显式恢复
	E, F = 1, 2
	M, L = iota, iota << 10 // 3,包含E,F所在行
)

引用类型包括slice,map,channel,除了需要申请内存,还需要初始化相关属性

内置函数 new 计算类型⼤⼩,为其分配零值内存,返回指针。⽽ make 会被编译器翻译 成具体的创建函数,由其分配内存和初始化成员结构,返回对象⽽⾮指针

字符串是不可变值类型,内部使用指针指向UTF-8字节数组,不能使用&a[i]的形式去地址,不能修改字节数组,使用反引号定义不被转义的原始字符串

下面代码会原样输出

go 复制代码
a := `Hello
\n\b\r\x00
World;
`
println(a)

连接字符串使用+,如果换行,则+应在上一行末尾,否则编译错误。单引号字符常量表⽰ Unicode Code Point,⽀持 \uFFFF、\U7FFFFFFF、\xFF 格式。 对应 rune 类型,UCS-4

go 复制代码
func main() { 
	fmt.Printf("%T\n", 'a') 
	var c1, c2 rune = '\u6211', '们'
	println(c1 == '我', string(c2) == "\xe4\xbb\xac") 
}

要修改字符串,可先将其转换成 []rune 或 []byte,完成后再转换为 string。⽆论哪种转 换,都会重新分配内存,并复制字节数组,遍历字符串也有区别

go 复制代码
a := "你好啊"  
for i := 0; i < len(a); i++ {  
   fmt.Printf("%c ", a[i])   // 逐字节
}  
for _, v := range a {  
   fmt.Printf("%c", v)   // 逐字符
}

指针:指针不支持加减法运算,可以在unsafe.Pointer和任意类型指针间转换

go 复制代码
a := 0x12345678  
p := unsafe.Pointer(&a)  
n := (*[4]byte)(p)  
for i := 0; i < len(n); i++ {  
   fmt.Printf("%X", n[i])  
}  
// 78563412

函数返回局部变量指针是安全的,编译器会根据需要将其分配在GC Heap上。Pointer转为uintptr可以变相实现指针运算

go 复制代码
a := struct {  
   Name string  
   X    int  
}{"LiBai", 1}  
p := uintptr(unsafe.Pointer(&a))  
p += unsafe.Offsetof(a.X)  
p2 := unsafe.Pointer(p)  
px := (*int)(p2)  
*px = 200  
fmt.Printf("%#v\n", a)  
// struct { Name string; X int }{Name:"LiBai", X:200}

注意:GC 把 uintptr 当成普通整数对象,它⽆法阻⽌ "关联" 对象被回收

自定义类型

go 复制代码
type (  
   AInt8 = uint8   // 类型别名
   BInt8 uint8  // 新的类型,和uint8有相同数据存储结构,不会持有原类型的信息
)

位运算

  • 0110 & 1011 = 0010 AND 都为 1。

  • 0110 | 1011 = 1111 OR ⾄少⼀个为 1。

  • 0110 ^ 1011 = 1101 XOR 只能⼀个为 1。

  • 0110 &^ 1011 = 0100 AND NOT 清除标志位。

标志位操作

  • a := 0

  • a |= 1 << 2 // 0000100: 在 bit2 设置标志位。

  • a |= 1 << 6 // 1000100: 在 bit6 设置标志位

  • a = a &^ (1 << 6) // 0000100: 清除 bit6 标志位。

不支持运算符重载, ++/--是语句而不是表达式

初始化:初始化值以 "," 分隔。可以分多⾏,但最后⼀⾏必须以 "," 或 "}" 结尾。

go 复制代码
a := []int {
	1,
	2,
}

控制流:代码块左⼤括号必须在条件表达式尾部

go 复制代码
if a == true {
	// do something
}

range:会复制对象

go 复制代码
a := [3]int{0, 1, 2}  
for i, v := range a {  
   if i == 0 {  
      a[1], a[2] = 999, 999  
      fmt.Println(a) // [0 999 999]  
   }  
  
   a[i] = v + 100 // 使用复制品中取出的v修改原数组 -> [100 101 102]}  
fmt.Println(a)

建议改成引用类型,底层数据不会被复制

go 复制代码
s := []int{1, 2, 3, 4, 5}  
for i, v := range s { // 复制 struct slice { pointer, len, cap }。  
   if i == 0 {  
      s = s[:3]  // 对 slice 的修改,不会影响 range。  
      s[2] = 100 // 对底层数据的修改。  
   }  
   println(i, v)  
}  
fmt.Println(s) // [1 2 100]

另外两种类型map,channel是指针包装,而不像slice是struct

分支语句switch:可使用fallthrough继续下一个分支,但是不判断条件

go 复制代码
a := 1
switch a {
	case 1: 
		fmt.Println("1")
		fallthrough
	case 0: 
		fmt.Println("0")
}

// 1 0

跳转goto:标签区分大小写,未使用的标签引发错误

defer:允许延迟调用修改返回值

go 复制代码
func add(x, y int) (z int) {  
   defer func() {  
      println(z) // 输出: 203  
   }()  
   z = x + y  
   return z + 200 // 执⾏顺序: (z = z + 200) -> (call defer) -> (ret)  
}

闭包复制原对象指针

go 复制代码
func test() func() {  
   x := 100  
   fmt.Printf("x (%p) = %d\n", &x, x)  
   return func() {  
      fmt.Printf("x (%p) = %d\n", &x, x)  
   }  
}

多个defer注册,会按照FIFO次序执行,即使函数或者调用出错也会执行

go 复制代码
defer func() {  
   panic("aaa")  
}()  
defer func() {  
   fmt.Println("bbb")  
}()  
fmt.Println("ccc")

// ccc
// bbb                                         
// panic: aaa

延迟调⽤参数在注册时求值或复制,可⽤指针或闭包 "延迟" 读取

go 复制代码
x, y := 10, 20  
defer func(i int) {  
   println("defer:", i, y) // y 闭包引⽤  
}(x) // x 被复制  
x += 10  
y += 100  
println("x =", x, "y =", y)

延迟调用中的错误可被后续延迟调用捕获,但仅最后一个错误可以被捕获

数组:值类型,赋值和传参会复制整个数组可以使用==/!= 运算

go 复制代码
a := [3]int{1, 2}           // 未初始化元素值为 0。  
b := [...]int{1, 2, 3, 4}   // 通过初始化值确定数组⻓度。  
c := [5]int{2: 100, 4: 200} // 使⽤索引号初始化元素。  
d := [...]struct {  
   name string  
   age  uint8  
}{  
   {"user1", 10}, // 可省略元素类型。  
   {"user2", 20}, // 别忘了最后⼀⾏的逗号。  
}

值拷贝可能会造成性能问题,建议使用slice或者数组指针

slice:引用类型,自身是结构体,值拷贝传递。len,cap

go 复制代码
data := [...]int{1, 2, 3, 4, 5, 6, 7, 8}  
slice := data[1:4:5]  
fmt.Println(slice) // [2 3 4]

创建表达式使用的是元素的索引,个人理解: data[index:len:cap],取下标为index,长度是len的范围内的元素,例如data[1:4:5] index 1为2,len为4 取到第四个元素,即[2,3,4]

go 复制代码
data := [...]int{4, 3, 5, 6, 5, 7, 3, 2, 1}  
s := data[3:7]  
fmt.Println(s) // [6 5 7 3]

元素读写操作的是底层数组

append 向slice追加,当append超过slice的cap时会重新分配底层数组并复制数据

go 复制代码
s := make([]int, 1, 2)  
fmt.Printf("%p\n", &s[0])  // 0xc00001a0a0  
s = append(s, 1, 2, 3, 4, 5)  
fmt.Printf("%p\n", &s[0])  // 0xc000012420

通常以 2 倍容量重新分配底层数组。在⼤批量添加数据时,建议⼀次性分配⾜够⼤的空 间,以减少内存分配和数据复制开销。或初始化⾜够⻓的 len 属性,改⽤索引号进⾏操 作。及时释放不再使⽤的 slice 对象,避免持有过期数组,造成 GC ⽆法回收。

copy 函数 copy 在两个 slice 间复制数据,复制⻓度以 len ⼩的为准。两个 slice 可指向同⼀ 底层数组,允许元素区间重叠

go 复制代码
data := [...]int{0, 1, 2, 3, 4, 5, 6, 7, 8, 9}  
s := data[8:]  
s2 := data[:5]  
copy(s2, s) // dst:s2, src:s  
fmt.Println(s2)   // [8 9 2 3 4]
fmt.Println(data) // [8 9 2 3 4 5 6 7 8 9]
快来和小猫聊天吧~