前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >理解Golang 赋值的并发安全性

理解Golang 赋值的并发安全性

作者头像
sunsky
发布2023-03-08 15:06:35
7900
发布2023-03-08 15:06:35
举报
文章被收录于专栏:sunsky

1.什么是并发安全

并发安全就是程序在并发情况下执行的结果是正确的。

比如对一个变量简单的自增操作count++,在非并发下很好理解,而在并发情况下却容易出现预期之外的结果,这样的代码就是非并发安全的。

因为count++其实是分成两步执行的,当分成了两步执行,那么其他协程就可以趁着这个时间间隙作怪。

如一下 a b 两个协程同时 count++:

代码语言:javascript
复制
count:= 1
a > 读取count : 1
b > 读取count : 1
a > 计算count+1 : 2
b > 计算count+1 : 2
a > 赋值count : 2
b > 赋值count : 2

复制

这就会发生明明 a b 协程计算了两次,可结果还是 2。

2.struct 并发赋值安全吗

对一个简单变量的自增都会出现偏差,那么赋值一个更为复杂的结构体会不会有问题呢?

例如以下代码,在多协程的情况下,并发使用两个不同的值对结构体变量进行赋值,如果结构体成员出现异常情况, 那么说明并发出现了问题。

代码语言:javascript
复制
type Test struct {
	X int
	Y int
}

func main() {
	var g Test

	for i := 0; i < 1000000; i++ {
		var wg sync.WaitGroup
		// 协程 1
		wg.Add(1)
		go func() {
			defer wg.Done()
			g = Test{1,2}
		}()

		// 协程 2
		wg.Add(1)
		go func(){
			defer wg.Done()
			g = Test{3,4}
		}()
		wg.Wait()

		// 赋值异常判断
		if !((g.X == 1 && g.Y == 2) || (g.X == 3 && g.Y == 4)) {
			fmt.Printf("concurrent assignment error, i=%v g=%+v", i, g)
			break
		}
	}
}

复制

运行一次或多次,将出现赋值异常。

代码语言:javascript
复制
concurrent assignment error, i=48714 g={X:1 Y:4}

复制

结构体中有多个字段,协程 1 赋值了字段 X,协程 2 赋值了字段 Y,此时整个结构体既不是协程 1 想要的结果,也不是协程 2 想要的结果。可见 struct 赋值时,并不是原子操作,各个字段的赋值是独立的,在并发操作的情况下可能会出现异常。

3.如何保证并发赋值的安全性

Golang 早已想到该问题,并为我们提供一个开箱即用的类型 atomic.Value 来保证赋值的并发安全。

代码语言:javascript
复制
// A Value provides an atomic load and store of a consistently typed value.
// The zero value for a Value returns nil from Load.
// Once Store has been called, a Value must not be copied.
//
// A Value must not be copied after first use.
type Value struct {
	v interface{}
}

复制

让我们借助 atomic.Value 来完成对 struct 的安全并发赋值。

代码语言:javascript
复制
func main() {
	var v atomic.Value

	for i := 0; i < 1000000; i++ {
		var wg sync.WaitGroup
		// 协程 1
		wg.Add(1)
		go func() {
			defer wg.Done()
			v.Store(Test{1,2})
		}()

		// 协程 2
		wg.Add(1)
		go func(){
			defer wg.Done()
			v.Store(Test{3,4})
		}()
		wg.Wait()

		// 赋值异常判断
		g := v.Load().(Test)
		if (g.X == 1 && g.Y == 2) || (g.X == 3 && g.Y == 4) {
		} else {
			fmt.Printf("concurrent assignment error, i=%v g=%+v", i, g)
			break
		}
	}
}

复制

上面执行将不会出现并发赋值异常的情况。

4.哪些类型并发赋值是安全的

我们已经知道了 struct 因为存在多个字段,赋值时各个字段时独立完成,所以并发不安全。那么对于 Golang 中其他的数据类型,并发赋值是安全的吗?

Golang 中数据类型可以分类两大类:基本数据类型和复合数据类型。

基本数据类型有:字节型,布尔型、整型、浮点型、字符型、复数型、字符串。

复合数据类型包括:指针、数组、切片、结构体、字典、通道、函数、接口。

复合数据类又可细分为如下三类: (1)非引用类型:数组、结构体; (2)引用类型:指针、切片、字典、通道、函数; (3)接口。

