文章

基本数据类型2

1.常量

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

声明格式:

const name [type] = value

所有常量的运算都可以在编译期完成,这样不仅可以减少运行时的工作,也方便其他代码的编译优化,当操作数是常量时,一些运行时的错误也可以在编译时被发现,例如整数除零、字符串索引越界、任何导致无效浮点数的操作等。

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

因为它们的值是在编译期就确定的,因此常量可以是构成类型的一部分

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

const (
a = 1
b
c = 2
d
)

fmt.Println(a, b, c, d) // 1 1 2 2

1.1 iota 常量生成器

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

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

const (
		A = iota
		B
		C
		_ //跳过值
		E
	)
	fmt.Println(A, B, C, E) // 0 1 2 4

const (
        AA = iota
        BB
        CC = 123
        DD
        _
        EE
        FF = iota
        )
fmt.Println(AA, BB, CC, DD, EE, FF) //0 1 123 123 123 6

2.指针

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

Go语言的指针类型变量即拥有指针高效访问的特点,又不会发生指针偏移,从而避免了非法修改关键性数据的问题。

同时,垃圾回收也比较容易对不会发生偏移的指针进行检索和回收。

切片比原始指针具备更强大的特性,而且更为安全。

切片在发生越界时,运行时会报出宕机,并打出堆栈,而原始指针只会崩溃

2.1 理解指针

var a int = 10

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

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

在变量名前面添加&操作符(前缀)来获取变量的内存地址(取地址操作)

	var a int = 10
	fmt.Printf("%p\n", &a)

假如地址为: 0x140000a4008

当一个指针被定义后没有分配到任何变量时,它的默认值为 nil

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

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

	var room int = 10
	var ptr = &room
	fmt.Printf("%p\n", &room)        // 0x1400011c008
	fmt.Printf("%T, %p\n", ptr, ptr) // *int, 0x1400011c008]

	fmt.Println("指针地址", ptr)      // 0x1400011c008
	fmt.Println("指针地址代表的值", *ptr) // 10

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

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

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

2.2 使用指针修改值

通过指针不仅可以取值,也可以修改值

func func main() {
    // 利用指针修改值
    var num = 10
    modifyFromPoint(num)          // 未使用指针,方法内: 1000
    fmt.Println("未使用指针,方法外", num) // 未使用指针,方法外 10
    
    var num2 = 22
    newModifyFromPoint(&num2)     // 使用指针,方法内: 9999
    fmt.Println("使用指针 方法外", num2) // 使用指针 方法外 9999
}
func modifyFromPoint(num int) {
	num = 1000
	fmt.Println("未使用指针,方法内:", num)
}
func newModifyFromPoint(ptr *int) {
	// 使用指针
	*ptr = 9999 // 修改指针地址指向的值
	fmt.Println("使用指针,方法内:", *ptr)
}

2.3 创建指针的另一种方法

Go语言还提供了另外一种方法来创建指针变量,格式如下:

new(类型)

	str := new(string)
	fmt.Printf("地址为:%p\n", &str) // 地址为:0x14000114018
	fmt.Println(*str) // nil
	*str = "123"
	fmt.Println(*str) // 123

new() 函数可以创建一个对应类型的指针,创建过程会分配内存,被创建的指针指向默认值。

2.4 指针小案例

获取命令行的输入信息

Go语言内置的 flag 包实现了对命令行参数的解析,flag 包使得开发命令行工具更为简单。

	mode := flag.String("mode", "", "fast模式能让程序运行的更快")
	flag.Parse()
	fmt.Println(*mode)

终端输入:

go run main.go --mode=fast

go run main.go --help

3. 变量的生命周期

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

变量的生命周期与变量的作用域有不可分割的联系:

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

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

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

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

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

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

var global *int
func f() {
    var x int
    x = 1
    global = &x
}
func g() {
    y := new(int)
    *y = 1
}

上述代码中,函数 f 里的变量 x 必须在堆上分配,因为它在函数退出后依然可以通过包一级的 global 变量找到,虽然它是在函数内部定义的。

用Go语言的术语说,这个局部变量 x 从函数 f 中逃逸了。

相反,当函数 g 返回时,变量 y 不再被使用,也就是说可以马上被回收的。因此,y 并没有从函数 g 中逃逸,编译器可以选择在栈上分配 *y 的存储空间,也可以选择在堆上分配,然后由Go语言的 GC(垃圾回收机制)回收这个变量的内存空间。

4. 类型别名

类型别名:

type TypeAlias = Type

还有一种是类型定义:

//定义Name为Type类型 ,定义之后 Name为一种新的类型
type Name Type

类型别名与类型定义表面上看只有一个等号的差异,区别:

	// 将NewInt定义为int类型
	type NewInt int
	// 将int取一个别名叫IntAlias
	type IntAlias = int

	// 将a声明为NewInt类型
	var a NewInt
	// 查看a的类型名 main.NewInt
	fmt.Printf("a type: %T\n", a) // a type: main.NewInt
	// 将a2声明为IntAlias类型
	var a2 IntAlias
	// 查看a2的类型名 int
	//IntAlias 类型只会在代码中存在,编译完成时,不会有 IntAlias 类型。
	fmt.Printf("a2 type: %T\n", a2) // a2 type: int

5. 注释

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

// 但行注释

/*

第一行注释

第二行注释

...

*/

每一个包都应该有相关注释,在使用 package 语句声明包名之前添加相应的注释,用来对包的功能及作用进行简要说明。

同时,在 package 语句之前的注释内容将被默认认为是这个包的文档说明。一个包可以分散在多个文件中,但是只需要对其中一个进行注释说明即可。

6. 关键字和标识符

关键字即是被Go语言赋予了特殊含义的单词,也可以称为保留字。

Go语言中的关键字一共有 25 个:

breakdefaultfuncinterfaceselect
casedefergomapstruct
chanelsegotopackageswitch
constfallthroughifrangetype
continueforimportreturnvar

之所以刻意地将Go语言中的关键字保持的这么少,是为了简化在编译过程中的代码解析。

和其它语言一样,关键字不能够作标识符使用。

标识符

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

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

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

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

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

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

在Go语言中还存在着一些特殊的标识符,叫做预定义标识符,如下表所示:

appendboolbytecapclosecomplexcomplex64complex128uint16
copyfalsefloat32float64imagintint8int16uint32
int32int64iotalenmakenewnilpanicuint64
printprintlnrealrecoverstringtrueuintuint8uintptr

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

7. 运算符优先级

所谓优先级,就是当多个运算符出现在同一个表达式中时,先执行哪个运算符。

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

⚠️:优先级值越大,表示优先级越高。

8. 字符串与其他数据类型的转换

  1. 整数 与 字符串
// 字符串与其他类型的转换
// str 转 int
newStr1 := "1"
intValue, _ := strconv.Atoi(newStr1)
fmt.Printf("%T,%d\n", intValue, intValue)  // int,1

// int 转 str
intValue2 := 1
strValue := strconv.Itoa(intValue2)
fmt.Printf("%T, %s\n", strValue, strValue)
  1. 浮点数 与字符串
    // str 转  float
    string3 := "3.1415926"
    f,_ := strconv.ParseFloat(string3, 32)
    fmt.Printf("%T, %f\n", f, f)  // float64, 3.141593
    //float 转 string
	floatValue := 3.1415926
	//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 表示总的数字位数(整数部分+小数部分)
	formatFloat := strconv.FormatFloat(floatValue, 'f', 2, 64)
	fmt.Printf("%T,%s",formatFloat,formatFloat)
License:  CC BY 4.0 test