golang从入门到放弃【二】

常量

Go语言中的常量使用关键字const定义,用于存储不会改变的数据,常量是在编译时被创建的,即使定义在函数内部也是如此,并且只能是布尔型数字型(整数型、浮点型和复数)和字符串型

由于编译时的限制,定义常量的表达式必须为能被编译器求值的常量表达式。

声明格式:

const name [type] = value

const a1 int8 = 9

type可以省略,和变量声明一样,可以批量声明多个常量。

所有常量的运算都可以在编译期完成,常量间的所有算术运算、逻辑运算、比较运算的结果、对常量的类型转换操作或以下函数调用都是返回常量。len、cap、real、imag、complex 和 unsafe.Sizeof。

如果是批量声明的常量,除了第一个外其它的常量右边的初始化表达式都可以省略,如果省略初始化表达式则表示使用前面常量的初始化表达式,对应的常量类型也是一样的。例如:

const a1 int8 = 9
const (
	a = 1
	b
	c = 13.0
	d
)

func main() {
	fmt.Println(a)
	fmt.Printf("%d %d %.2f %.2f\n", a, b, c, d)
}

iota常量生成器

常量声明可以使用 iota 常量生成器初始化,它用于生成一组以相似规则初始化的常量,但是不用每行都写一遍初始化表达式。

在一个 const 声明语句中,在第一个声明的常量所在的行,iota 将会被置为 0,然后在每一个有常量声明的行加1

const (
	Sunday = iota
	Monday
	Tuesday
	Wednesday
	Thursday
	Friday
	Saturday
)

func main() {
	fmt.Println(Sunday, Monday, Tuesday, Wednesday, Thursday, Friday, Saturday)
}

指针

指针在Go语言中分为两种:

  • 类型指针,允许对这个指针类型的数据进行修改,传递数据可以直接使用指针,而无须拷贝数据,类型指针不能进行偏移和运算。
  • 切片,由指向起始元素的原始指针、元素数量和容量组成。

指针类型变量拥有指针高效访问的特点,又不会发生指针偏移,从而避免了非法修改关键性数据的问题。垃圾回收也比较容易对不会发生偏移的指针进行检索和回收。

切片比原始指针具备更强大的特性,而且更为安全。切片在发生越界时,运行时会报出宕机,并打出堆栈,而原始指针只会崩溃。

理解指针

在内存中开辟了一片空间,空间内存放着数值,这片空间在整个内存当中,有一个唯一的地址,用来进行标识,指向这个地址的变量就称为指针。

一个指针变量可以指向任何一个值的内存地址,它所指向的值的内存地址在 32 和 64 位机器上分别占用 4 或 8 个字节,占用字节的大小与所指向的值的大小无关。

当一个指针被定义后没有分配到任何变量时,它的默认值为 nil。每个变量在运行时都拥有一个地址,这个地址代表变量在内存中的位置。

Go语言中使用在变量名前面添加&操作符(前缀)来获取变量的内存地址(取地址操作),格式如下:

//其中 v 代表被取地址的变量,变量 v 的地址使用变量 ptr 进行接收,ptr 的类型为*T,称做 T 的指针类型,*代表指针。
ptr := &v    // v 的类型为 T

func main() {
	a := 1
	str := "贝塔"
	var aptr *int = &a
	var sptr *string = &str
	fmt.Printf("%p %p \n", aptr, sptr)
}

变量、指针和地址三者的关系是,每个变量都拥有地址,指针的值就是地址

当使用&操作符对普通变量进行取地址操作并得到变量的指针后,可以对指针使用*操作符,也就是指针取值

取地址操作符&和取值操作符*是一对互补操作符,&取出地址,*根据地址取出地址指向的值

变量、指针地址、指针变量、取地址、取值的相互关系和特性如下:

  • 对变量进行取地址操作使用&操作符,可以获得这个变量的指针变量。
  • 指针变量的值是指针地址。
  • 对指针变量进行取值操作使用*操作符,可以获得指针变量指向的原变量的值。

指针修改值

可以通过指针修改指针指向的值。

func main() {
	var a = 10
    //	10
	fmt.Printf("main方法中的值=%d\n", a)
	update1(a)
    // 10
	fmt.Printf("update1执行后,main方法中的值=%d\n", a)
	update2(&a)
    // 99
	fmt.Printf("update2执行后,main方法中的值=%d\n", a)
}