下面一一列举哪些数据类型是并发不安全的。

4.1 基本类型的并发赋值

4.1.1 字节型、布尔型、整型、浮点型、字符型(安全)

由于字节型、布尔型、整型、浮点型、字符型的位宽不会超过 64 位,在 64 位的指令集架构中可以由一条机器指令完成,不存在被细分为更小的操作单位,所以这些类型的并发赋值是安全的。

下面以浮点型为例进行测试。

代码语言:javascript
复制
func main() {
	var g float64

	for i := 0; i < 1000000; i++ {
		var wg sync.WaitGroup
		// 协程 1
		wg.Add(1)
		go func() {
			defer wg.Done()
			g = 1.1
		}()

		// 协程 2
		wg.Add(1)
		go func(){
			defer wg.Done()
			g = 2.2
		}()
		wg.Wait()

		// 赋值异常判断
		if g != 1.1) && g != 2.2 {
			fmt.Printf("concurrent assignment error, i=%v g=%+v", i, g)
			break
		}
	}
}

复制

上面个的测试代码对一个 float64 类型的变量进行并发赋值是没有问题的,其他类型读者可自行验证。

4.1.2 复数型(不安全)

按照上面的分析,因为复数型分为实部和虚部,两者的赋值是分开进行的,所以复数类型并发赋值是不安全的。

代码语言:javascript
复制
func main() {
	var g complex64

	for i := 0; i < 1000000; i++ {
		var wg sync.WaitGroup
		// 协程 1
		wg.Add(1)
		go func() {
			defer wg.Done()
			g = complex(1,2)
		}()

		// 协程 2
		wg.Add(1)
		go func(){
			defer wg.Done()
			g = complex(3,4)
		}()
		wg.Wait()

		// 赋值异常判断
		if g != complex(1,2) && g != complex(3,4) {
			fmt.Printf("concurrent assignment error, i=%v g=%+v", i, g)
			break
		}
	}
}

复制

运行输出:

代码语言:javascript
复制
concurrent assignment error, i=131512 g=(1+4i)

复制

注意:如果复数并发赋值时,有相同的虚部或实部,那么两个字段赋值就退化成一个字段,这种情况下时并发安全的。读者可自行验证。

4.1.3 字符串(不安全)

字符串在 Go 中是一个只读字节切片。

字符串有两个重要特点: (1)string 可以为空(长度为 0),但不会是 nil; (2)string对象不可以修改。

在源码包src/runtime/string.go我们可以找到 string 的底层数据结构:

代码语言:javascript
复制
type stringStruct struct {
	str unsafe.Pointer
	len int
}

复制

其数据结构很简单: str 为字符串的首地址; len 为字符串的长度(单位字节); string 数据结构跟切片有些类似,只不过切片还有一个表示容量的成员,事实上 string 和字节切片间经常强制互转。

因为 string 底层结构是个 struct,前面已经讨论过 struct 并发赋值是不安全的,所以 string 的并发赋值同样是不安全。我们来验证一下。

代码语言:javascript
复制
func main() {
	var s string

	for i := 0; i < 1000000; i++ {
		var wg sync.WaitGroup
		// 协程 1
		wg.Add(1)
		go func() {
			defer wg.Done()
			s = "ab"
		}()

		// 协程 2
		wg.Add(1)
		go func() {
			defer wg.Done()
			s = "abc"
		}()
		wg.Wait()

		// 赋值异常判断
		if s != "ab" && s != "abc" {
			fmt.Printf("concurrent assignment error, i=%v s=%v", i, s)
			break
		}
	}
}

复制

运行输出:

代码语言:javascript
复制
concurrent assignment error, i=509383 s=abi

复制

并发赋值不出意料地出现了异常情况,推测正确。

从这里我们可以得到一个基本结论:只要底层结构是 struct 的类型,那么并发赋值都是不安全的。

注意不安全不代表一定发生错误。就是说不安全不代表任何并发赋值的情况下都会发生错误。比如上面测试代码循环次数少的情况下,很难出现出现异常情况。

不过我这里想说的不是次数的问题,因为次数多少是个概率的问题,我这里说的是和所要赋的值有关。只要不同的值满足一定特点,不管多少次并发,都是安全的。

