首页 > 其他分享 >02_go语言的变量和常量

02_go语言的变量和常量

时间:2023-10-08 13:34:28浏览次数:50  
标签:02 slice 常量 int value key func go string

1. 内置类型和内置函数

1.1 内置类型

  1. 总体上分为四类: image-20220328082843528

其中数字类型主要包括如下,uint8 就是 byte、int16 相当于C语言的short型、int64相当于C语言的long型

image-20220328082926453
  1. 也可以总体分为值类型、引用类型

image-20220921155639339

1.2 内置函数

image-20220921160041056

1.3 值类型和引用类型

func main() {
    var a *int
    *a = 100
    fmt.Println(*a)

    var b map[string]int
    b["测试"] = 100
    fmt.Println(b)
}

​ 该代码会引发 panic,因为在 Go 语言中对于引用类型的变量,我们使用时不仅要申明它,还需要为它分配内存空间,否则我们的值就没法存储。对于值类型就不需要分配内存,因为他们在申明时就默认分配好了内存空间。通过 new、make 来为引用类型分配内存空间。注意指针也为引用类型。

1.4 关于new 和 make

func new(Type) *Type :new 函数的函数签名,Type表示类型,*Type表示类型指针,new函数返回一个指向该类型内存地址的指针。

func make(t Type, size ...IntegerType) Type :make函数也是用于内存分配的,区别于 new,它只用于 slice、map、chan的内存创建。

func main() {
    // 通过 new 分配内存
    a := new (int)
    b := new (bool)
    fmt.Printf("%T\n", a) // *int
    fmt.Printf("%T\n", b) // *bool
    fmt.Println(*a)       // 0
    fmt.Println(*b)       // false
    // 通过 make 分配内存
    var b map[string]int
    b = make(map[string]int, 10)
    b["测试"] = 100
    fmt.Println(b)
}
  • new 和 make 都是用来做内存分配的,make 只用于 slice、map、channel 的初始化,返回的是这三个引用类型本身;
  • new 用于类型的内存分配,并且内存对应的值为类型零值,返回指向类型的指针。

2. strings包

  • 是否存在某个字符或者字串
// 子串substr在s中,返回true
func Contains(s, substr string) bool
// chars中任何一个Unicode代码点在s中,返回true
func ContainsAny(s, chars string) bool
// Unicode代码点r在s中,返回true
func ContainsRune(s string, r rune) bool


// ContainsAny 中第二个参数中任意一个字符如果在第一个参数中出现返回true
fmt.Println(strings.ContainsAny("team", "i"))              //false
fmt.Println(strings.ContainsAny("failure", "u & i"))       //true
fmt.Println(strings.ContainsAny("in failure", "s g"))      //false
fmt.Println(strings.ContainsAny("foo", ""))                //false
fmt.Println(strings.ContainsAny("", ""))                   //false
  • 字符或字串在字符串中出现的位置
//返回子串sep在字符串s中第一次出现的索引值,不在的话返回-1.
func Index(s, sep string) int
//chars中任何一个Unicode代码点在s中首次出现的位置,不存在返回-1
func IndexAny(s, chars string) int
//查找字符 c 在 s 中第一次出现的位置,其中 c 满足 f(c) 返回 true
func IndexFunc(s string, f func(rune) bool) int   //rune类型是int32别名,UTF-8字符格式编码。
//返回字符c在s中第一次出现的位置
func IndexByte(s string, c byte) int   //byte是字节类型
// Unicode 代码点 r 在 s 中第一次出现的位置
func IndexRune(s string, r rune) int

//查找最后一次出现的位置
func LastIndex(s, sep string) int
func LastIndexByte(s string, c byte) int
func LastIndexAny(s, chars string) int
func LastIndexFunc(s string, f func(rune) bool) int

// 因为y的unicode大于u,所以返回y的位置
fmt.Println(strings.IndexFunc("studygolang", func(r rune) bool {
    if r > 'u' {
        return true
    } else {
        return false
    }
}))
  • 子串出现次数
func Count(s, sep string) int    //子串在s中的出现次数
  • 字符串是否有某前缀或后缀
// s 中是否以 prefix 开始
func HasPrefix(s, prefix string) bool {
    return len(s) >= len(prefix) && <strong><span style="color:#ff0000;">s[0:len(prefix)]</span></strong> == prefix
}
// s 中是否以 suffix 结尾
func HasSuffix(s, suffix string) bool {
    return len(s) >= len(suffix) && s[len(s)-len(suffix):] == suffix
}
  • 字符串转换
func ToUpper(s string) string
func ToLower(s string) string
func ToTitle(s string) string  //首字母大写
  • 比较
func Compare(a, b string) int  //返回不相等-1或者  相等0

func EqualFold(s, t string) bool  //忽略大小写
  • 清理
func Trim(s string, cutset string) string                 //去除字符串的头尾子字符串
func TrimLeft(s string, cutset string) string
func TrimRight(s string, cutset string) string
func TrimPrefix(s, prefix string) string
func TrimSuffix(s, suffix string) string
func TrimFunc(s string, f func(rune) bool) string         //函数
func TrimLeftFunc(s string, f func(rune) bool) string
func TrimRightFunc(s string, f func(rune) bool) string
  • 拆合函数
func Fields(s string) []string  // 用空格分隔字符串s
func FieldsFunc(s string, f func(rune) bool) []string  //用空格分隔字符串s
func Split(s, sep string) []string { return genSplit(s, sep, 0, -1) }
func SplitAfter(s, sep string) []string { return genSplit(s, sep, len(sep), -1) }
func SplitN(s, sep string, n int) []string { return genSplit(s, sep, 0, n) }
func SplitAfterN(s, sep string, n int) []string { return genSplit(s, sep, len(sep), n) }

// 其中 Split和SplitN等价; SplitAfter和SplitAfterN等价
// Split和 SplitAfterN 区别在于,后者会保留sep
fmt.Printf("%q\n", strings.Split("foo,bar,baz", ","))         //  ["foo" "bar" "baz"]
fmt.Printf("%q\n", strings.SplitAfter("foo,bar,baz", ","))    //  ["foo," "bar," "baz"]
// SplitN 的N决定了分隔后的字符总数
fmt.Printf("%q\n", strings.SplitN("foo,bar,baz", ",", 2))     // ["foo" "bar,baz"]
func Join(a []string, sep string) string   // 按照sep组合字符数组a
// fmt.Println(Join([]string{"name=xxx", "age=xxx"}, "&"))
  • 替换
// 用 new 替换 s 中的 old,一共替换 n 个。
// 如果 n < 0,则不限制替换次数,即全部替换
func Replace(s string, old string, new string, n int) string       // -1表示全部替换
func Map(mapping func(rune) rune, s string) string   //满足函数实现的进行替换

// 清理字符串中单词之间的空格
        fun := func(c rune) rune {
            if c == ' ' {
                return 0
            } else {
                return c
            }
        }
        input := "GeeksforGeeks is a computer science portal."
        fmt.Println(strings.Map(fun, input))

3. strconv包

这里的基本数据类型包括布尔、整型(包括无符号、二进制、八进制、十六进制、十进制)、浮点型。

1. 采用 strconv 包进行数据类型转换

先看看 strconv 包进行数据类型转换时的错误处理,该包定义了两个 error 类型的变量,ErrRange和 ErrSyntax。其中ErrRange表示值超过了类型能够表示的最大范围,比如将"128" 转化为 int8就会出现这个错误(int8 最大能够表示127);ErrSyntax表示语法错误,比如将 "" 转化为 int 类型就会返回这个错误。

