Go语言中array、slice、map的用法和细节分析

目录

【array】

数组的基本用法

数组的更多用法

【slice】

创建切片

切片的动态扩容 

使用 for 语句遍历切片

分割切片(子切片)

删除切片元素

切⽚之间共享存储空间

切片排序

【map】

创建map

map的增删改查

map的并发问题

使用map实现工厂模式

使用map实现集合

使用map返回给前端json数据

map的注意事项

扩展:关于深拷贝和浅拷贝


上一篇文章 中讲述了Go语言的基本数据类型:整型、浮点型、字符串的用法,除了基本数据类型以外,Go语言中还有复合数据类型,由多个相同类型或不同类型元素的值组合而成。Go 语言内置了如下的复合数据类型:数组(array)、切片(slice)、映射(map)、结构体(struct)、channel等。

【array】

Go 的数组类型包含两个属性:元素的类型 和 元素的个数(也就是数组长度),元素的类型可以是任意的 Go 原生类型或自定义类型,数组长度必须在声明数组的时候提供。如果两个数组的元素类型 与 数组长度  都是一样的则是等价数组,如果有一个属性不同则是两个不同的数组类型。数组在内存中占据着一整块连续的内存,这块内存全部空间都被用来表示数组元素。如果两个数组所分配的内存大小不同则是不同的数组类型。

数组的基本用法

使用 len() 函数可以获取数组的长度,使用 unsafe.Sizeof() 函数可以获取数组的总大小。

var arr2 = [6]int{1, 2, 3, 4, 5, 6}
fmt.Println("数组长度:", len(arr2))           // 6
fmt.Println("数组大小:", unsafe.Sizeof(arr2)) // 48 //64位平台上int 类型的大小为8,因此是6*8=48

可以指定数组长度,也可以用 ... 代替

f1 := [5]int{1, 2, 3, 4, 5}
f1 := [...]int{1, 2, 3, 4, 5}

可以使用下标赋值的方式对数组初始化,跳过某些元素

g := [...]int{1: 3, 6: 5}
fmt.Println(g)      //[0 3 0 0 0 0 5]
fmt.Println(len(g)) // 7

如果不显式初始化,那么数组中的元素值就是它类型的零值。比如下面的数组类型变量 arr1 的各个元素值都为 0

var arr1 [6]int
fmt.Println(arr1) // [0 0 0 0 0 0]

数组由零个或多个元素组成,一旦声明了,数组的长度就固定了,不能动态变化。数组的 长度len() 和 容量cap() 返回结果始终一样。

数组的更多用法

//go01/array.go

package main

import "fmt"

func main() {

	arr := [3]int{1, 2, 3}
	fmt.Println(arr)

	//for range 遍历数组
	for key, val := range arr {
		fmt.Print(key, "==>", val, ",") //0==>1,1==>2,2==>3,
	}
	fmt.Println()

	fmt.Println(arr[2]) //打印第三个数值:3
	//fmt.Println(arr[4]) //invalid array index 4 (out of bounds for 4-element array)
	fmt.Printf("长度:%d, 容量:%d \n", len(arr), cap(arr))

	//因为数组定长,长度和容量相同
	arr[0] = 100
	fmt.Println(arr[0])

	//字符串数组
	var e = [5]string{"name", "age", "sex"}
	fmt.Println(e)

	f := [...]int{1, 2, 3, 4, 5}
	fmt.Println(f)

	g := [...]int{1: 3, 6: 5}
	fmt.Println(g)      //[0 3 0 0 0 0 5]
	fmt.Println(len(g)) // 7

    //数组分割
	arr3 := [...]int{1, 2, 3, 4, 5}
	arr3Sec := arr3[:3]
	fmt.Println(arr3Sec) //[1 2 3]

	//冒泡排序
	array := [5]int{15, 23, 8, 10, 7}
	for i := 1; i < len(array); i++ {
		for j := 0; j < len(array)-i; j++ {
			if array[j] > array[j+1] {
				array[j], array[j+1] = array[j+1], array[j]
			}
		}
	}
	fmt.Println(array) //[7 8 10 15 23]

	//二维数组
	a := [3][4]int{{1, 2, 3, 4}, {5, 6, 7, 8}, {9, 10, 11, 12}}
	fmt.Println(a)
	fmt.Printf("二维数组的地址:%p,长度:%d \n", &a, len(a))
}