为什么可以这么说呢,我们还是要回看 string 的底层数据结构。因为是两个字段,字节指针 str 和字符串长度 len,我们只要保证并发赋值情况下,两个字段的赋值正确就行。前面也说了,因为 struct 多个字段的赋值是独立,所以如果两个字段中只要有一个字段是不同的,那么并发赋值就变成了一个字段的并发赋值,这样就不会出现问题。

比如我们并发赋值两个等长度但内容不同的字符串,就不会有问题。验证如下:

代码语言:javascript
复制
func main() {
	var s string

	for i := 0; i < 1000000; i++ {
		var wg sync.WaitGroup
		// 协程 1
		wg.Add(1)
		go func() {
			defer wg.Done()
			s = "123"
		}()

		// 协程 2
		wg.Add(1)
		go func() {
			defer wg.Done()
			s = "abc"
		}()
		wg.Wait()

		// 赋值异常判断
		if s != "123" && s != "abc" {
			fmt.Printf("concurrent assignment error, i=%v s=%v", i, s)
			break
		}
	}
}

复制

上面的代码,因为字符串 123 和 abc 是等长的,所以并发赋值不管循环多少次都是绝对的安全。因为 struct 赋值蜕变成了一个数值型指针的赋值。

4.2 复合数据类型的并发赋值

4.2.1 指针(安全)

指针是保存另一个变量的内存地址的变量。指针的零值为 nil。

因为是内存地址,所以位宽为 32位(x86平台)或 64位(x64平台),赋值操作由一个机器指令即可完成,不能被中断,所以也不会出现并发赋值不安全的情况。

这在上面讨论 string 等长不同值并发赋值时,已经验证没有问题。

4.2.2 函数(安全)

Go 函数可以像值一样传递。

Go 函数定义形式如下:

代码语言:javascript
复制
func some_func_name(arguments) return_values

复制

定义函数类型时去掉函数名:

代码语言:javascript
复制
type TypeName func(arguments) return_values

复制

其中 TypeName 是自定义的类型名称。

下面是一个函数类型的使用示例:

代码语言:javascript
复制
package main

import "fmt"

func main() {
	// 定义函数类型的变量
    add := func(x, y int) int {
        return x + y
    }
    fmt.Println(add(1, 2))
}

// 函数作为形参
func doOperation(fn func(int, int) int, x, y int) int {
    return fn(x, y)
}

复制

函数类型的变量赋值时,实际上赋的是函数地址,一条机器指令便可以完成,所以并发赋值是安全的。

我们使用unsafe.Sizeof()可以查看函数类型的宽度(字节)。

代码语言:javascript
复制
type Add func(int, int) int
var add Add
fmt.Println(unsafe.Sizeof(add)) // 8

复制

下面验证一下函数变量并发赋值的安全性。

代码语言:javascript
复制
type Add func(int, int) int

func main() {
	var g Add

	var i int
	for ; i < 10000000; i++ {
		var wg sync.WaitGroup
		// 协程 1
		wg.Add(1)
		go func() {
			defer wg.Done()
			g = func(x, y int) int {
				return x + y
			}
		}()

		// 协程 2
		wg.Add(1)
		go func() {
			defer wg.Done()
			g = func(x, y int) int {
				return x - y
			}
		}()
		wg.Wait()

		// 赋值异常判断
		if !(g(1, 1) == 2 || g(1, 1) == 0) {
			fmt.Printf("concurrent assignment error, i=%v g=%+v", i, g)
			break
		}
	}
	if i == 10000000 {
		fmt.Println("no error")
	}
}

复制

运行输出:

代码语言:javascript
复制
no error

复制

4.2.2 数组、切片、字典、通道、接口(不安全)

数组、切片、字典、通道、接口,这些复合类型,除了数组,其他底层数据结构都是 struct,所以并发都不是安全的,当然数组并发赋值也是不安全的。

下面的讲解不会对所有类型一一验证,不过相关的底层数据我们应该着重了解一下。

数组

array 是相同类型值的集合,数组的长度是其类型的一部分。

数组赋值和传参都会拷贝整个数组的数据,所以数组不是引用类型。

数组的底层数据结构就是其本身,是一个相同类型不同值的顺序排列。所以如果数组位宽不大于 64 位且是 2 的整数次幂(8,16,32,64),那么其并发赋值其实也是安全的,只不过这个大部分情况并非如此,所以其并发赋值是不安全的。