func update1(a int) {
	a = 66
    // 66
	fmt.Printf("update1方法内=%d\n", a)
}

func update2(a *int) {
	*a = 99
    // 99
	fmt.Printf("update2方法内的值=%d\n", *a)
}

创建指针

通过new关键字创建,格式:new(类型)

func main() {
	ptr := new(string)
	*ptr = "贝塔贝塔"
	fmt.Println(*ptr)
}

练习

获取命令行的输入信息

var mode = flag.String("name", "", "请输入你的名字")

func main() {
	flag.Parse()
	fmt.Printf("你的名字是:%s", *mode)
}

>  go run .\main2.go --name 贝塔贝塔

变量的生命周期

变量的生命周期指的是在程序运行期间变量有效存在的时间间隔。

变量的生命周期分为:

  1. 全局变量:它的生命周期和整个程序的运行周期是一致的;
  2. 局部变量:它的生命周期则是动态的,从创建这个变量的声明语句开始,到这个变量不再被引用为止;
  3. 形式参数和函数返回值:都属于局部变量,在函数被调用的时候创建,函数调用结束后被销毁。

go的内存中应用了两种数据结构用于存放变量:

  1. 堆(heap):堆是用于存放进程执行中被动态分配的内存段。它的大小并不固定,可动态扩张或缩减。当进程调用 malloc 等函数分配内存时,新分配的内存就被动态加入到堆上(堆被扩张)。当利用 free 等函数释放内存时,被释放的内存从堆中被剔除(堆被缩减);
  2. 栈(stack):栈又称堆栈, 用来存放程序暂时创建的局部变量,也就是我们函数的大括号{ }中定义的局部变量。

栈是先进后出,往栈中放元素的过程,称为入栈,取元素的过程称为出栈。

栈可用于内存分配,栈的分配和回收速度非常快

在程序的编译阶段,编译器会根据实际情况自动选择或者上分配局部变量的存储空间,不论使用 var 还是 new 关键字声明变量都不会影响编译器的选择。

var global *int
//	变量 x 必须在堆上分配,因为它在函数退出后依然可以通过包一级的 global 变量找到,虽然它是在函数内部定义的。这个局部变量 x 从函数 f 中逃逸了。
func f() {
    var x int
    x = 1
    global = &x
}
// 当函数 g 返回时,变量 y 不再被使用,也就是说可以马上被回收的。因此,y 并没有从函数 g 中逃逸,编译器可以选择在栈上分配 *y 的存储空间,也可以选择在堆上分配,然后由Go语言的 GC(垃圾回收机制)回收这个变量的内存空间。
func g() {
    y := new(int)
    *y = 1
}

类型别名

//	定义IntNew为int类型 ,定义之后 IntNew为一种新的类型
type IntNew int
//	TypeAlias 只是 Type 的别名,本质上 TypeAlias 与 Type 是同一个类型
type IntAlias = int

func main() {
	var a IntNew
	var b IntAlias
    //	aType=main.IntNew
	fmt.Printf("aType=%T\n", a)
    //	bType=int
	fmt.Printf("bType=%T\n", b)
}

注释

注释主要分成两类,分别是单行注释和多行注释。

  • 单行注释简称行注释,是最常见的注释形式,可以在任何地方使用以//开头的单行注释;
  • 多行注释简称块注释,以/*开头,并以*/结尾,且不可以嵌套使用,多行注释一般用于包的文档描述或注释成块的代码片段。

每一个包都应该有相关注释,在使用 package 语句声明包名之前添加相应的注释,用来对包的功能及作用进行简要说明。在 package 语句之前的注释内容将被默认认为是这个包的文档说明。一个包可以分散在多个文件中,但是只需要对其中一个进行注释说明即可。

关键字和标识符

关键字一共有 25 个:

breakdefaultfuncinterfaceselect
casedefergomapstruct
chanelsegotopackageswitch
constfallthroughifrangetype
continueforimportreturnvar

标识符指对各种变量、方法、函数等命名时使用的字符序列,标识符由若干个字母、下划线_、和数字组成,且第一个字符必须是字母。

下划线_是一个特殊的标识符,称为空白标识符

标识符的命名需要遵守以下规则:

  • 由 26 个英文字母、0~9、_组成;
  • 不能以数字开头,例如 var 1num int 是错误的;
  • Go语言中严格区分大小写;
  • 标识符不能包含空格;
  • 不能以系统保留关键字作为标识符,比如 break,if 等等。