【slice】

数组在使用上有两点不足:(1)固定的元素个数,(2)数组是值传递,会导致开销较大

因此,Go引入了切片Slice。Go 语言的切片类型属于引用类型,同属引用类型的还有字典类型、通道类型、函数类型等;而 Go 语言的数组类型则属于值类型,同属值类型的有基础数据类型以及结构体类型。从传递成本的角度讲,引用类型的值往往要比值类型的值低很多。Go语言中切片的定义如下:

type slice struct {
    array unsafe.Pointer //指向底层数组的指针
    len int //切片的长度,即切片中当前元素的个数
    cap int //底层数组的长度,也是切片的最大容量,cap 值永远大于等于 len 值
}

创建切片

切片和数组相比少了长度的定义。但切片也有自己的长度,只不过这个长度是随着切片中元素个数的变化而变化的。使用 len() 获取切片的长度,使用 cap() 获取切片的容量。

sli := []int{1, 2, 3, 4, 5, 6}
fmt.Printf("len=%d cap=%d slice=%v\n", len(sli), cap(sli), sli) //len=6 cap=6 slice=[1 2 3 4 5 6]

可以通过 make 函数来创建切片,并指定底层数组的长度

sli1 := make([]byte, 6, 10) // 其中10为cap值,即底层数组长度,6为切片的初始长度
fmt.Printf("sli1:%d, len:%d, cap:%d \n", sli1, len(sli1), cap(sli1)) //sli1:[0 0 0 0 0 0], len:6, cap:10
sli2 := make([]byte, 6) //如果没有在 make 中指定 cap 参数,那么底层数组长度 cap 就等于 len
fmt.Printf("sli2:%d, len:%d, cap:%d \n", sli2, len(sli2), cap(sli2)) //sli2:[0 0 0 0 0 0], len:6, cap:6

基于一个已存在的数组创建切片

//array[low : high : max]
arrx := [10]int{1, 2, 3, 4, 5, 6, 7, 8, 9, 10}
slx3 := arrx[3:7:9]
fmt.Println(slx3) //[4 5 6 7]

切片只能和nil比较,不能两个切片对比 

a := []int{1, 2, 3, 4}
b := []int{1, 2, 3, 4}
// fmt.Println(a == b) //切片只能和nil比较,所以此处会报错
fmt.Println(a, b)

切片的动态扩容 

使用 append() 对切片扩容

//基于数组创建切片
//采用 array[low : high : max]语法基于一个已存在的数组创建切片
arrx := [10]int{1, 2, 3, 4, 5, 6, 7, 8, 9, 10}
slx3 := arrx[3:7:9]
fmt.Println(slx3) //[4 5 6 7]

//数组和切片的关系变化
var arr = [8]int{1, 2, 3, 4, 5, 6, 7, 8}
var slice = arr[0:3]
fmt.Println(slice, &arr[0] == &slice[0]) //[1 2 3] true

//切片扩容
slice[1] = 22             //把切片的第1位改成22: [1 22 3]
slice = append(slice, 11) //给切片追加元素(也就是扩容)
slice = append(slice, 12)
slice = append(slice, 13)
slice = append(slice, 14)
fmt.Println(arr) //[1 22 3 11 12 13 14 8]
fmt.Printf("slice=%d,len=%d,cap=%d \n", slice, len(slice), cap(slice)) //slice=[1 22 3 11 12 13 14],len=7,cap=8
fmt.Println(&slice[0] == &arr[1]) //false, 此时原来的arr和现在的slice已经不是一个内存地址了

数组和切片都可以通过[start:end] 的形式来获取子切片,左闭右开:
1. arr[start:end],获得[start, end)之间的元素
2. arr[:end],获得[0, end) 之间的元素
3. arr[start:],获得[start, len(arr))之间的元素 

使用 for 语句遍历切片

//遍历切片
for i := 0; i < len(slice); i++ {
	fmt.Print(slice[i], " ") //1 22 3 11 12 13 14
}
fmt.Println()