下面以字节数组为例,看下位宽不大于 64 位的并发赋值安全的情况。

代码语言:javascript
复制
func main() {
	var g [4]byte

	var i int
	for ; i < 10000000; i++ {
		var wg sync.WaitGroup
		// 协程 1
		wg.Add(1)
		go func() {
			defer wg.Done()
			g = [...]byte{1, 2, 3, 4}
		}()

		// 协程 2
		wg.Add(1)
		go func() {
			defer wg.Done()
			g = [...]byte{3, 4, 5, 6}
		}()
		wg.Wait()

		// 赋值异常判断
		if !(g == [...]byte{1, 2, 3, 4} || g == [...]byte{3, 4, 5, 6}) {
			fmt.Printf("concurrent assignment error, i=%v g=%+v", i, g)
			break
		}
	}
	if i == 10000000 {
		fmt.Println("no error")
	}
}

复制

运行输出:

代码语言:javascript
复制
no error

复制

可以看到,位宽为 32 位的数组 [4]byte,虽然有四个元素,但是赋值时由一条机器指令完成,所以也是原子操作。

如果你把字节数组的长度换成下面这样子,即使没有超过 64 位,也需要多条指令完成赋值,因为 CPU 中并没有这样位宽的寄存器,需要拆分为多条指令来完成。

代码语言:javascript
复制
[3]byte
[5]byte
[7]byte

复制

切片

slice 也是相同类型值的集合,只不过切片是动态调整大小的,内部是对数组的引用,相当于动态数组。如上所述,数组的大小是固定的,因此切片为数组提供了更灵活的接口。

切片是一种引用类型,它内部由三个字段表示:

  • 数组地址
  • 数组长度
  • 容量大小

在源码包src/runtime/slice.go我们可以找到切片的底层数据结构:

代码语言:javascript
复制
type slice struct {
	array unsafe.Pointer
	len   int
	cap   int
}

复制

因为其是一个 struct,所以并发赋值是不安全的,这里不再以代码验证。

字典

map 是经常被使用的内置 key-value 型容器,是一个同类型元素的无序组,元素通过另一类型唯一键进行索引。

map 的底层结构也是一个 struct,定义于src/runtime/map.go

代码语言:javascript
复制
// A header for a Go map.
type hmap struct {
	// Note: the format of the hmap is also encoded in cmd/compile/internal/gc/reflect.go.
	// Make sure this stays in sync with the compiler's definition.
	count     int // # live cells == size of map.  Must be first (used by len() builtin)
	flags     uint8
	B         uint8  // log_2 of # of buckets (can hold up to loadFactor * 2^B items)
	noverflow uint16 // approximate number of overflow buckets; see incrnoverflow for details
	hash0     uint32 // hash seed

	buckets    unsafe.Pointer // array of 2^B Buckets. may be nil if count==0.
	oldbuckets unsafe.Pointer // previous bucket array of half the size, non-nil only when growing
	nevacuate  uintptr        // progress counter for evacuation (buckets less than this have been evacuated)

	extra *mapextra // optional fields
}

复制

map 并发读写会引发 panic,一般使用读写锁 sync.RWMutex 来保证安全。

通道

channel 在 goroutine 之间提供同步和通信。您可以将其视为 goroutines 通过其发送值和接收值的管道。操作符<-用于发送或接收数据,箭头方向指定数据流的方向。

代码语言:javascript
复制
ch <- val    	// Sending a value present in var variable to channel
val := <-cha	// Receive a value from  the channel and assign it to val variable

复制

因为 channel 通常用法是初始化后作为共享变量在 goroutine 之间提供同步和通信,很少会发生赋值,就是把一个 channel 赋给另一个 channel,所以这里就不过多讨论其并发赋值的安全性。如果真的有这种情况,那么只要知道其底层数据结构是个 struct,并发赋值时不安全的即可。

关于 channel 的底层数据接口可在 Go 源码src\runtime\chan.go