2. 字符串转化为整型

func ParseInt(s string, base int, bitSize int) (i int64, err error)     //转化为有符号
func ParseUint(s string, base int, bitSize int) (n uint64, err error)   //转化为无符号
func Atoi(s string) (i int, err error)

Atoi 是 ParseInt 的便捷版,表示为 ParseInt(s, 10, 0);

参数 base 表示字符串按照给定的进制解释,一般取 2~36,如果base 取0则会根据字符串的前缀来确定base的值,"0x"表示16进制;"0"表示8进制;否则为10进制;

参数 bitSize 表示整数取值范围,或者说整数的具体类型,取值 0、8、16、32、64分别表示int、int8、int16、int32、int64

// 举例
func Teststrconv() {
	// 字符串转整数
	var numStr string = "1234"
	//var num, err = strconv.Atoi(numStr)
	var num, err = strconv.ParseInt(numStr, 10, 8)      //最终会输出127,因为int8最大表示127
	if err != nil {
		fmt.Println("出错了")
	}
	fmt.Println(num)

	// 整数转字符串
	var str string
	str = strconv.FormatInt(666, 10)
	fmt.Println(str)
}

3. 整型转化为字符串

func FormatUint(i uint64, base int) string    // 无符号整型转字符串
func FormatInt(i int64, base int) string    // 有符号整型转字符串
func Itoa(i int) string

fmt.Sprintf("%d", 127)   // 调用fmt包进行数据类型转换

Itoa 内部调用 FormatInt(i, 10) 来实现。

注意性能上 Itoa 远高于 Sprintf,所以不要通过 string(65) 这种方式来将整数转换为字符串,它实际上会得到 ASCCII 值为65的字符,即'A'.

4. 字符串和布尔之间转换

// 接受 1, t, T, TRUE, true, True, 0, f, F, FALSE, false, False 等字符串;
// 其他形式的字符串会返回错误
func ParseBool(str string) (value bool, err error)
// 直接返回 "true" 或 "false"
func FormatBool(b bool) string
// 将 "true" 或 "false" append 到 dst 中
// 这里用了一个 append 函数对于字符串的特殊形式:append(dst, "true"...)
func AppendBool(dst []byte, b bool)

5. 字符串和浮点数之间转换

func ParseFloat(s string, bitSize int) (f float64, err error)
func FormatFloat(f float64, fmt byte, prec, bitSize int) string
func AppendFloat(dst []byte, f float64, fmt byte, prec int, bitSize int)

fmt 表示转换形式,包括 'e'、'E'、'f'、'G'、'g';

prec 表示有效数字,对 fmt='b' 无效,对于 'e'、'E'、'f' 有效数字用于小数点之后的位数;对于 'G'、'g',有效数字则是对所有的有效数字

bitSize 表示转换精度,有0、8、16、64可取。

4. 数组

  • 数组声明
var variable_name [SIZE] variable_type
// 声明一个空数组
var arr []int
  • 数组初始化
balance := [5]float32{1000.0, 2.0, 3.4, 7.0, 50.0}
// 长度不确定,用 ... 代替数组长度
balance := []int{1,2,3,4}  或者   balance := [...]int{1,2,3,4}
// 指定下标进行初始化
balance := [5]int{2:1,3:2,4:3}
  • 杨辉三角形
package main

import "fmt"

func main() {
	trangle(10)
}

func trangle(n int) {
	var item []int
	for i := 1; i < n; i++ {
		item_len := len(item)
		if item_len == 0 {
			item = append(item, 1)
		} else {
			temp_s := []int{1}
			for j := 0; j < item_len-1; j++ {
				temp_s = append(temp_s, item[j]+item[j+1])
			}
			temp_s = append(temp_s, 1)
			item = temp_s
		}
		fmt.Println(item)
	}
}
  • 注意数组作为参数时是值拷贝,切片作为参数是是传递引用,如果要用数组传参,应该使用数组指针
//数组引用传递
func changeArr(arr *[8]int) {
	for k, v := range *arr {
		arr[k] = v * 2
	}
}

//切片引用传递
func changeSlice(slice []int) {
	for k, v := range slice {
		slice[k] = v * 2
	}
}

func main() {
	//arr := [8]int{}
	//for i := 0; i < 8; i++ {
	//	arr[i] = i
	//}
	//fmt.Println(arr)
	//changeArr(&arr)
	//fmt.Println(arr)
	slice := []int{1, 2, 3, 4, 5}
	fmt.Println(slice)
	changeSlice(slice)
	fmt.Println(slice)
}

5. 结构体

1. 基础使用

一个包中定义的结构体如果要被其他包引用,属性字段的首字母就必须大写;这样同时能够保证转化为json时各个属性字段能够成功转化。

type student struct {
	Name string
	Age  int
	Sex  bool
}

type people struct {
	Name string `json:"name"`
	Age  int    `json:"age"`
	Sex  bool   `json:"-"` //忽略该标记字段
}

func main {
	stu1 := student{"小明", 15, false}
    // 转换为json形式,注意结构体属性首字母必须大写,否则结果中不会出现
	if result, err := json.Marshal(&stu1); err == nil { 
		fmt.Println(string(result))
	}

	peo1 := people{"小王", 16, true}
    // 给属性添加json标签可以设定输出时的属性字段名称
	if result, err := json.Marshal(&peo1); err == nil { 
		fmt.Println(string(result))
	}
}

2. 匿名字段和内嵌结构体

结构体内部可以包含一个或多个匿名(内嵌)字段,即这些字段没有显式的名字,此时类型就是字段的名字。匿名字段本身可以是一个结构体类型,即结构体可以包含内嵌结构体

需要注意在一个结构体中对于每一种数据类型只能有一个匿名字段

type innerS struct {
	in1 int
	in2 int
}
type outerS struct {
	b int
	c float32
	int
	innerS
}

func Test02() {
	outer := new(outerS)
	outer.b = 5
	outer.c = 6.2
	outer.int = 2
	outer.innerS = innerS{1, 2}
}

6. 切片 Slice

1. 基础使用

// 定义切片
var identifier []type                             //类似于数组定义方式
var slice1 []type = make([]type, len ,capicity)   //make创建切片

// 切片初始化
s := []int{1,2,3}       //类似于定义无长度的数组
s := arr[startIndex, endIndex]  //将数组arr中从下标startIndex到endIndex-1的元素创建为一个新的切片

// append()函数和copy()函数
var numbers []int
printfSlice(numbers)
numbers = append(numbers, 1) //添加一个元素
printfSlice(numbers)
numbers = append(numbers, 1, 2, 3, 4) //添加多个元素
printfSlice(numbers)
numbers1 := make([]int, len(numbers), cap(numbers)*2) //make创建切片
copy(numbers1, numbers)                               //copy复制切片
printfSlice(numbers1)

func printfSlice(x []int) {
	fmt.Printf("len=%d cap=%d slice=%v\n", len(x), cap(x), x)
}
//输出如下
len=0 cap=0 slice=[]
len=1 cap=1 slice=[1]         
len=5 cap=6 slice=[1 1 2 3 4] 
len=5 cap=12 slice=[1 1 2 3 4]
  • 在基于原数组创建一个新的切片后,新切片的大小为切片中包括的元素个数,新切片的容量为原数组大小 - 切片中第一个元素的下标。