for i, v := range slice {
	fmt.Printf("%d => %d\n", i, v)
}
/*
	0 => 1
	1 => 22
	2 => 3
	3 => 11
	4 => 12
	5 => 13
	6 => 14
*/

分割切片(子切片)

sli := []int{1, 2, 3, 4, 5, 6}
fmt.Printf("len=%d cap=%d slice=%v\n", len(sli), cap(sli), sli) //len=6 cap=6 slice=[1 2 3 4 5 6]

fmt.Println("sli[1] ==", sli[1])   //sli[1] == 2
fmt.Println("sli[:] ==", sli[:])   //sli[:] == [1 2 3 4 5 6]
fmt.Println("sli[1:] ==", sli[1:]) //sli[1:] == [2 3 4 5 6]
fmt.Println("sli[:4] ==", sli[:4]) //sli[:4] == [1 2 3 4]

fmt.Println("sli[0:3] ==", sli[0:3]) //sli[0:3] == [1 2 3]
fmt.Printf("len=%d cap=%d slice=%v\n", len(sli[0:3]), cap(sli[0:3]), sli[0:3]) //len=3 cap=6 slice=[1 2 3]

fmt.Println("sli[0:3:4] ==", sli[0:3:4]) //sli[0:3:4] == [1 2 3]
fmt.Printf("len=%d cap=%d slice=%v\n", len(sli[0:3:4]), cap(sli[0:3:4]), sli[0:3:4]) //len=3 cap=4 slice=[1 2 3]

删除切片元素

sli = []int{1, 2, 3, 4, 5, 6, 7, 8}
fmt.Printf("len=%d cap=%d slice=%v\n", len(sli), cap(sli), sli) //len=8 cap=8 slice=[1 2 3 4 5 6 7 8]

//删除尾部 2 个元素
fmt.Printf("len=%d cap=%d slice=%v\n", len(sli[:len(sli)-2]), cap(sli[:len(sli)-2]), sli[:len(sli)-2]) //len=6 cap=8 slice=[1 2 3 4 5 6]

//删除开头 2 个元素
fmt.Printf("len=%d cap=%d slice=%v\n", len(sli[2:]), cap(sli[2:]), sli[2:]) //len=6 cap=6 slice=[3 4 5 6 7 8]

//删除中间 2 个元素
sli = append(sli[:3], sli[3+2:]...)
fmt.Printf("len=%d cap=%d slice=%v\n", len(sli), cap(sli), sli) //len=6 cap=8 slice=[1 2 3 6 7 8]

切⽚之间共享存储空间

year := []string{"Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep",
	"Oct", "Nov", "Dec"}
Q2 := year[3:6]
fmt.Println(Q2, len(Q2), cap(Q2)) //[Apr May Jun] 3 9
summer := year[5:8]
fmt.Println(summer, len(summer), cap(summer)) //[Jun Jul Aug] 3 7
summer[0] = "Unknow"                          //这里修改子切片的值,原切片值也会被修改
fmt.Println(Q2)                               //[Apr May Unknow]
fmt.Println(year)    

切片排序

//切片排序,可以使用冒泡
sl := []int{15, 23, 8, 10, 7}
for i := 1; i < len(sl); i++ {
	for j := 0; j < len(sl)-i; j++ {
		if sl[j] > sl[j+1] {
			sl[j], sl[j+1] = sl[j+1], sl[j]
		}
	}
}
fmt.Println(sl) //[7 8 10 15 23]

//也可以直接使用sort包下的排序方法
sl2 := []int{19, 2, 8, 11, 30}
sort.Ints(sl2)
fmt.Println(sl2) //[2 8 11 19 30]

//字符串切片排序
sl3 := []string{"Apple", "Windows", "Orange", "abc", "你好", "acd", "acc"}
sort.Strings(sl3)
fmt.Println(sl3) //[Apple Orange Windows abc acc acd 你好]

切片总结:切片是一个引用类型的容器,底层指向的是一个数组,切片是对数组的连续片段引用。切片可以是整个数组,也可以是数组的一部分,保存有数组开始的下标、长度、容量;注意切片的 len() 和 cap() 返回的结果不一定相同。使用 append() 方法向切片的尾部追加元素。

  • 每一个切片引用了一个底层数组
  • 切片本身不存储任何数据,都是这个底层数组存储,所以修改切片也就是修改这个数组中的数据
  • 当向切片中添加数据时,如果没有超过容量,直接添加,如果超过容量,自动扩容(成倍增长)
  • 切片一旦扩容,就是重新指向一个新的底层数组