命名标识符时还需要注意以下几点:

  • 标识符的命名要尽量采取简短且有意义;
  • 不能和标准库中的包名重复;
  • 为变量、函数、常量命名时采用驼峰命名法,例如 stuName、getVal;

预定义标识符一共有 36 个,主要包含基础数据类型和内置函数,这些预定义标识符也不可以当做标识符来使用。

appendboolbytecapclosecomplexcomplex64complex128uint16
copyfalsefloat32float64imagintint8int16uint32
int32int64iotalenmakenewnilpanicuint64
printprintlnrealrecoverstringtrueuintuint8uintptr

运算符优先级

优先级值越大,表示优先级越高。

优先级分类运算符结合性
1逗号运算符,从左到右
2赋值运算符=、+=、-=、*=、/=、 %=、 >=、 <<=、&=、^=、|=从右到左
3逻辑或||从左到右
4逻辑与&&从左到右
5按位或|从左到右
6按位异或^从左到右
7按位与&从左到右
8相等/不等==、!=从左到右
9关系运算符<、<=、>、>=从左到右
10位移运算符<<、>>从左到右
11加法/减法+、-从左到右
12乘法/除法/取余*(乘号)、/、%从左到右
13单目运算符!、*(指针)、& 、++、–、+(正号)、-(负号)从右到左
14后缀运算符( )、[ ]、->从左到右

字符串转换

整数与字符串

func main() {
	str := "666"
	iVal, _ := strconv.Atoi(str)
	fmt.Printf("%d %T \n", iVal, iVal)

	sVal := strconv.Itoa(iVal)
	fmt.Printf("%s %T \n", sVal, sVal)
}

浮点型与字符串

func main() {
	str := "3.1415"
	fValue, _ := strconv.ParseFloat(str, 64)
	fmt.Printf("%f %T \n", fValue, fValue)
	//4个参数,1:要转换的浮点数 2. 格式标记(b、e、E、f、g、G)
	//3. 精度 4. 指定浮点类型(32:float32、64:float64)
	// 格式标记:
	// ‘b’ (-ddddp±ddd,二进制指数)
	// ‘e’ (-d.dddde±dd,十进制指数)
	// ‘E’ (-d.ddddE±dd,十进制指数)
	// ‘f’ (-ddd.dddd,没有指数)
	// ‘g’ (‘e’:大指数,‘f’:其它情况)
	// ‘G’ (‘E’:大指数,‘f’:其它情况)
	//
	// 如果格式标记为 ‘e’,‘E’和’f’,则 prec 表示小数点后的数字位数
	// 如果格式标记为 ‘g’,‘G’,则 prec 表示总的数字位数(整数部分+小数部分)
	sValue := strconv.FormatFloat(fValue, 'f', -1, 64)
	fmt.Printf("%s %T \n", sValue, sValue)
}

练习

var level = 1
var exp = 0

func main() {
	fmt.Println("请输入你的角色名")
	reader := bufio.NewReader(os.Stdin)
	input, err := reader.ReadString('\n')
	if err != nil {
		panic(err)
	}
	name := input[:len(input)-1]
	fmt.Printf("%s,您好,欢迎来到打怪兽,目前等级%d\n", name, level)
	s := `你遇到一个史莱姆,选择战斗还是逃跑
1:战斗
2:逃跑
exit:退出游戏`
	fmt.Println(s)
	for {
		input, err := reader.ReadString('\n')
		if err != nil {
			panic(err)
		}
		selector := input[:len(input)-1]
		switch selector {
		case "1":
			exp += 10
			fmt.Println("您杀死了史莱姆,获得了10经验!")
			if exp <= 10 {
				level = 2
			} else if exp <= 20 {
				level = 3
			} else if exp <= 30 {
				level = 4
			} else {
				level = 999
			}
			fmt.Printf("您目前的等级为:%d\n", level)
			continue
		case "2":
			fmt.Println("逃跑成功!------------")
			fmt.Println(s)
			continue
		case "exit":
			fmt.Println("退出游戏!")
			os.Exit(0)
		default:
			fmt.Println("非法指令,请重新输入!")
		}
	}

}

数组

数组是一个由固定长度的特定类型元素组成的序列,一个数组可以由零个或多个元素组成。