image-20220328143121435

  • 关于 append(list,[params]) 的底层实现
// $GOROOT/src/runtime/slice.go源码
func growslice(et *_type, old slice, cap int) slice {
    if raceenabled {
        callerpc := getcallerpc(unsafe.Pointer(&et))
        racereadrangepc(old.array, uintptr(old.len*int(et.size)), callerpc, funcPC(growslice))
    }
    if msanenabled {
        msanread(old.array, uintptr(old.len*int(et.size)))
    }

    if et.size == 0 {
        // 如果新要扩容的容量比原来的容量还要小,这代表要缩容了,那么可以直接报panic了。
        if cap < old.cap {
            panic(errorString("growslice: cap out of range"))
        }

        // 如果当前切片的大小为0,还调用了扩容方法,那么就新生成一个新的容量的切片返回。
        return slice{unsafe.Pointer(&zerobase), old.len, cap}
    }

  // 这里就是扩容的策略
    newcap := old.cap
    doublecap := newcap + newcap
    if cap > doublecap {
        newcap = cap
    } else {
        if old.len < 1024 {
            newcap = doublecap
        } else {
            for newcap < cap {
                newcap += newcap / 4
            }
        }
    }

    // 计算新的切片的容量,长度。
    var lenmem, newlenmem, capmem uintptr
    const ptrSize = unsafe.Sizeof((*byte)(nil))
    switch et.size {
    case 1:
        lenmem = uintptr(old.len)
        newlenmem = uintptr(cap)
        capmem = roundupsize(uintptr(newcap))
        newcap = int(capmem)
    case ptrSize:
        lenmem = uintptr(old.len) * ptrSize
        newlenmem = uintptr(cap) * ptrSize
        capmem = roundupsize(uintptr(newcap) * ptrSize)
        newcap = int(capmem / ptrSize)
    default:
        lenmem = uintptr(old.len) * et.size
        newlenmem = uintptr(cap) * et.size
        capmem = roundupsize(uintptr(newcap) * et.size)
        newcap = int(capmem / et.size)
    }

    // 判断非法的值,保证容量是在增加,并且容量不超过最大容量
    if cap < old.cap || uintptr(newcap) > maxSliceCap(et.size) {
        panic(errorString("growslice: cap out of range"))
    }

    var p unsafe.Pointer
    // 在老的切片后面继续扩充容量
    if et.kind&kindNoPointers != 0 {
        // 计算新切片的容量,并生成这么大容量的内存空间
        p = mallocgc(capmem, nil, false)
        // 将 lenmem 这个多个 bytes 从 old.array地址 拷贝到 p 的地址处
        memmove(p, old.array, lenmem)
        // 先将 P 地址加上新的容量得到新切片容量的地址,然后将新切片容量地址后面的 capmem-newlenmem 个 bytes 这块内存初始化。为之后继续 append() 操作腾出空间。
        memclrNoHeapPointers(add(p, newlenmem), capmem-newlenmem)
    } else {
        // 重新申请新的数组给新切片
        // 重新申请 capmen 这个大的内存地址,并且初始化为0值
        p = mallocgc(capmem, et, true)
        if !writeBarrier.enabled {
            // 如果还不能打开写锁,那么只能把 lenmem 大小的 bytes 字节从 old.array 拷贝到 p 的地址处
            memmove(p, old.array, lenmem)
        } else {
            // 循环拷贝老的切片的值
            for i := uintptr(0); i < lenmem; i += et.size {
                typedmemmove(et, add(p, i), add(old.array, i))
            }
        }
    }
    // 返回最终新切片,容量更新为最新扩容之后的容量
    return slice{p, old.len, newcap}
}

cap(新申请容量)、newcap(最终容量)、old.cap(旧切片容量)

扩容逻辑:

(1)首先判断,如果新申请容量(cap) 大于 2倍的旧容量(doublecap),最终容量(newcap) 就是新申请容量(cap);

(2)否则判断,如果旧容量小于1024,最终容量就是2倍的旧容量;

(3)否则判断,如果旧切片长度大于等于1024,最终容量会从旧容量开始循环增加原来的 1/4,即(newcap=old.cap,for {newcap += newcap/4}),直到最终容量大于等于新申请的容量;

(4)如果最终容量计算溢出,最终容量就是新申请的容量。

新申请容量的计算:

新申请容量 = 原切片容量 + 追加的长度,如果结果是单数则 +1,再将这个容量传入进行判断;

  • 切片的底层结构
type slice struct {
	array unsafe.Pointer    //指针
	len   int
	cap   int
}

切片底层是一个指针、int类型的len和cap;所以 unsafe.Sizeof(切片)永远都是 24;

如果把 slice作为参数,本身传递的是值,但它的内容 byte array* 实际传递的是引用,所以可以在函数内部进行修改;而如果对 slice本身做 append,导致它进行扩容,实际扩容的是函数内复制的一份切片,对于函数外部的切片没有任何影响。

package main

import (
	"fmt"
	"unsafe"
)

func main() {
	slice_test := []int{1, 2, 3, 4, 5}
	fmt.Println(unsafe.Sizeof(slice_test))
	printSlice("outer", slice_test)
	sliceValue(slice_test)
	printSlice("outer", slice_test)
	slice_ptr(&slice_test)
	printSlice("outer", slice_test)
}

func printSlice(str string, slice []int) {
	fmt.Printf("%s ---> main:%#v,%#v,%#v\n", str, slice, len(slice), cap(slice))
}

func sliceValue(slice []int) {
	slice[1] = 100           // 函数外的slice发生了改变
	slice = append(slice, 5) //函数外的slice不变
	printSlice("inner", slice)
}

func slice_ptr(slice *[]int) {
	*slice = append(*slice, 7) //这样才改变了整个slice
	printSlice("inner", *slice)
}


// 输出
24
outer ---> main:[]int{1, 2, 3, 4, 5},5,5      
inner ---> main:[]int{1, 100, 3, 4, 5, 5},6,10
outer ---> main:[]int{1, 100, 3, 4, 5},5,5    
inner ---> main:[]int{1, 100, 3, 4, 5, 7},6,10
outer ---> main:[]int{1, 100, 3, 4, 5, 7},6,10

2. 切片生成方式

切片的生成有两种方式:new()make()

make([]int, 50, 100)
new([100]int)[0:50]

数组的创建一般使用 new()

var p *[]int = new([]int)
p := new([]int)
  • new(T) 为每个新类型T分配一块内存,并且返回 *T 的内存地址,这种方法返回一个指向类型为T,值为0的地址的指针,适用于值类型如数组和结构体。

  • make(T) 返回一个类型为T的初始值,只适用于3种内建的引用类型:切片、map、channel。

如何理解 new、make、slice、map、channel 的关系?

  • slice、map、channel 都是 golang 内建的一种引用类型,三者在内存中存在多个组成部分,需要对内存组成部分初始化后才能使用,而make 就是对三者进行初始化的一种操作方式。
  • new获取的是存储指定变量内存地址的一个变量,对于变量内部结构并不会执行相应的初始化操作,所以 slice、map、channel 需要make 进行初始化并获取相应的内存地址,而非new 简单的获取内存地址。

字符串与字节切片

字符串转换为字节切片

假设 s 是一个字符串,那么可以直接通过 c := []byte(s) 来获取该字符串对应的字节切片;另外还可以通过copy(dst []byte, src string) 来达到相同的目的。

修改字符串中的某个字符

go 语言中字符串是不可变的,所以需要首先将字符串转化为字节数组然后再修改