【map】

map 一般被翻译为映射、哈希表或字典。Go语言中的map表示一组无序的键值对,也就是key/value结构,并且每个key都是唯一的,key 与 value 的类型可以相同也可以不同。如果两个 map 的 key 元素类型相同,value 元素类型也相同,那么它们就是同一个 map 类型,否则就是不同的 map 类型。

在 Go 语言中,函数类型、map 类型自身,以及切片只支持与 nil 的比较,而不支持同类型两个变量的比较。 map的 key 不能使用 函数类型、map 类型、切片类型,为什么?

因为Go语言规定了,键类型的值必须要支持 == 和 != 操作,由于函数、map、切片 的值并不支持判断等于或不等于的操作,所以map的键类型不能是这三种类型。另外,如果键的类型是接口类型的,那么键值的实际类型也不能是上述三种类型,否则在程序运行过程中会引发 panic

s1 := make([]int, 1)
s2 := make([]int, 2)
f1 := func() {}
f2 := func() {}
t1 := make(map[int]string)
t2 := make(map[int]string)
//println(s1 == s2) // 错误:invalid operation: s1 == s2 (slice can only be compared to nil)
//println(f1 == f2) // 错误:invalid operation: f1 == f2 (func can only be compared to nil)
//println(t1 == t2) // 错误:invalid operation: t1 == t2 (map can only be compared to nil)

//key作为接口类型时,也不能使用 函数、map、切片
var badMap = map[interface{}]int{
	"1":      1,
	[]int{2}: 2, //panic: runtime error: hash of unhashable type []int
	3:        3,
}
//如果把上面的 []int{2}: 2 改成 [1]int{2}: 2,即把切片类型改成数组类型就没问题了
var badMap = map[interface{}]int{
	"1":       1,
	[1]int{2}: 2, //panic: runtime error: hash of unhashable type []int
	3:         3,
}
fmt.Println(badMap) //map[3:3 1:1 [2]:2]

创建map

注意:对零值状态的 map 变量直接进行操作,就会导致运行时异常(panic)

//创建map
var map1 map[int]string //没有初始化,值为 nil
// map1[5] = "test" //报错:panic: assignment to entry in nil map(因为未初始化)
var map2 = make(map[int]string) //初始化
// map2[5] = "test" //不会报错
var map3 = map[string]int{"Go": 98, "Python": 87, "Java": 79, "Html": 93}
fmt.Println(map1) //map[]
fmt.Println(map2) //map[]
fmt.Println(map3) //map[Go:98 Html:93 Java:79 Python:87]

fmt.Println(map1 == nil) //true
fmt.Println(map2 == nil) //false
fmt.Println(map3 == nil) //false

//如果没有初始化就赋值,会报错 panic: assignment to entry in nil map
//因此可以这样判断
if map1 == nil {
	map1 = make(map[int]string)
	fmt.Println(map1 == nil) //false
}

//或者使用短变量
mx := map[string]int{}
mx["key"] = 1
fmt.Println(mx) //map[key:1]

//初始化并指定容量
map4 := make(map[int]string, 8) // 指定初始容量为8

//稍微复杂一点的map初始化
type Position struct {
	x float64
	y float64
}
m2 := map[Position]string{
	Position{29.935523, 52.568915}:  "school",
	Position{25.352594, 113.304361}: "shopping-mall",
	Position{73.224455, 111.804306}: "hospital",
}
fmt.Println("m2", m2) //map[{25.352594 113.304361}:shopping-mall {29.935523 52.568915}:school {73.224455 111.804306}:hospital]

//上面的初始化方式可以简化一下:Go 允许省略字面值中的元素类型
m3 := map[Position]string{
	{29.935523, 52.568915}:  "school",
	{25.352594, 113.304361}: "shopping-mall",
	{73.224455, 111.804306}: "hospital",
}
fmt.Println("m3", m3) //map[{25.352594 113.304361}:shopping-mall {29.935523 52.568915}:school {73.224455 111.804306}:hospital]