因为数组的长度是固定的,所以在Go语言中很少直接使用数组。

  • 数组变量名:数组声明及使用时的变量名。
  • 元素数量:数组的元素数量,可以是一个表达式,但最终通过编译期计算的结果必须是整型数值,元素数量不能含有到运行时才能确认大小的数值。
  • Type:可以是任意基本类型,包括数组本身,类型为数组本身时,可以实现多维数组。
var 数组变量名 [元素数量]Type
//默认数组中的值是类型的默认值
var arr [3]int

func main() {
    // 通过下标获取值
	fmt.Printf("%d %d %d \n", arr[0], arr[1], arr[2])
	// 通过for range 获取索引及值
	for idx, val := range arr {
		fmt.Printf("idx = %d  value = %d |", idx, val)
	}
	fmt.Println()
    // 通过for range 获取值
	for _, val := range arr {
		fmt.Printf("value=%d |", val)
	}
    // 通过for range 获取下标索引
	fmt.Println()
	for idx := range arr {
		fmt.Printf("index=%d |", idx)
	}

}

赋值

var arr [3]int = [3]int{1, 2, 3}

// 如果第三个不赋值,就是默认值0
var arr1 [3]int = [3]int{1, 2}

// 如果不写数据数量,而使用...,表示数组的长度是根据初始化值的个数来计算
var arr2 [3]int = [...]int{1, 2, 3}

func main() {
	arr := [3]int{1, 2, 3}
	fmt.Printf("%d", arr)
	// 通过下标进行赋值
	arr[0] = 4
	arr[1] = 5
	arr[2] = 6
}

定义新类型

type arr3 [3]int

var arr arr3

func main() {
	arr[0] = 1
	arr[1] = 2
	//	[1 2 0] main.arr3
	fmt.Printf("%d %T", arr, arr)
}

通过索引赋值

var arr [3]int

func main() {
	arr = [3]int{1: 5}
	//	[0 5 0]
	fmt.Printf("%d", arr)
}

数组比较

如果两个数组类型相同(包括数组的长度,数组中元素的类型)的情况下,我们可以直接通过较运算符(==!=)来判断两个数组是否相等,只有当两个数组的所有元素都是相等的时候数组才是相等的,不能比较两个类型不同的数组,否则程序将无法完成编译。

func main() {
	a := [2]int{1, 2}
	b := [...]int{1, 2}
	c := [2]int{1, 3}
    // true true true
	fmt.Printf("%t %t %t \n", a == b, b != c, a != c)
	d := [3]int{1, 2}
    //	编译错误
	fmt.Printf("%t", a == d)
}

多维数组

Go语言中允许使用多维数组,因为数组属于值类型,所以多维数组的所有维度都会在创建时自动初始化零值,多维数组尤其适合管理具有父子关系或者与坐标系相关联的数据。

// 声明一个二维整型数组,两个维度的长度分别是 4 和 2
var arr [4][2]int
// 使用数组字面量来声明并初始化一个二维整型数组
var arr1 = [4][2]int{{10, 11}, {20, 21}, {30, 31}, {40, 41}}
// 声明并初始化数组中索引为 1 和 3 的元素
var arr2 = [4][2]int{1: {20, 21}, 3: {40, 41}}
// 声明并初始化数组中指定的元素
var arr3 = [4][2]int{1: {0: 20}, 3: {1: 41}}

func main() {
    // 通过索引下标取值
	fmt.Printf("%d\n", arr2[1])
	// 循环取值
	for idx, val := range arr3 {
		fmt.Printf("索引:%d 值:%d \n", idx, val)
	}
}

只要类型一致,就可以将多维数组互相赋值。因为数组中每个元素都是一个值,所以可以独立复制某个维度。

func main() {
	var arr [2][2]int
	arr[0][0] = 10
	arr[0][1] = 20
	arr[1][0] = 30
	arr[1][1] = 40

	var arr1 [2][2]int
	arr1 = arr
	// [[10 20] [30 40]]
	fmt.Println(arr1)
	// [10 20]
	var arr3 [2]int = arr1[0]
	fmt.Println(arr3)
	// 30
	var val int = arr1[1][0]
	fmt.Println(val)
}

切片

切片(Slice)与数组一样,也是可以容纳若干类型相同的元素的容器。

与数组不同的是,无法通过切片类型来确定其值的长度。

每个切片值都会将数组作为其底层数据结构。

我们也把这样的数组称为切片的底层数组

切片(slice)是对数组的一个连续片段的引用,所以切片是一个引用类型。