s := "hello"
c := []byte(s)
c[0] = 'c'
s2 := string(c) // s2 == "cello"

切片和数组的搜索排序

可以通过 sort 包来实现搜索和排序功能,对于 []int 数组, sort.Ints(xxx) 排序,sort.IntsAreSorted(xxx) 判断是否有序,sort.SearchInts(xxx, xxx) 搜索查找;

对于[]float64 数组,sort.Float64s(xxx) 排序,sort.FloatsAreSorted(xxx) 判断是否有序,sort.SearchFloats(xxx,xxx) 搜索查找;

对于[]string 数组,sort.Strings(xxx) 排序,sort.StringsAreSorted(xxx) 判断是否有序,sort.SearchStrings(xxx,xxx) 搜索查找。

append 函数巧用

1.   a = append(a,b...)       //将切片b追加到切片a之后
2.   b = make([]T, len(a))    //复制切片a的元素到切片b上
     copy(b, a)
3.   a = append(a[:i], a[i+1:]...)   //删除位于索引i的元素
4.   a = append(a[:i], a[j:]...)     //删除切片a中从索引i至j位置的元素

3. slice 的底层实现

type slice struct {
    array unsafe.Pointer      //指向底层数组的指针
    len   int                 //切片长度
    cap   int                 //切片容量
}

​ 切片是对数组的一个连续片段的引用,所以切片是一个引用类型,这个引用可以是整个数组或者是数组的某一部分。

​ 从 slice 中获取一块内存地址可以这样做:

s := make([]byte, 200)
ptr := unsafe.Pointer(&s[0])

​ 反过来从 Go 的内存地址中构造一个 slice,可以构造一个虚拟结构体并把 slice 的数据结构拼出来;或者采用 Go 反射的数据结构 SliceHeader,用它来构造一个 slice 切片。

var ptr unsafe.Pointer
var s1 = struct {
    addr uintptr
    len int
    cap int
}{ptr, length, length}
s := *(*[]byte)(unsafe.Pointer(&s1))

​ 这里再列举一个例子,用来说明 slice 底层的数据结构:

func main() {

   value := []int{1, 2, 3}
   fmt.Printf("modify begin addr: %p value: %d\n", value, value)
   fmt.Printf("modify begin addr: %p value: %d\n", &value, value)

   modify1(value)

   fmt.Printf("modify after addr: %p value: %d\n", value, value)
   fmt.Printf("modify after addr: %p value: %d\n", &value, value)
}

func modify1(value []int) {
   value[0] = 0
   fmt.Printf("modify addr: %p value: %d\n", value, value)
   fmt.Printf("modify addr: %p value: %d\n", &value, value)
}

//output
modify begin addr: 0xc0000b4000 value: [1 2 3]
modify begin addr: 0xc0000a4018 value: [1 2 3]
modify addr: 0xc0000b4000 value: [0 2 3]
modify addr: 0xc0000a4078 value: [0 2 3]
modify after addr: 0xc0000b4000 value: [0 2 3]
modify after addr: 0xc0000a4018 value: [0 2 3]

​ 可以发现地址无论是实参还是函数内部的地址都是相同的,而另外一个地址是不同的,这是因为切片是一个引用类型,变量里面保存的是实际数据的地址,在进行参数传递的时候我们传递的是保存的地址而不是保存这个地址的地址。也就是说我们将这个地址值传递过去了,然后函数内部开辟了一个内存将这个地址存起来了,然后我们就可以看见无论是在外面还是里面,这个传递的地址值是不变的。而保存这个地址的地址是改变的。这也是为什么是golang是值传递的原因。

image-20220922110549358

​ 空切片和 nil 切片需要区分开,区别在于 nil 切片的指针指向 nil;空切片指向的地址不是 nil,指向的是一个内存地址,但是它没有分配任何内存空间,即底层包含了0个元素。

  • nil 切片:var slice []int
  • 空切片:slice := make([]int, 0) 或者 slice := []int{}
image-20220921201755198 image-20220921201808215

4. 介于扩容逻辑的注意点

func main() {
    slice := []int{10, 20, 30, 40}
    newSlice := append(slice, 50)
    fmt.Printf("Before slice = %v, Pointer = %p, len = %d, cap = %d\n", slice, &slice, len(slice), cap(slice))
    fmt.Printf("Before newSlice = %v, Pointer = %p, len = %d, cap = %d\n", newSlice, &newSlice, len(newSlice), cap(newSlice))
    newSlice[1] += 10
    fmt.Printf("After slice = %v, Pointer = %p, len = %d, cap = %d\n", slice, &slice, len(slice), cap(slice))
    fmt.Printf("After newSlice = %v, Pointer = %p, len = %d, cap = %d\n", newSlice, &newSlice, len(newSlice), cap(newSlice))
}


//output
Before slice = [10 20 30 40], Pointer = 0xc4200b0140, len = 4, cap = 4
Before newSlice = [10 20 30 40 50], Pointer = 0xc4200b0180, len = 5, cap = 8
After slice = [10 20 30 40], Pointer = 0xc4200b0140, len = 4, cap = 4   //原数组未变
After newSlice = [10 30 30 40 50], Pointer = 0xc4200b0180, len = 5, cap = 8

​ 由于 append 触发了扩容逻辑,所以会新开辟空间来存储元素,新的切片并不会影响旧的切片。

func main() {
    array := [4]int{10, 20, 30, 40}
    slice := array[0:2]
    newSlice := append(slice, 50)
    fmt.Printf("Before slice = %v, Pointer = %p, len = %d, cap = %d\n", slice, &slice, len(slice), cap(slice))
    fmt.Printf("Before newSlice = %v, Pointer = %p, len = %d, cap = %d\n", newSlice, &newSlice, len(newSlice), cap(newSlice))
    newSlice[1] += 10
    fmt.Printf("After slice = %v, Pointer = %p, len = %d, cap = %d\n", slice, &slice, len(slice), cap(slice))
    fmt.Printf("After newSlice = %v, Pointer = %p, len = %d, cap = %d\n", newSlice, &newSlice, len(newSlice), cap(newSlice))
    fmt.Printf("After array = %v\n", array)
}

//output
Before slice = [10 20], Pointer = 0xc4200c0040, len = 2, cap = 4
Before newSlice = [10 20 50], Pointer = 0xc4200c0060, len = 3, cap = 4
After slice = [10 30], Pointer = 0xc4200c0040, len = 2, cap = 4
After newSlice = [10 30 50], Pointer = 0xc4200c0060, len = 3, cap = 4
After array = [10 30 50 40]    //原数组变化了

​ 可以发现整个过程没有达到数组扩容的标准,不会生成新的切片,这样所有的操作都在原数组上进行,会无意间产生一些bug。

​ 实际在创建切片时可以通过 make、切片字面量两种方式创建切片,注意使用字面量方式时控制好切片的容量,需要避免共享原数组从而导致的 bug。

​ 最后需要注意 range-for 遍历切片时,value 对应的是切片里面的值拷贝而并非引用传递,所以直接修改 value 达不到更改原切片的目的,需要通过 &slice[index] 获取真实地址进行修改。

func main() {
    slice := []int{10, 20, 30, 40}
    for index, value := range slice {
        fmt.Printf("value = %d , value-addr = %x , slice-addr = %x\n", value, &value, &slice[index])
    }
}