map的增删改查

//存储键值对到map中
map1[1] = "语文"
map1[2] = "数学"
map1[3] = "英语"
map1[4] = "物理"
map1[7] = ""
fmt.Println(map1) //map[1:语文 2:数学 3:英语 4:物理 7:]

//使用 for...range... 遍历map,打印结果无序
for k, v := range map1 {
	fmt.Print(k, "=>", v, " ") //1=>语文 2=>数学 3=>英语 4=>物理 7=>
}
fmt.Println()

//将map的key组成新的slice
keys := make([]int, 0, len(map1))
for k, _ := range map1 {
	keys = append(keys, k)
}
fmt.Println(keys) //[1 2 3 4 7]

//根据key获取对应的value值
fmt.Println(map1[4])  //物理
fmt.Println(map1[40]) //""

//在访问的 Key 不存在时,仍会返回零值,因此不能通过返回 nil 来判断元素是否存在
v1, ok := map1[40]
if ok {
	fmt.Println("对应的数值是:", v1)
} else {
	fmt.Println("操作的key不存在")
}

//如果并不关心某个键对应的 value,而只关心某个键是否在于 map 中,可以使用空标识符替代变量 v
_, ok = map1[40]
fmt.Println(ok) //false

//修改map的数据
map1[3] = "历史"
fmt.Println(map1) //map[1:语文 2:数学 3:历史 4:物理 7:]

//删除map的数据
delete(map1, 3)
fmt.Println(map1) //map[1:语文 2:数学 4:物理 7:]

//获取map的长度
fmt.Println(len(map1)) //4

//获取map的类型
map11 := make(map[int]string)
map12 := make(map[string]float64)
fmt.Printf("%T\n", map11) //map[int]string
fmt.Printf("%T\n", map12) //map[string]float64

map只有长度len,没有容量cap
注意:如果使用的map的key是int类型,建议优先使用slice,效率会比较高,内存占用比较少

map和切片都是引用类型, 因此当 map 类型变量作为参数被传递给函数或方法的时候,在函数内部对 map 的修改会影响函数外部的值。

因为 map 可以自动扩容,map 中数据元素的 value 位置可能在这一过程中发生变化,所以 Go 不允许获取 map 中 value 的地址

func main() {
    //map是引用传递
	m := make(map[int]string)
	m[1] = "value1"
	m[3] = "value2"
	foo(m)
	fmt.Println(m) //map[1:changed 3:value2]

    //不允许获取 map 中 value 的地址
    p := &m[1] //invalid operation: cannot take address of m[1] (map index expression of type string)
}

func foo(data map[int]string) {
	data[1] = "changed"
}

map的并发问题

map 不是并发写安全的,不支持同时并发读写。如果对 map 实例进行并发读写,程序运行时就会抛出异常。

func main() {
	m := map[int]int{
		1: 11,
		2: 12,
		3: 13,
	}

	go func() {
		for i := 0; i < 1000; i++ {
			doIteration(m)
		}
	}()

	go func() {
		for i := 0; i < 1000; i++ {
			doWrite(m)
		}
	}()
	time.Sleep(5 * time.Second)
    
    //执行后报错:fatal error: concurrent map iteration and map write
    //把上面的 doIteration() 和 doWrite() 任意一个注释掉就OK了
}

func doIteration(m map[int]int) {
	for k, v := range m {
		_ = fmt.Sprintf("[%d, %d] ", k, v)
	}
}
func doWrite(m map[int]int) {
	for k, v := range m {
		m[k] = v + 1
	}
}

map 如果只是并发读或只是并发写是没有问题的。另外,Go 1.9 版本中引入了支持并发写安全的 sync.Map 类型,可以用来在并发读写的场景下替换掉 map。关于并发相关的知识点后面再细说。

使用map实现工厂模式

map 的 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 } //计算立方
fmt.Println(m[1](2)) //2
fmt.Println(m[2](2)) //4
fmt.Println(m[3](2)) //8

使用map实现集合

redis中有个“集合”的类型,可以存储无序的数据,无序集合的特点: 唯一性,无序性,确定性。具体使用方法参考:redis笔记04-无序集合和有序集合_redis 有序集合,无序集合_浮尘笔记的博客-CSDN博客