这个片段可以是整个数组,也可以是由起始和终止索引标识的一些项的子集,需要注意的是,终止索引标识的项不包括在切片内(左闭右开的区间)。

Go语言中切片的内部结构包含地址大小容量,切片一般用于快速地操作一块数据集合。

slice [开始位置 : 结束位置]
- slice:表示目标切片对象;
- 开始位置:对应目标切片对象的索引;
- 结束位置:对应目标切片的结束索引。

func main() {
	var val = [3]int{1, 2, 3}
	slice := val[1:2]
	fmt.Printf("%d %d %T", val, slice, slice)
}

从数组或切片生成新的切片拥有如下特性:

  • 取出的元素数量为:结束位置 - 开始位置;
  • 取出元素不包含结束位置对应的索引,切片最后一个元素使用 slice[len(slice)] 获取;
  • 当缺省开始位置时,表示从连续区域开头到结束位置(a[:2])
  • 当缺省结束位置时,表示从开始位置到整个连续区域末尾(a[0:])
  • 两者同时缺省时,与切片本身等效(a[:])
  • 两者同时为 0 时,等效于空切片,一般用于切片复位(a[0:0])
func main() {
	var val [30]int
	for i := 0; i < 30; i++ {
		val[i] = i + 1
	}
	// [11 12 13 14 15]
	fmt.Printf("%d\n", val[10:15])
	// [21 22 23 24 25 26 27 28 29 30]
	fmt.Printf("%d\n", val[20:])
	// [1 2]
	fmt.Printf("%d\n", val[:2])
}

直接声明

除了可以从原有的数组或者切片中生成切片外,也可以声明一个新的切片每一种类型都可以拥有其切片类型,表示多个相同类型元素的连续集合。

//name 表示切片的变量名,Type 表示切片对应的元素类型。
var name []Type

func main() {
    // 声明字符串切片
	var strList []string
    // 声明整型切片
	var val1 []int
    // 声明一个空切片
	var val2 []int = []int{}
    // [] [] []
	fmt.Println(strList, val1, val2)
    // 0 0 0
	fmt.Println(len(strList), len(val1), len(val2))
    // true true false
	fmt.Println(strList == nil, val1 == nil, val2 == nil)
	// 可以使用 append() 函数向切片中添加元素。
	strList = append(strList, "贝塔贝塔")
	fmt.Println(strList)
}

make

如果需要动态地创建一个切片,可以使用 make() 内建函数。

make( []Type, size, cap )

Type 是指切片的元素类型,size 指的是为这个类型分配多少个元素,cap 为预分配的元素数量,这个值设定后不影响 size,只是能提前分配空间,降低多次分配空间造成的性能问题

func main() {
	var a = make([]int, 2)
	b := make([]int, 2, 3)
	// [0 0] [0 0]
	fmt.Println(a, b)
	// 2 2
	fmt.Println(len(a), len(b))
	// 2 3
	fmt.Println(cap(a), cap(b))
}

使用 make() 函数生成的切片一定发生了内存分配操作,但给定开始与结束位置(包括切片复位)的切片只是将新的切片结构指向已经分配好的内存区域,设定开始与结束位置,不会发生内存分配操作。

func main() {
	number := [...]int{1, 2, 3, 4, 5, 6, 7, 8, 9}
	mySlice := number[4:6]
	fmt.Printf("长度:%d 值:%d\n", len(mySlice), mySlice)
	fmt.Println(cap(mySlice))
	mySlice = mySlice[:cap(mySlice)]
	fmt.Printf("第四个元素为:%d", mySlice[3])
}

切片复制

内置函数 copy() 可以将一个数组切片复制到另一个数组切片中,如果加入的两个数组切片不一样大,就会按照其中较小的那个数组切片的元素个数进行复制。

copy( destSlice, srcSlice []T) int

srcSlice 为数据来源切片,destSlice 为复制的目标(也就是将 srcSlice 复制到 destSlice),目标切片必须分配过空间且足够承载复制的元素个数,并且来源和目标的类型必须一致,copy() 函数的返回值表示实际发生复制的元素个数。

func main() {
	slice1 := []int{1, 2, 3, 4, 5}
	slice2 := []int{7, 6, 5}
    只会复制slice2的前3个元素到slice1中
	copy(slice1, slice2)
	//	[7 6 5 4 5]  
	fmt.Println(slice1)
	slice2 = []int{1, 1, 1}
    只会复制slice1的前3个元素到slice2中
	copy(slice2, slice1)
	// [7 6 5]
	fmt.Println(slice2)
}