//output
value = 10 , value-addr = c4200aedf8 , slice-addr = c4200b0320
value = 20 , value-addr = c4200aedf8 , slice-addr = c4200b0328
value = 30 , value-addr = c4200aedf8 , slice-addr = c4200b0330
value = 40 , value-addr = c4200aedf8 , slice-addr = c4200b0338

7. Map集合

7.1 map的特点

map 是引用类型的,引用类型通过 make 来构造而不是 new.

如果想要判断某个 key 是否存在而不关心它的值是多少就可以通过 _, ok := map1[key]

map 默认是无序的,不管是按照 key 还是 value 默认都是无序。如果要使其有序,则先生成key的切片并排序,然后 for-range 打印所有Key 和 value.

判断Map中某个键是否存在: value, ok := map[key] ,如果key存在 ok 为 true;不存在 ok 为 false。

删除键值对: delete(map, key) ,删除 map 中指定 key值的键值对。

7.2 用Go语言实现HashMap结构

package main

import "fmt"

type HashMap struct { // HashMap结构体,参考Java实现 数组+链表
	key      string
	value    string
	hashcode int
	next     *HashMap
}

var table [16](*HashMap) // 哈希表

func main() {
	getInstance()
	put("a", "a_put")
	put("b", "b_put")
	fmt.Println(get("a"))
	fmt.Println(get("b"))
	put("p", "p_put")
	fmt.Println(get("p"))
	put("a", "copy_aaaa")
	fmt.Println(get("a"))
}

func initTable() { //初始化哈希表
	for i := range table {
		table[i] = &HashMap{"", "", i, nil}
	}
}

func getInstance() [16](*HashMap) { // 单例模式返回哈希表
	if table[0] == nil {
		initTable()
	}
	return table
}

func genHashCode(k string) int { // 生成哈希码,注意尽量保证哈希表的散列性,减少碰撞
	if len(k) == 0 {
		return 0
	}
	var hashcode int = 0
	var lastIndex int = len(k) - 1
	for i := range k {
		if i == lastIndex {
			hashcode += int(k[i])
			break
		}
		hashcode += (hashcode + int(k[i])) * 31
	}
	return hashcode
}

func indexTable(hashcode int) int { // 获取数据在哈希表的下标
	return hashcode % 16
}

func indexNode(hashcode int) int { // 获取数据在链表中的下标
	return hashcode >> 4
}

func put(k string, v string) string { // 存放键值对,先根据hashcode找到哈希表下标,再根据hashcode找到链表中的位置
	var headPtr [16](*HashMap) = getInstance()
	var hashCode = genHashCode(k)
	var thisNode = HashMap{k, v, hashCode, nil}
	if len(k) == 0 { // key为空直接放在表头位置
		*(headPtr[0]) = thisNode
		return ""
	}
	var tableIndex = indexTable(hashCode) // 否则tableIndex查表、nodeIndex查链
	var nodeIndex = indexNode(hashCode)
	var headNode = headPtr[tableIndex]
	if (*headNode).key == "" { // 表位置为空,直接存
		*headNode = thisNode
		return ""
	}
	var lastNode (*HashMap) = headNode
	var nextNode (*HashMap) = (*headNode).next
	for nextNode != nil && (indexNode((*nextNode).hashcode) < nodeIndex) { //查链,保证链表前元素nodeIndex小于后边元素的nodeIndex
		lastNode = nextNode
		nextNode = (*nextNode).next
	}
	if lastNode.hashcode == hashCode { // 找到后比较hashcode,相同直接替换并返回旧值
		var oldValue = (*lastNode).value
		lastNode.value = thisNode.value
		return oldValue
	}
	if lastNode.hashcode < thisNode.hashcode { // hashcode不等,插入在lastNode后
		lastNode.next = &thisNode
	}
	if nextNode != nil { // 处理插入后的nextNode
		thisNode.next = nextNode
	}
	return ""
}

func get(k string) string {
	var hashcode = genHashCode(k)
	var tableIndex = indexTable(hashcode)
	var headPtr ([16]*HashMap) = getInstance()
	var node (*HashMap) = headPtr[tableIndex]
	if (*node).key == k {
		return (*node).value
	}
	for (*node).next != nil {
		if (*node).key == k {
			return (*node).value
		}
		node = (*node).next
	}
	return ""
}

7.3 使用实例

func main() {	
//元素为map类型的切片
	var mapslice = make([]map[string]string, 3)
	for index, value := range mapslice {
		fmt.Printf("index:%d value:%v\n", index, value)
	}
	fmt.Println("after init")
	mapslice[0] = make(map[string]string, 10)
	mapslice[0]["name"] = "王五"
	mapslice[0]["age"] = "10"
	mapslice[0]["address"] = "收到罚单"
	for index, value := range mapslice {
		fmt.Printf("index:%d value:%v\n", index, value)
	}
    
//值为切片类型的map
	var sliceMap = make(map[string][]string, 3)
	fmt.Println(sliceMap)
	fmt.Println("after init")
	key := "中国"
	value, ok := sliceMap[key]
	if !ok {
		value = make([]string, 0, 2)
	}
	value = append(value, "北京", "上海")
	sliceMap[key] = value
	fmt.Println(sliceMap)
}

7.4 Map实现原理

map 源码位于 src/runtime/map.go 中,map 也是数组存储的,每个数组下标存储的是一个 bucket,每个bucket可以存储 8个键值对,当每个 bucket 存储的键值对达到 8个后,会通过 overflow 指针指向一个新的 bucket。

// A bucket for a Go map.
type bmap struct {
	// tophash generally contains the top byte of the hash value
	// for each key in this bucket. If tophash[0] < minTopHash,
	// tophash[0] is a bucket evacuation state instead.
	tophash [bucketCnt]uint8
	// Followed by bucketCnt keys and then bucketCnt elems.
	// NOTE: packing all the keys together and then all the elems together makes the
	// code a bit more complicated than alternating key/elem/key/elem/... but it allows
	// us to eliminate padding which would be needed for, e.g., map[int64]int8.
	// Followed by an overflow pointer.
}

​ 每个 bucket 利用 tophash来快速查找 key是否在 bucket中,tophash 保存了每个 hash 的高8位,查找时利用计算出key的hash值的高8位判断键是否存在。其次是 kv的存放,不是 k1v1、k2v2、k3v3 这样存放,而是 k1k2k3 、v1v2v3,因为对于 map[int64]int8 ,value是int8(一个字节),key是int64(八个字节),kv的长度不同,如果按照kv格式存放,考虑到内存对齐v也要占用 int64;而如果按照k一起存储、v一起存储,8个v刚好占用int64,这样存储内存浪费更少,更紧凑。

image-20220922114714695

go整体内存结构,当往map中存储一个kv时,通过key获取hash值,hash值的低八位和bucket数组长度取余,定位到在数组中的那个下标,hash值的高八位存储在bucket中的tophash中,用来快速判断key是否存在,key和value的具体值则通过指针运算存储,当一个bucket满时,通过overfolw指针链接到下一个bucket。

				<img src="D:\note\Golang\passageImg\02_go语言的变量和常量.assets\2l83pmdsbj.jpeg" alt="img" style="zoom:67%;" />

7.5 Map与工厂模式

​ map 的值可以为任意值,那么值类型也可以为函数,利用 map 传递参数就类似于实现了工厂模式。