代码语言:javascript
复制
type hchan struct {
	qcount   uint           // total data in the queue
	dataqsiz uint           // size of the circular queue
	buf      unsafe.Pointer // points to an array of dataqsiz elements
	elemsize uint16
	closed   uint32
	elemtype *_type // element type
	sendx    uint   // send index
	recvx    uint   // receive index
	recvq    waitq  // list of recv waiters
	sendq    waitq  // list of send waiters

	// lock protects all fields in hchan, as well as several
	// fields in sudogs blocked on this channel.
	//
	// Do not change another G's status while holding this lock
	// (in particular, do not ready a G), as this can deadlock
	// with stack shrinking.
	lock mutex
}

复制

关于 channel 的用法和实现原理,感兴趣的同学可自行查阅资料探究,这里不再赘述。

接口

接口是 Go 中的一个类型,它是方法的集合。实现接口的所有方法的任何类型都属于该接口类型。接口的零值为 nil。

定义一个接口类型的变量后,如果具体类型实现了接口的所有方法,我们可以将任何具体类型的值赋给这个变量。

实际上 Go 中的接口有个特殊情况,就是空接口,其不包含任何方法。因此,默认情况下,所有具体类型都实现空接口。

如果编写的函数接受空接口,则可以向该函数传递任何类型。

代码语言:javascript
复制
package main

import "fmt"

func main() {
    test("thisisstring")
    test("10")
    test(true)
}

func test(a interface{}) {
    fmt.Printf("(%v, %T)\n", a, a)
}

复制

运行输出:

代码语言:javascript
复制
(thisisstring, string)
(10, string)
(true, bool)

复制

因为存在两种类型的接口,包含方法的非空接口和不包含任何方法的空接口,所以在底层实现上使用runtime.iface表示非空接口,使用runtime.eface表示空接口 interface{}。

在 Go 源码中 runtime 包下,我们可以找到 runtime.iface 和 runtime.eface 的定义。

代码语言:javascript
复制
type iface struct { // 16 字节
	tab  *itab
	data unsafe.Pointer
}

复制

这个结构体中有指向原始数据的指针 data 和 runtime.itab。

runtime.itab 结构体是接口类型的核心组成部分,每一个 runtime.itab 都占 32 字节,我们可以将其看成接口类型和具体类型的组合,它们分别用 inter 和 _type 两个字段表示:

代码语言:javascript
复制
type itab struct { // 32 字节
	inter *interfacetype
	_type *_type
	hash  uint32
	_     [4]byte
	fun   [1]uintptr
}

复制

除了 inter 和 _type 两个用于表示类型的字段之外,上述结构体中的另外两个字段也有自己的作用: hash 是对 _type.hash 的拷贝,当我们想将 interface 类型转换成具体类型时,可以使用该字段快速判断目标类型和具体类型 runtime._type 是否一致; fun 是一个动态大小的数组,它是一个用于动态派发的虚函数表,存储了一组函数指针。虽然该变量被声明成大小固定的数组,但是在使用时会通过原始指针获取其中的数据,所以 fun 数组中保存的元素数量是不确定的。

代码语言:javascript
复制
type eface struct { // 16 字节
	_type *_type
	data  unsafe.Pointer
}

复制

由于 interface{} 类型不包含任何方法,所以它的结构也相对来说比较简单,只包含指向底层数据和类型的两个指针。从上述结构我们也能推断出 Go 语言的任意类型都可以转换成 interface{}。

其中runtime._type是 Go 语言类型的运行时表示。下面是运行时包中的结构体,其中包含了很多类型的元信息,例如:类型的大小、哈希、对齐以及种类等。

代码语言:javascript
复制
type _type struct {
	size       uintptr
	ptrdata    uintptr
	hash       uint32
	tflag      tflag
	align      uint8
	fieldAlign uint8
	kind       uint8
	equal      func(unsafe.Pointer, unsafe.Pointer) bool
	gcdata     *byte
	str        nameOff
	ptrToThis  typeOff
}

复制

size 字段存储了类型占用的内存空间,为内存空间的分配提供信息; hash 字段能够帮助我们快速确定类型是否相等; equal 字段用于判断当前类型的多个对象是否相等,该字段是为了减少 Go 语言二进制包大小从 typeAlg 结构体中迁移过来的。

我们只需要对 runtime._type 结构体中的字段有一个大体的概念,不需要详细理解所有字段的作用和意义。

根据上面对接口底层结构的分析,我们可以得出如下结论:

接口底层数据结构包含两个字段,相互赋值时如果是相同具体类型不同值并发赋给一个接口,那么只有一个字段 data 的值是不同的,此时退化成指针的并发赋值,所以是安全的。但如果是不同具体类型的值并发赋给一个接口,那么并引发 panic。