切片的引用和复制操作对切片元素的影响:

func main() {
	s1 := make([]int, 1000)
	for i := 0; i < 1000; i++ {
		s1[i] = i
	}
	// 引用切片数据 切片不会因为等号操作进行元素的复制
	s2 := s1
	s3 := make([]int, 1000)
	copy(s3, s1)
	s1[0] = 123
	// s2:123 s3:0
	fmt.Printf("s2:%d s3:%d\n", s2[0], s3[0])
	copy(s3, s1[4:6])
	// 4 5 2 3 4
	for i := 0; i < 5; i++ {
		fmt.Printf("%d ", s3[i])
	}
}

map

map 是一种无序的键值对的集合。可以像迭代数组和切片那样迭代它。map 是引用类型,使用如下方式声明:

//[keytype] 和 valuetype 之间允许有空格。
- mapname 为 map 的变量名。
- keytype 为键类型。
- valuetype 是键对应的值类型。

var mapname map[keytype]valuetype

在声明的时候不需要知道 map 的长度,因为 map 是可以动态增长的,未初始化的 map 的值是 nil,使用函数 len() 可以获取 map 中 键值对的数目。

func main() {
	var map1 = map[string]int{"k1": 1, "k2": 2}
	var map2 map[string]int
    // map2 是 map1 的引用,对 map2 的修改也会影响到 map1 的值。
	map2 = map1
	map2["k1"] = 99
    // 99 99 0  key不存在则返回value的初始值
	fmt.Println(map1["k1"], map2["k1"], map1["k99"])
}

map的另外一种创建方式:

当 map 增长到容量上限的时候,如果再增加新的 key-value,map 的大小会自动加 1,所以出于性能的考虑,对于大的 map 或者会快速扩张的 map,即使只是大概知道容量,也最好先标明。

map 可以根据新增的 key-value 动态的伸缩,因此它不存在固定长度或者最大限制,但是也可以选择标明 map 的初始容量 capacity。
make(map[keytype]valuetype, cap)

一个 key 只能对应一个 value, value 又是一个原始类型,使用切片可以实现一个 key对应多个值。

mp1 := make(map[int][]int)
mp2 := make(map[int]*[]int)

遍历

map 的遍历使用 for range 循环完成。

func main() {
	var map1 map[string]string
	map1 = make(map[string]string, 3)
	map1["k1"] = "key1"
	map1["k2"] = "key2"
	map1["k3"] = "key3"

	for key, value := range map1 {
		fmt.Printf("key:%s value:%s \n", key, value)
	}
}

删除

使用 delete() 内建函数从 map 中删除一组键值对。

Go语言中并没有为 map 提供任何清空所有元素的函数、方法,清空 map 的唯一办法就是重新 make 一个新的 map,并行垃圾回收效率比写一个清空函数要高效的多。

func main() {
	map1 := make(map[string]int, 3)
	map1["a"] = 1
	map1["b"] = 2
	map1["c"] = 3
	delete(map1, "b")
	fmt.Println(map1)
	//map1 = map[string]int{}
	map1 = make(map[string]int)
	fmt.Println(map1)
}

并发问题

map 在并发情况下,只读是线程安全的,同时读写是线程不安全的。

运行代码会报错,输出如下:

fatal error: concurrent map read and map write

两个并发函数不断地对 map 进行读和写而发生了竞态问题,map 内部会对这种并发操作进行检查并提前发现。

func main() {
	map1 := make(map[string]int)
	go func() {
		for {
			map1["a"] = 1
		}
	}()

	go func() {
		for {
			_ = map1["a"]
		}
	}()

	for {
		
	}
}

sync.Map

sync.Map 和 map 不同,不是以语言原生形态提供,而是在 sync 包下的特殊结构。

sync.Map 有以下特性:

  • 无须初始化,直接声明即可。
  • sync.Map 不能使用 map 的方式进行取值和设置等操作,而是使用 sync.Map 的方法进行调用,Store 表示存储,Load 表示获取,Delete 表示删除。
  • 使用 Range 配合一个回调函数进行遍历操作,通过回调函数返回内部遍历出来的值,Range 参数中回调函数的返回值在需要继续迭代遍历时,返回 true,终止迭代遍历时,返回 false。