// Map与工厂模式
func TestMapFactory(t *testing.T) {
    // 这里key为Int;Value为自定义函数类型
	m := map[int]func(op int) int{}
	m[1] = func(op int) int { return op }
	m[2] = func(op int) int { return op * op }
	m[3] = func(op int) int { return op * op * op }
	t.Log(m[1](2), m[2](2), m[3](2))
}

7.6 利用Map实现Set

// 测试用Map构造Set,将key设置为元素类型,值设置为bool
func TestMapForSet(t *testing.T) {
	mySet := map[int]bool{}
	//put 操作
	mySet[1] = true
	n := 3
	//exist 操作
	if mySet[n] {
		t.Logf("%d is existing", n)
	} else {
		t.Logf("%d is not existing", n)
	}
	mySet[3] = true
	//delete操作
	delete(mySet, 1)
	n = 1
	if mySet[n] {
		t.Logf("%d is existing", n)
	} else {
		t.Logf("%d is not existing", n)
	}
}

7.7 Map源码分析

https://juejin.cn/post/7130082747715944461

https://cloud.tencent.com/developer/article/1468799

7.8 Concurrent-map

​ 我们知道 go 语言的 map 在高并发情况下不能保证并发安全,因此可以结合原始 Map 和并发锁来实现线程安全的 Map,有以下四种方式:

// 使用 sync.Mutex 互斥锁,对数据的任意操作都会上锁
type LockMap struct {
    m    map[interface{}]interface{}
    lock sync.Mutex
}

// 使用 sync.RWMutex 读写锁,允许多个协程一起读,多个协程写时再上锁
 type RwLockMap struct {
    m    map[interface{}]interface{}
    lock sync.RWMutex
}

// sync.Map,内部包含两块区域,一块只读区域,一块读写区域,每次操作时,会先去只读区域中寻找,只读区域未找到的时候触发miss,然后才会去可读可写区域寻找。很显然,最糟糕的情况下比LockMap性能更低,因为比LockMap多一次查找的动作。sync.Map也是适合读多写少的情况。
sync.Map

// https://github.com/orcaman/concurrent-map
concurrentMap

​ concurrentMap 的原理类似于 Java 中的 ConcurrentHashMap,都是对 Map 进行分段,然后对每一段进行加解锁,这样分区1上锁时不会影响分区2的读写操作。

image-20221006152956406
// 源码:https://github.com/orcaman/concurrent-map

package cmap

import (
	"encoding/json"
	"sync"
)

var SHARD_COUNT = 32     //默认采用32个 ConcurrentMapShared 结构

// A "thread" safe map of type string:Anything.
// To avoid lock bottlenecks this map is dived to several (SHARD_COUNT) map shards.
type ConcurrentMap[V any] []*ConcurrentMapShared[V]

// A "thread" safe string to anything map.
type ConcurrentMapShared[V any] struct {   //对外表现为ConucrrentMap,实质上是一个包含32个ConcurrentMapShared结构的数组
	items        map[string]V		      //每个ConcurrentMapShared结构体包含读写锁、原始Map
	sync.RWMutex // Read Write mutex, guards access to internal map.
}

// Creates a new concurrent map.
func New[V any]() ConcurrentMap[V] {
	m := make(ConcurrentMap[V], SHARD_COUNT)
	for i := 0; i < SHARD_COUNT; i++ {
		m[i] = &ConcurrentMapShared[V]{items: make(map[string]V)}
	}
	return m
}

// GetShard returns shard under given key
func (m ConcurrentMap[V]) GetShard(key string) *ConcurrentMapShared[V] {
	return m[uint(fnv32(key))%uint(SHARD_COUNT)]
}

func (m ConcurrentMap[V]) MSet(data map[string]V) {
	for key, value := range data {
		shard := m.GetShard(key)
		shard.Lock()
		shard.items[key] = value
		shard.Unlock()
	}
}

// Sets the given value under the specified key.
func (m ConcurrentMap[V]) Set(key string, value V) {   
	// Get map shard.
	shard := m.GetShard(key)   //set接口先根据key计算应该锁住哪个分区,然后上锁进行set,最后释放锁
	shard.Lock()
	shard.items[key] = value
	shard.Unlock()
}

// Callback to return new element to be inserted into the map
// It is called while lock is held, therefore it MUST NOT
// try to access other keys in same map, as it can lead to deadlock since
// Go sync.RWLock is not reentrant
type UpsertCb[V any] func(exist bool, valueInMap V, newValue V) V

// Insert or Update - updates existing element or inserts a new one using UpsertCb
func (m ConcurrentMap[V]) Upsert(key string, value V, cb UpsertCb[V]) (res V) {
	shard := m.GetShard(key)
	shard.Lock()
	v, ok := shard.items[key]
	res = cb(ok, v, value)         //将自定义函数的结果当作value进行存储
	shard.items[key] = res
	shard.Unlock()
	return res
}

// Sets the given value under the specified key if no value was associated with it.
func (m ConcurrentMap[V]) SetIfAbsent(key string, value V) bool {
	// Get map shard.
	shard := m.GetShard(key)
	shard.Lock()
	_, ok := shard.items[key]
	if !ok {
		shard.items[key] = value
	}
	shard.Unlock()
	return !ok
}

// Get retrieves an element from map under given key.
func (m ConcurrentMap[V]) Get(key string) (V, bool) {   //get操作也上锁,不过加的是读锁,多个协程可以一起读
	// Get shard
	shard := m.GetShard(key)
	shard.RLock()
	// Get item from shard.
	val, ok := shard.items[key]
	shard.RUnlock()
	return val, ok
}

// Count returns the number of elements within the map.
func (m ConcurrentMap[V]) Count() int {   //count循环对每个分区加写锁,然后统计map中元素个数,再释放锁,高并发下可能会有误差
	count := 0						   //因为统计完分区1并对分区2加锁后,可能会对分区1进行再操作,但对于count精度要求不高
	for i := 0; i < SHARD_COUNT; i++ {
		shard := m[i]
		shard.RLock()
		count += len(shard.items)
		shard.RUnlock()
	}
	return count
}

// Looks up an item under specified key
func (m ConcurrentMap[V]) Has(key string) bool {
	// Get shard
	shard := m.GetShard(key)
	shard.RLock()
	// See if element is within shard.
	_, ok := shard.items[key]
	shard.RUnlock()
	return ok
}

// Remove removes an element from the map.
func (m ConcurrentMap[V]) Remove(key string) {
	// Try to get shard.
	shard := m.GetShard(key)
	shard.Lock()
	delete(shard.items, key)
	shard.Unlock()
}

// RemoveCb is a callback executed in a map.RemoveCb() call, while Lock is held
// If returns true, the element will be removed from the map
type RemoveCb[V any] func(key string, v V, exists bool) bool

// RemoveCb locks the shard containing the key, retrieves its current value and calls the callback with those params
// If callback returns true and element exists, it will remove it from the map
// Returns the value returned by the callback (even if element was not present in the map)
func (m ConcurrentMap[V]) RemoveCb(key string, cb RemoveCb[V]) bool {
	// Try to get shard.
	shard := m.GetShard(key)
	shard.Lock()
	v, ok := shard.items[key]
	remove := cb(key, v, ok)      //自定义函数返回bool,如果为true就删除指定的Key
	if remove && ok {
		delete(shard.items, key)
	}
	shard.Unlock()
	return remove
}

// Pop removes an element from the map and returns it
func (m ConcurrentMap[V]) Pop(key string) (v V, exists bool) {
	// Try to get shard.
	shard := m.GetShard(key)
	shard.Lock()
	v, exists = shard.items[key]
	delete(shard.items, key)
	shard.Unlock()
	return v, exists
}