如何在Go中实现一个类似于redis的集合?可以使用map,将map的值设为bool类型,代码如下:

findNum := 1
mySet := map[int]bool{}
mySet[1] = true             //添加元素
mySet[3] = true             //添加元素
fmt.Println(mySet[findNum]) //判断元素是否存在:true
fmt.Println(len(mySet))     //获取元素个数
delete(mySet, 1)            //删除元素
fmt.Println(mySet[findNum]) //false

使用map返回给前端json数据

/*多个map组合成为slice,模拟返回给客户端的接口列表*/
//创建多个map存储用户的信息
user1 := make(map[string]string)
user1["name"] = "张三"
user1["age"] = "30"
user1["sex"] = "男"
user2 := map[string]string{"name": "李四", "age": "28", "sex": "女"}
user3 := map[string]string{"name": "王五", "age": "21", "sex": "女"}
//将map存入到slice中
list := make([]map[string]string, 0, 3)
list = append(list, user1)
list = append(list, user2)
list = append(list, user3)

//遍历结果
for i, val := range list {
	fmt.Printf("用户 %d:", i+1)
	fmt.Printf("姓名:%s,", val["name"])
	fmt.Printf("年龄:%s,", val["age"])
	fmt.Printf("性别:%s\n", val["sex"])
}

//将结果输出为json
result, _ := json.Marshal(list)
fmt.Println(string(result)) //[{"age":"30","name":"张三","sex":"男"},{"age":"28","name":"李四","sex":"女"},{"age":"21","name":"王五","sex":"女"}]

/*二维结构*/
maps := make(map[string]map[string]string)
maps["user"] = map[string]string{"name": "zhangsan", "age": "30"}
maps["fruit"] = map[string]string{"name": "apple", "price": "6"}
res, _ := json.Marshal(maps)
fmt.Println(string(res)) //{"fruit":{"name":"apple","price":"6"},"user":{"age":"30","name":"zhangsan"}}

/*封装*/
res := make(map[string]interface{})
res["code"] = 200
res["msg"] = "success"
res["data"] = map[string]interface{}{
	"username": "Tom",
	"age":      "30",
	"hobby":    []string{"读书", "爬山"},
}
fmt.Println("map data :", res) // map[code:200 data:map[age:30 hobby:[读书 爬山] username:Tom] msg:success]

//序列化
jsons, errs := json.Marshal(res)
if errs != nil {
	fmt.Println("json marshal error:", errs)
}
fmt.Println("--- map to json ---")
fmt.Println("json data :", string(jsons)) // {"code":200,"data":{"age":"30","hobby":["读书","爬山"],"username":"Tom"},"msg":"success"}

//反序列化
res2 := make(map[string]interface{})
errs = json.Unmarshal([]byte(jsons), &res2)
if errs != nil {
	fmt.Println("json marshal error:", errs)
}
fmt.Println("--- json to map ---")
fmt.Println("map data :", res2) // map[code:200 data:map[age:30 hobby:[读书 爬山] username:Tom] msg:success]

map的注意事项

  • map和切片都是引用传递
  • map没有cap()容量
  • map 不是线程安全的,不支持并发读写
  • map中的元素是无序的,每次遍历得到的顺序可能不一样
  • map中新增的元素如果key已经存在,会覆盖已存在的key的值

扩展:关于深拷贝和浅拷贝

深拷贝:拷贝的是数据本身。值类型的数据默认都是深拷贝:array,int,float,string,bool,struct
浅拷贝:拷贝的是数据地址。导致多个变量指向同一块内存,引用类型的数据,默认都是浅拷贝:slice,map,因为切片是引用类型的数据,直接拷贝的是地址。使用copy() 可以实现切片的拷贝。

sla := []int{1, 2, 3, 4}
slb := []int{7, 8, 9}
copy(slb[1:], sla[2:]) //将sla中的部分元素(3,4)拷贝到slb中
fmt.Println(sla)       //[1 2 3 4]
fmt.Println(slb)       //[7 3 4]

本篇内容涉及的源代码请参考:go-demo-2023: Go语言基本用法和实用笔记 - Gitee.com

【路虽远,行则将至。加油!】

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

码农兴哥

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值