func main() {
	var syncMap sync.Map
	// 将键值对保存到sync.Map
	syncMap.Store("a", 1)
	syncMap.Store("b", 2)
	syncMap.Store("c", 3)
	// 从sync.Map中根据键取值
	fmt.Println(syncMap.Load("a"))
	// 根据键删除对应的键值对
	syncMap.Delete("b")
	syncMap.Range(func(k, v interface{}) bool {
		fmt.Printf("%s %d \n", k, v)
		return true
	})
}

nil

布尔类型的零值(初始值)为 false,数值类型的零值为 0,字符串类型的零值为空字符串"",而指针、切片、映射、通道、函数和接口的零值则是 nil。

nil 标识符是不能比较的,nil 没有默认类型,不同类型 nil 的指针是一样的

func main() {
	var a []int
	var b *int
	// 0x0 0x0 不同类型 nil 的指针是一样的
	fmt.Printf("%p %p \n", a, b)

	var m map[string]string
	var sl []int
	var ptr *int
	var c chan int
	var f func()
	var i interface{}
	// nil 是 map、slice、pointer、channel、func、interface 的零值
	fmt.Printf(" %#v\n %#v\n %#v\n %#v\n %#v\n %#v\n", m, sl, ptr, c, f, i)
	
    // 不同类型的 nil 值占用的内存大小可能是不一样的,具体的大小取决于编译器和架构
	var p *struct{}
	// 8
	fmt.Printf("%d\n", unsafe.Sizeof(p))
	var m1 map[int]string
	// 8
	fmt.Println(unsafe.Sizeof(m1))
	var s []int
	// 24
	fmt.Println(unsafe.Sizeof(s))
	var c1 chan string
	// 8
	fmt.Println(unsafe.Sizeof(c1))
	var f1 func()
	// 8
	fmt.Println(unsafe.Sizeof(f1))
	var i1 interface{}
	// 16
	fmt.Println(unsafe.Sizeof(i1))
}

new和make

make 关键字的主要作用是创建 slice、map 和 Channel 等内置的数据结构,而 new 的主要作用是为类型申请一片内存空间,并返回指向这片内存的指针。

  1. make 分配空间后,会进行初始化,new分配的空间被清零
  2. new 分配返回的是指针,即类型 *Type。make 返回引用,即 Type;
  3. new 可以分配任意类型的数据;

流程控制

if else

关键字if是用于测试某个条件(布尔型或逻辑型)的语句,如果该条件成立,则会执行 if 后由大括号{}括起来的代码块,否则就忽略该代码块继续执行后续的代码。

if condition1 {
    // condition1 满足 执行
} else if condition2 {
    // condition1 不满足 condition2满足 执行
}else {
    // condition1和condition2都不满足 执行
}



if 还有一种特殊的写法,可以在 if 表达式之前添加一个执行语句,再根据变量值进行判断,代码如下:

// 这种写法可以将返回值与判断放在一行进行处理,而且返回值的作用范围被限制在 if、else 语句组合中。
	if a := 5; a > 0 {
		fmt.Println(a)
	}

for

GO语言中的循环语句只支持 for 关键字。

func main() {
    //i := 0; 赋初值,i<10 循环条件 如果为真就继续执行 ;i++ 后置执行 执行后继续循环
	for i := 0; i < 5; i++ {
		fmt.Println(i)
	}

	var a int
    // 相当于while(true),没有break就无限循环
	for {
		a++
		if a == 2 {
			break
		}
	}
	
    i := 0
    //初值可以省略,但是;必须有,但是这样写i的作用域就比较大了,脱离了for循环
	for ; i < 3; i++ {
		fmt.Println(i)
	}
    
	b := 0
    // 将 if 判断整合到 for 中
	for b < 2 {
		fmt.Println(b)
		b++
	}
}

**跳出循环的几种方式:**break、return、panic、goto

	for i := 0; i < 5; i++ {
		fmt.Println(i)
		if i == 2 {
			break
		}
	}
	fmt.Println("end")

	for i := 0; i < 5; i++ {
		fmt.Println(i)
		if i == 2 {
			return
		}
	}
	fmt.Println("end")

	for i := 0; i < 5; i++ {
		fmt.Println(i)
		if i == 2 {
			panic("exception")
		}
	}
	fmt.Println("end")

func main() {
	for i := 0; i < 10; i++ {
		for j := 0; j < 10; j++ {
			if j == 2 {
				goto breakHere
			}
		}
	}
	// 手动返回, 避免执行进入标签
	return
breakHere:
	fmt.Println("end")
}