// IsEmpty checks if map is empty.
func (m ConcurrentMap[V]) IsEmpty() bool {
	return m.Count() == 0
}

// Used by the Iter & IterBuffered functions to wrap two variables together over a channel,
type Tuple[V any] struct {
	Key string
	Val V
}

// Iter returns an iterator which could be used in a for range loop.
//
// Deprecated: using IterBuffered() will get a better performence
func (m ConcurrentMap[V]) Iter() <-chan Tuple[V] {
	chans := snapshot(m)
	ch := make(chan Tuple[V])
	go fanIn(chans, ch)
	return ch
}

// IterBuffered returns a buffered iterator which could be used in a for range loop.
func (m ConcurrentMap[V]) IterBuffered() <-chan Tuple[V] {
	chans := snapshot(m)
	total := 0
	for _, c := range chans {
		total += cap(c)
	}
	ch := make(chan Tuple[V], total)
	go fanIn(chans, ch)
	return ch
}

// Clear removes all items from map.
func (m ConcurrentMap[V]) Clear() {
	for item := range m.IterBuffered() {
		m.Remove(item.Key)
	}
}

// Returns a array of channels that contains elements in each shard,
// which likely takes a snapshot of `m`.
// It returns once the size of each buffered channel is determined,
// before all the channels are populated using goroutines.
func snapshot[V any](m ConcurrentMap[V]) (chans []chan Tuple[V]) {
	//When you access map items before initializing.
	if len(m) == 0 {
		panic(`cmap.ConcurrentMap is not initialized. Should run New() before usage.`)
	}
	chans = make([]chan Tuple[V], SHARD_COUNT)
	wg := sync.WaitGroup{}
	wg.Add(SHARD_COUNT)
	// Foreach shard.
	for index, shard := range m {
		go func(index int, shard *ConcurrentMapShared[V]) {
			// Foreach key, value pair.
			shard.RLock()
			chans[index] = make(chan Tuple[V], len(shard.items))
			wg.Done()
			for key, val := range shard.items {
				chans[index] <- Tuple[V]{key, val}
			}
			shard.RUnlock()
			close(chans[index])
		}(index, shard)
	}
	wg.Wait()
	return chans
}

// fanIn reads elements from channels `chans` into channel `out`
func fanIn[V any](chans []chan Tuple[V], out chan Tuple[V]) {
	wg := sync.WaitGroup{}
	wg.Add(len(chans))
	for _, ch := range chans {
		go func(ch chan Tuple[V]) {
			for t := range ch {
				out <- t
			}
			wg.Done()
		}(ch)
	}
	wg.Wait()
	close(out)
}

// Items returns all items as map[string]V
func (m ConcurrentMap[V]) Items() map[string]V {
	tmp := make(map[string]V)

	// Insert items to temporary map.
	for item := range m.IterBuffered() {
		tmp[item.Key] = item.Val
	}

	return tmp
}

// Iterator callbacalled for every key,value found in
// maps. RLock is held for all calls for a given shard
// therefore callback sess consistent view of a shard,
// but not across the shards
type IterCb[V any] func(key string, v V)

// Callback based iterator, cheapest way to read
// all elements in a map.
func (m ConcurrentMap[V]) IterCb(fn IterCb[V]) {
	for idx := range m {
		shard := (m)[idx]
		shard.RLock()
		for key, value := range shard.items {
			fn(key, value)     //遍历时执行自定义操作
		}
		shard.RUnlock()
	}
}

// Keys returns all keys as []string
func (m ConcurrentMap[V]) Keys() []string {
	count := m.Count()
	ch := make(chan string, count)
	go func() {
		// Foreach shard.
		wg := sync.WaitGroup{}
		wg.Add(SHARD_COUNT)
		for _, shard := range m {
			go func(shard *ConcurrentMapShared[V]) {
				// Foreach key, value pair.
				shard.RLock()
				for key := range shard.items {
					ch <- key
				}
				shard.RUnlock()
				wg.Done()
			}(shard)
		}
		wg.Wait()
		close(ch)
	}()

	// Generate keys
	keys := make([]string, 0, count)
	for k := range ch {
		keys = append(keys, k)
	}
	return keys
}

//Reviles ConcurrentMap "private" variables to json marshal.
func (m ConcurrentMap[V]) MarshalJSON() ([]byte, error) {
	// Create a temporary map, which will hold all item spread across shards.
	tmp := make(map[string]V)

	// Insert items to temporary map.
	for item := range m.IterBuffered() {
		tmp[item.Key] = item.Val
	}
	return json.Marshal(tmp)
}

func fnv32(key string) uint32 {       //根据key计算存储的分区号
	hash := uint32(2166136261)
	const prime32 = uint32(16777619)
	keyLength := len(key)
	for i := 0; i < keyLength; i++ {
		hash *= prime32
		hash ^= uint32(key[i])
	}
	return hash
}

// Reverse process of Marshal.
func (m *ConcurrentMap[V]) UnmarshalJSON(b []byte) (err error) {
 	tmp := make(map[string]V)

 	// Unmarshal into a single map.
 	if err := json.Unmarshal(b, &tmp); err != nil {
 		return err
 	}

 	// foreach key,value pair in temporary map insert into our concurrent map.
 	for key, val := range tmp {
 		m.Set(key, val)
 	}
	return nil
}

基本接口:

// 获取分区key
func (m ConcurrentMap) GetShard(key string) *ConcurrentMapShared
// 合并map
func (m ConcurrentMap) MSet(data map[string]interface{})
// 添加一个元素
func (m ConcurrentMap) Set(key string, value interface{})
// 获取一个元素
func (m ConcurrentMap) Get(key string) (interface{}, bool)
// 计算有多少元素
func (m ConcurrentMap) Count() int
//判断元素是否存
func (m ConcurrentMap) Has(key string) bool
// 移除指定元素
func (m ConcurrentMap) Remove(key string)
// 获取并移除指定的元素
func (m ConcurrentMap) Pop(key string) (v interface{}, exists bool)
// 判断是否是map
func (m ConcurrentMap) IsEmpty() bool
// 清空map
func (m ConcurrentMap) Clear()

高级接口:

// 插入-更新回调, cb是传入的自定义插入更新时的操作
func (m ConcurrentMap[V]) Upsert(key string, value V, cb UpsertCb[V]) (res V) 

// 遍历回调,fn是自定义操作
func (m ConcurrentMap[V]) IterCb(fn IterCb[V])

// 删除回到,cb是自定义操作
func (m ConcurrentMap[V]) RemoveCb(key string, cb RemoveCb[V]) bool

基准测试:

func BenchmarkSingleInsertPresent(b *testing.B) {
    m := New()
    m.Set("key", "value")
    b.ResetTimer()
    for i := 0; i < b.N; i++ {
        m.Set("key", "value")
    }
}

func BenchmarkSingleInsertPresentSyncMap(b *testing.B) {
    var m sync.Map
    m.Store("key", "value")
    b.ResetTimer()
    for i := 0; i < b.N; i++ {
        m.Store("key", "value")
    }
}


//运行
> go test -bench=InsertPresent -benchtime 5s
goos: linux
goarch: amd64
pkg: concurrent-map
BenchmarkSingleInsertPresent-8                  172822759               34.9 ns/op
BenchmarkSingleInsertPresentSyncMap-8           65351324                92.9 ns/op

