Go数组、字符串、切片原理和优化

数组

1、赋值和传参

数组本身的赋值和传参都是以整体复制的方式处理的,所以为深拷贝,在函数内对数组进行操作,不会影响原数组。

package main

import (
	"fmt"
)

func forRange(a [3]int){
	for k,v := range a{
		fmt.Printf("%d:%d\n", k, v)
	}
	a[2] = 9
	fmt.Println(a)
}

func main()  {
	a := [...]int{5, 2, 3}
	fmt.Println(a)
	forRange(a)
	fmt.Println(a)
}
[5 2 3]
0:5
1:2
2:3
[5 2 9]
[5 2 3]

当一个数组变量被赋值或者被传递的时候,实际上会复制整个数组,如果数组较大的花,数组的赋值也会有较大开销。为了避免复制数组带来的开销,可以传递一个指向数组的指针,然后对指针进行遍历和操作,此时需要注意,由于传的是指针,所以此时为浅拷贝,如果在函数内改变数组对元素,会影响原数组。

package main

import (
	"fmt"
)

func forRange(b *[]int){
	a := *b
	for k,v := range a{
		fmt.Printf("%d:%d\n", k, v)
	}
	a[2] = 9
}

func main()  {
	a := []int{5, 2, 3}
	fmt.Println(a)
	b := &a
	forRange(b)
	fmt.Println(a)
}
[5 2 3]
0:5
1:2
2:3
[5 2 9]

2、长度为0的空数组

长度为0的空数组在内存中不占用空间,使用情况很少,但是可以用于强调某种特有类型的操作时避免分配额外的内存空间,比如用于通道的同步操作:

chan := make(chan [0]struct{})
go func() {
    chan <- struct{}{}
<-chan

字符串

1、结构

reflect.StringHeader

type StringHeader struct {
    Data uintptr
    Len  int
}

字符串的结构由两个信息组成:第一个是字符串指向的底层字节数组,第二个是字符串的字节长度。字符串实际是一个结构体,因此字符串的赋值操作也就是reflect.StringHeader结构体的复制过程,并不会涉及底层字节数组的复制。

一般我们都用len函数返回字符串的长度,也可以通过reflect.StringHeader结构访问字符串的长度,但是不推荐这种不安全的方法

s := 'hello, world'
fmt.Println("len:", (*reflect.StringHeader)(unsafe.Pointer(&s)).Len)

2、ASCII码组成[]byte数组,byte=uint8,中文字符utf8编码,组成[]rune数组,[]rune=[]int32一个汉字占3个字节,并且中文的错误编码不会向后扩散。

3、string和[]byte的相互转换

func string2bytes(s string) []byte {
    p := make([]byte, len(s))
    for i := 0; i < len(s); i++ {
        c := s[i]
        p[i] = c
    }
    return p
}

func bytes2string(s []byte) (p string) {
    data := make([]byte, len(s))
    for i, c := range s {
        data[i] = c
    }

    hdr := (*reflect.StringHeader)(unsafe.Pointer(&p))
    hdr.Data = uintptr(unsafe.Pointer(&data[0]))
    hdr.Len = len(s)

    return p
}

4、string和[]rune相互转换

func string2runes(s []byte) []rune {
	var p []int32
	for len(s) > 0 {
		r, size := utf8.DecodeRune(s)
		p = append(p, int32(r))
		s = s[size:]
	}
	return []rune(p)
}


func runes2string(s []int32) string {
	var p []byte
	buf := make([]byte, 1)
	for _, r := range s {
		n := utf8.EncodeRune(buf, r)
		p = append(p, buf[:n]...)
	}
	return string(p)
}

切片

1、结构

type SliceHeader struct {
    Data uintptr    //数据内存地址
    Len  int    //实际使用长度
    Cap  int    //内存空间最大容量
}

2、添加元素

append函数可以在切片的尾部追加N个元素,在容量不足的情况下,会导致内存重新分配

var a []int
a = append(a, 1)
a = append(a, 2, 3)
a = append(a , []int(1, 2, 3))

开头添加元素都会导致内存的重新分配,而且会导致已有的元素全部重新复制一次,因此在开头添加元素的性能一般比从尾部追加元素的性能差很多。

a := []int{1, 2, 3}
a = append(6, a...)    //开头添加1个元素
s = append([]int{3, 2, 1}, a...)    //开头添加一个切片

3、插入元素

func InsertSliceByIndex(slice interface{}, index int, value interface{}) (interface{}, error) {
	v := reflect.ValueOf(slice)
	if v.Kind() != reflect.Slice {
		return slice, errors.New("not slice")
	}
	if index < 0 || index > v.Len() || reflect.TypeOf(slice).Elem() != reflect.TypeOf(value){
		return slice, errors.New("index error")
	}
	if index == v.Len() {
		return reflect.Append(v, reflect.ValueOf(value)).Interface(), nil
	}
	v = reflect.AppendSlice(v.Slice(0, index + 1), v.Slice(index, v.Len()))
	v.Index(index).Set(reflect.ValueOf(value))
	return v.Interface(), nil
}

4、删除元素


func DeleteSliceByPos(slice interface{}, index int) (interface{}, error) {
	v := reflect.ValueOf(slice)
	if v.Kind() != reflect.Slice {
		return slice, errors.New("not slice")
	}
	if v.Len() == 0 || index < 0 || index > v.Len() - 1 {
		return slice, errors.New("index error")
	}
	return reflect.AppendSlice(v.Slice(0, index), v.Slice(index+1, v.Len())).Interface(), nil
}

5、避免切片内存泄漏

由于切片的复制操作是浅拷贝,并不会复制底层的数据,底层的数据会被保存在内存中,直到它不再被饮用。但是有时候可能会因为一个内存饮用导致底层整个切片处于被使用的状态,这会延迟垃圾回收器对底层数组对回收。

例如下面代码,在文件中给搜索第一个出现对手机号后返回:

func FindPhone(filename string) []byte {
    b, _ := ioitil.ReadFile(filename)
    return regexp.MustComplie("^1[3|5|7|8|][\d]{9}&").Find(b)
}

这段代码返回的[]byte指向保存整个文件的数组,由于切片饮用了整个原始数组,导致垃圾回收器不能及时释放底层数组的空间。解决方法是将需要的数据处复制到一个新的切片中,数据的传值是Go的一个哲学,虽然传值有一定的代价,但是换取的好处是切断了对原始数据的依赖。

func FindPhone(filename string) []byte {
    b, _ := ioitil.ReadFile(filename)
    b = regexp.MustComplie("^1[3|5|7|8|][\d]{9}&").Find(b)
    return append([]byte[], b...)
}

类似的问题在删除切片元素时可能会遇到,假设切片里存放的是指针对象,那么下面删除末尾的元素后,被删除的元素依然被切片底层数组引用,从而导致不能及时被垃圾回收器回收(这要依赖回收器的实现方式)

a = a[:len(a)-1] //被删除的最后一个元素依然被使用,可会影响回收

保险的方式是先将指向需要回收内存的指针设置为nil,保证垃圾回收器可以发现待回收的对象,然后再进行切片的删除操作:

a[len(a)-1] = nil
a = a[:len(a)-1]
发布了226 篇原创文章 · 获赞 31 · 访问量 15万+

猜你喜欢

转载自blog.csdn.net/why444216978/article/details/104341220