代码优化

func main() {
	s := "123123123"
	for i, n := 0, length(s); i < n; i++ {
		println(i, s[i])
	}
}

func length(str string) int {
	fmt.Println("execute length")
	return len(str)
}

// 九九乘法表
func main() {
	for y := 1; y <= 9; y++ {
		for x := 1; x <= y; x++ {
			fmt.Printf("%d*%d=%d\t", x, y, x*y)
		}
		fmt.Println()
	}
}

for range

for range 可以遍历数组、切片、字符串、map 及管道(channel)。

val 始终为集合中对应索引的值拷贝,因此它一般只具有只读性质,对它所做的任何修改都不会影响到集合中原有的值

通过 for range 遍历的返回值有一定的规律:

  • 数组、切片、字符串返回索引和值。
  • map 返回键和值。
  • channel只返回管道内的值。
	map1 := make(map[string]string, 3)
	map1["a"] = "1"
	map1["b"] = "2"
	map1["c"] = "3"

	for k, v := range map1 {
		fmt.Printf("key:%s value:%s \n", k, v)
		if k == "a" {
			v = "AAA"
			fmt.Printf("updated: %s\n", map1[k])
		}
	}

	//因为一个字符串是 Unicode 编码的字符(或称之为 rune )集合
	//char 实际类型是 rune 类型
	str := "你好贝塔"
	for idx, s := range str {
		fmt.Printf("%d %c \n", idx, s)
	}

switch

switch 语句用于基于不同条件执行不同动作,每一个 case 分支都是唯一的,从上至下逐一测试,直到匹配为止。

switch 分支表达式可以是任意类型,不限于常量。可省略 break,默认自动终止。

func main() {
	var exp = 100
	var level int
	// 类型不被局限于常量或整数,但必须是相同的类型;或者最终结果为相同类型的表达式。
    // 可以同时测试多个可能符合条件的值,使用逗号分割它们。
	switch exp {
	case 50, 60, 70:
		level = 7
	case 80:
		level = 8
	case 100:
		level = 10
	default:
		level = 5
	}
	//swtich后面如果没有条件表达式,则会对true进行匹配
	switch {
	case level == 7:
		fmt.Println("平均等级")
	case level > 7:
		fmt.Println("大佬等级")
	case level < 7:
		fmt.Println("菜鸟等级")
	}
}

switch默认相当于每个case最后带有break,匹配成功后不会自动向下执行其他case,而是跳出整个switch,可以通过fallthrough关键字执行下一个case。

加了fallthrough后,会直接运行【紧跟的后一个】case或default语句,不论条件是否满足都会执行

// GOGO Lets GO
	str := "GOGO"
	switch str {
	case "GOGO":
		fmt.Print("GOGO")
		fallthrough
	case "Lets GO":
		fmt.Print(" Lets GO")
	}

goto

goto 语句通过标签进行代码间的无条件跳转。

使用 goto 退出多层循环

func main() {
	for x := 0; x < 10; x++ {
		for y := 0; y < 10; y++ {
			if y == 2 {
				goto breakPoint
			}
		}
	}
	fmt.Println("done")
breakPoint:
	fmt.Println("goto break")
}

使用 goto 集中处理错误

func main() {
	err := errors.New("Error1")
	if err != nil {
		goto errorPoint
	}
	err = errors.New("Error2")
	if err != nil {
		goto errorPoint
	}
	fmt.Println("done")
	return
errorPoint:
	fmt.Println(err)
	os.Exit(1)
}

break

break 语句可以结束 for、switch 和 select 的代码块,另外 break 语句还可以在语句后面添加标签,表示退出某个标签对应的代码块,标签要求必须定义在对应的 forswitchselect 的代码块上。

func main() {
breakPoint:
	for i := 0; i < 3; i++ {
		for j := 0; j < 3; j++ {
			if j == 1 {
				break breakPoint
			}
			fmt.Println("j:", j)
		}
	}
}

continue

continue 语句可以结束当前循环,开始下一次的循环迭代过程,仅限在 for 循环内使用,在 continue 语句后添加标签时,表示开始标签对应的循环

func main() {
continuePoint:
	for i := 0; i < 3; i++ {
		for j := 0; j < 3; j++ {
			if j == 1 {
				continue continuePoint
			}
			fmt.Println("j:", j)
		}
	}
}

  • 23
    点赞
  • 12
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值