​ 从结果可以看出,set 添加元素操作,concurrent-map 比 sync.map 快了接近 3 倍。

func benchmarkMultiInsertDifferent(b *testing.B) {
	m := New()
	finished := make(chan struct{}, b.N)
	_, set := GetSet(m, finished)        //这个操作传入concurrentMap、通道;返回从通道get和往通道set的两个函数
	b.ResetTimer()
	for i := 0; i < b.N; i++ {
		go set(strconv.Itoa(i), "value")
	}
	for i := 0; i < b.N; i++ {
		<-finished
	}
}

func BenchmarkMultiInsertDifferentSyncMap(b *testing.B) {
	var m sync.Map
	finished := make(chan struct{}, b.N)
	_, set := GetSetSyncMap(&m, finished)  //这个操作传入sync.Map、通道;返回从通道get和往通道set的两个函数

	b.ResetTimer()
	for i := 0; i < b.N; i++ {
		go set(strconv.Itoa(i), "value")
	}
	for i := 0; i < b.N; i++ {
		<-finished
	}
}

func BenchmarkMultiInsertDifferent_1_Shard(b *testing.B) {
	runWithShards(benchmarkMultiInsertDifferent, b, 1)
}
func BenchmarkMultiInsertDifferent_16_Shard(b *testing.B) {
	runWithShards(benchmarkMultiInsertDifferent, b, 16)
}
func BenchmarkMultiInsertDifferent_32_Shard(b *testing.B) {
	runWithShards(benchmarkMultiInsertDifferent, b, 32)
}
func BenchmarkMultiInsertDifferent_256_Shard(b *testing.B) {
	runWithShards(benchmarkMultiGetSetDifferent, b, 256)
}

func runWithShards(bench func(b *testing.B), b *testing.B, shardsCount int) {
	oldShardsCount := SHARD_COUNT
	SHARD_COUNT = shardsCount
	bench(b)
	SHARD_COUNT = oldShardsCount
}


//结果
> go test -bench=InsertDifferent -benchtime 5s
goos: linux
goarch: amd64
pkg: concurrent-map
BenchmarkMultiInsertDifferentSyncMap-8            560900             11996 ns/op
BenchmarkMultiInsertDifferent_1_Shard-8          1000000              7499 ns/op
BenchmarkMultiInsertDifferent_16_Shard-8        10377100               662 ns/op
BenchmarkMultiInsertDifferent_32_Shard-8        10511775               603 ns/op
BenchmarkMultiInsertDifferent_64_Shard-8        11624546               590 ns/op
BenchmarkMultiInsertDifferent_128_Shard-8       11773946               578 ns/op
BenchmarkMultiInsertDifferent_256_Shard-8        7914397               912 ns/op

​ sync.map在插入不同key时的表现似乎是最差的。在concurrent-map的分区数设置为1时,可以认为是对单个map加了全局读写锁,居然也比sync.map要快。但sync.map和分区为1的concurrent-map在多次测试时差异比较大,有时sync.map快,有时分区为1的concurrent-map快。单都不会比分区为16以上的concurrent-map快。而且并不是分区数越大越快,在分区数为256时,执行速度已经开始变慢了。

其它

go内存逃逸:https://blog.csdn.net/qq_42849214/article/details/124478252

标签:02,slice,常量,int,value,key,func,go,string
From: https://www.cnblogs.com/istitches/p/17748636.html

相关文章

  • GO数组解密:从基础到高阶全解
    在本文中,我们深入探讨了Go语言中数组的各个方面。从基础概念、常规操作,到高级技巧和特殊操作,我们通过清晰的解释和具体的Go代码示例为读者提供了全面的指南。无论您是初学者还是经验丰富的开发者,这篇文章都将助您更深入地理解和掌握Go数组的实际应用。关注公众号【TechLeadClou......
  • 2023-2024-1 20231421 《计算机基础与程序设计》第二周学习总结
    作业信息作业要求:https://www.cnblogs.com/rocedu/p/9577842.html#WEEK02作业目标:自学《计算机科学概论》和《c语言程序设计》第一章教材学习内容总结一、《计算机科学概论》1.计算机系统是由信息,硬件,程序设计,操作系统,应用和通信由内而外组成的2.了解了计算机的历史二、《c......
  • 04_go语言io流
    1.基本IO接口1.1Reader接口//Reader接口定义typeReaderinterface{Read(p[]byte)(nint,errerror)}Read将len(p)个字节读取到p中。它返回读取的字节数n(0<=n<=len(p))以及遇到的任何错误。即使Read返回的n<len(p),它也会在调用过程中占用le......
  • P8813 [CSP-J 2022] 乘方
    题目描述小文同学刚刚接触了信息学竞赛,有一天她遇到了这样一个题:给定正整数\(a\)和\(b\),求\(a^b\)的值是多少。\(a^b\)即\(b\)个\(a\)相乘的值,例如\(2^3\)即为\(3\)个\(2\)相乘,结果为\(2\times2\times2=8\)。“简单!”小文心想,同时很快就写出了一份程序,......
  • 学期2023-2024-1 20231417 《计算机基础与程序设计》第二周学习总结
    计算机科学概论第一章1.计算系统分为:软件,硬件,数据2.计算系统的层次是抽象c语言程序设计1.生如了解了什么是编程,为什么要学习编程以及怎么学习学习进度条 代码行数(新增/累积)博客量(新增/累积)学习时间(新增/累积)重要成长目标5000行30篇400小时 第一周 1/1......
  • 2023-2024-20231409佟伟铭第二周学习总结
    学期2023-2024-120231409《计算机基础与程序设计》第二周学习总结作业信息这个作业属于哪个课程2023-2024-1-计算机基础与程序设计这个作业要求在哪里2023-2024-1计算机基础与程序设计第二周作业这个作业的目标自学教材:计算机科学概论第1章并完成云班课......
  • GO数组解密:从基础到高阶全解
    在本文中,我们深入探讨了Go语言中数组的各个方面。从基础概念、常规操作,到高级技巧和特殊操作,我们通过清晰的解释和具体的Go代码示例为读者提供了全面的指南。无论您是初学者还是经验丰富的开发者,这篇文章都将助您更深入地理解和掌握Go数组的实际应用。关注公众号【TechLeadClo......
  • ChatGPT 问答00023 java代码发邮件怎么带附件
    在Java中发送带附件的Email,可以使用JavaMail和JAF(JavaBeansActivationFramework)库来实现。以下是一个基本的示例代码,演示如何发送带有附件的Email:importjava.util.*;importjavax.mail.*;importjavax.mail.internet.*;publicclassSendEmailWithAttachment{public......
  • 2023-2024-1 20231302《计算机基础与程序设计》第二周学习总结
    作业信息这个作业属于哪个课程https://edu.cnblogs.com/campus/besti/2023-2024-1-CFAP/这个作业要求在哪里https://www.cnblogs.com/rocedu/p/9577842.html#WEEK02这个作业的目标<数字化,信息安全,自学教材《计算机科学概论》第1章并完成云班课测试、自学教材《C语......
  • 学期2023-2024-1 20231417 《计算机基础与程序设计》第二周学习总结
    学期2023-2024-120231417《计算机基础与程序设计》第二周学习总结教材学习内容总结《计算机科学概论》第一章:1.计算系统的分层以及计算系统的核心板块:硬件、软件、数据。2.抽象是一种思考问题的方式删除或隐藏了复杂的细节,保留实现目标所必须的信息3.计算机的四代硬件简史,......