不同具体类型并发赋值接口非安全验证如下:

代码语言:javascript
复制
func main() {
	var g interface{}

	var i int
	for ; i < 10000000; i++ {
		var wg sync.WaitGroup
		// 协程 1
		wg.Add(1)
		go func() {
			defer wg.Done()
			g = "a"
		}()

		// 协程 2
		wg.Add(1)
		go func() {
			defer wg.Done()
			g = 1
		}()
		wg.Wait()

		// 赋值异常判断
		v1, _ := g.(string)
		v2, _ := g.(int)
		if !(v1 == "a" || v2 == 1) {
			fmt.Printf("concurrent assignment error, i=%v g=%v ", i, g)
			break
		}
	}
	if i == 10000000 {
		fmt.Println("no error")
	}
}

复制

运行输出:

代码语言:javascript
复制
unexpected fault address 0x1fffffa8
fatal error: fault
[signal 0xc0000005 code=0x0 addr=0x1fffffa8 pc=0x5f5585]
...

复制

把上面的示例代码中协议 1 中的字符串换成一个 int 值,那么并发是安全的。

代码语言:javascript
复制
func main() {
	var g interface{}

	var i int
	for ; i < 10000000; i++ {
		var wg sync.WaitGroup
		// 协程 1
		wg.Add(1)
		go func() {
			defer wg.Done()
			g = 0
		}()

		// 协程 2
		wg.Add(1)
		go func() {
			defer wg.Done()
			g = 1
		}()
		wg.Wait()

		// 赋值异常判断
		v1, _ := g.(int)
		v2, _ := g.(int)
		if !(v1 == 0 || v2 == 1) {
			fmt.Printf("concurrent assignment error, i=%v g=%v ", i, g)
			break
		}
	}
	if i == 10000000 {
		fmt.Println("no error")
	}
}

复制

运行输出:

代码语言:javascript
复制
no error

复制

5.小结

Go 多协程并发的场景无处不在,并发对同一变量的赋值也是经常遇到。本文尝试探讨了 Go 中所有类型并发赋值的安全性。

(1)由一条机器指令完成赋值的类型并发赋值是安全的,这些类型有:字节型,布尔型、整型、浮点型、字符型、指针、函数。

(2)数组由一个或多个元素组成,大部分情况并发不安全。注意:当位宽不大于 64 位且是 2 的整数次幂(8,16,32,64),那么其并发赋值是安全的。

(3)struct 或底层是 struct 的类型并发赋值大部分情况并发不安全,这些类型有:复数、字符串、 数组、切片、字典、通道、接口。注意:当 struct 赋值时退化为单个字段由一个机器指令完成赋值时,并发赋值又是安全的。这种情况有: (a)实部或虚部相同的复数的并发赋值; (b)等长字符串的并发赋值; (c)同长度同容量切片的并发赋值; (d)同一种具体类型不同值并发赋给接口。

注意: 以上结论基于x86-64指令集架构,其他平台未作实际验证。

本文参与 腾讯云自媒体同步曝光计划,分享自作者个人站点/博客。
原始发表:2023-02-01,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客 前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 1.什么是并发安全
  • 2.struct 并发赋值安全吗
  • 3.如何保证并发赋值的安全性
  • 4.哪些类型并发赋值是安全的
    • 4.1 基本类型的并发赋值
      • 4.1.1 字节型、布尔型、整型、浮点型、字符型(安全)
      • 4.1.2 复数型(不安全)
      • 4.1.3 字符串(不安全)
    • 4.2 复合数据类型的并发赋值
      • 4.2.1 指针(安全)
      • 4.2.2 函数(安全)
      • 4.2.2 数组、切片、字典、通道、接口(不安全)
  • 5.小结
相关产品与服务
容器服务
腾讯云容器服务(Tencent Kubernetes Engine, TKE)基于原生 kubernetes 提供以容器为核心的、高度可扩展的高性能容器管理服务,覆盖 Serverless、边缘计算、分布式云等多种业务部署场景,业内首创单个集群兼容多种计算节点的容器资源管理模式。同时产品作为云原生 Finops 领先布道者,主导开源项目Crane,全面助力客户实现资源优化、成本控制。
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档