3、变量和数据类型

变量

Go语言是静态类型语言,因此变量(variable)是有明确类型的,编译器也会检查变量类型的正确性。在数学概念中,变量表示没有固定值且可改变的数。但从计算机系统实现角度来看,变量是一段或多段用来存储数据的内存。

变量声明

Go语言在声明变量时,自动对变量对应的内存区域进行初始化操作。每个变量会初始化其类型的默认值,例如:

变量一旦声明,就必须使用,不然编译报错!

Go虽然支持自动类型推导,但仍然是强类型语言!

var声明

// var 是声明变量的关键字,name 是变量名,type 是变量的类型
var name type

// 批量声明并赋值
var a,b,c int = 10,20,30

// 自动类型推导并批量声明变量然后赋值
var a,b,c = 10,20.99,true

// 批量声明一组数据
var (
	a int
	b string
	c []float32
)

// 自动类型推导并批量声明一组变量然后赋值
var (
	d = 10
	e = 20
)

简短模式

// 此时就是声明变量并赋值,而且自动类型推导
a := 10
func main(){
    s := 32
    a,b := 11,"world"
}

简短模式(short variable declaration)有以下限制:

因为简洁和灵活的特点,简短变量声明被广泛用于大部分的局部变量的声明和初始化。

var 形式的声明语句往往是用于需要显式指定变量类型地方,或者因为变量稍后会被重新赋值而初始值无关紧要的地方。

匿名变量

匿名变量的特点是一个下划线__本身就是一个特殊的标识符,被称为空白标识符。

可以像其他标识符那样用于变量的声明或赋值(任何类型都可以赋值给它),但任何赋给这个标识符的值都将被抛弃,因此这些值不能在后续的代码中使用,也不可以使用这个标识符作为变量对其它变量进行赋值或运算。

匿名变量不占用内存空间,不会分配内存。匿名变量与匿名变量之间也不会因为多次声明而无法使用。

package main

import (
	"fmt"
)

func main() {
	//只需要x坐标,所以y坐标就可以使用匿名变量来接收
	x, _ := getPoint()

	fmt.Println(x)
}

func getPoint() (int, int) {
	return 10, 200
}

作用域和声明周期

常量

Go语言中的常量使用关键字const定义,用于存储不会改变的数据,常量是在编译时被创建的,即使定义在函数内部也是如此。由于编译时的限制,定义常量的表达式必须为能被编译器求值的常量表达式。

注意:常量的数据类型只可以是布尔、数字(整数、浮点和复数)、字符串、枚举

常量声明后,并不要求必须使用

const a [type] = value
const a,b,c [type] = value1,value2,value3

//常量也支持自动类型推导
const a = value

//常量也支持批量声明
const a,b,c = value1,value2,value3
const (
  a     = 10
  b int = 20
  c     = "hello"
)

// 批量声明相同值常量,此时a、b、c的值都是10
const (
	a = 10
	b
	c
)

注意:常量的值必须是能够在编译时就能够确定的,可以在其赋值表达式中涉及计算过程,但是所有用于计算的值必须在编译期间就能获得。

const a int = 10 + 2 //正确
const a int = getValue() //错误

iota 常量生成器

常量声明可以使用 iota 常量生成器初始化(作为枚举),它用于生成一组以相似规则初始化的常量,但是不用每行都写一遍初始化表达式。在每一个const关键字出现时,都会被重新重置为0,然后每出现一个常量,iota所代表的数值会自动增加1。

const (
    a int = iota //0
    b            //1
    c            //2
)

自定义枚举类型

可以定义类似java的枚举类型

//将 int 定义为 Weekday 类型
type Weekday int

const (
    Sunday Weekday = iota //0
    Monday                //1
    Tuesday               //2
    Wednesday             //3
    Thursday              //4
    Friday                //5
    Saturday              //6
)

修改起始值

const (
    a int = iota + 10 //10
    b                 //11
    c                 //12
)

跳过中间值

使用下划线进行跳过

const (
    a int = iota //0
    _
    _
    b 			 //3
    c 			 //4
)

修改中间值

const (
    a int = iota //0
    b int = 35   //35
    c int = iota //2
)

数据类型

数字类型

1、整形

Go语言的数值类型分为以下几种:整数、浮点数、复数,其中每一种都包含了不同大小的数值类型,例如有符号整数包含 int8、int16、int32、int64 等,每种数值类型都决定了对应的大小范围和是否支持正负符号。

math包提供了方法math.Max[Type]math.Min[Type]查看各类型数据的最大最小值

例如int8代表该类型变量占8Bit(位),也就是1Byte(字节),可以使用unsafe.Sizeof(variable)查看变量所占Byte(字节)大小

有符号类型 说明 无符号类型 说明 占用字节大小
int8 有符号 8 位整型 (-128 到 127) uint8 无符号 8 位整型 (0 到 255) 1
int16 有符号 16 位整型 (-32768 到 32767) uint16 无符号 16 位整型 (0 到 65535) 2
int32 有符号 32 位整型 (-2147483648 到 2147483647) uint32 无符号 32 位整型 (0 到 4294967295) 4
int64 有符号 64 位整型 (-9223372036854775808 到 9223372036854775807) uint64 无符号 64 位整型 (0 到 18446744073709551615) 8
int 有符号整型,由cpu决定,长度在32bit 或 64bit 之间变化 uint 无符号整型,由cpu决定,长度在32bit 或 64bit 之间变化 4/8

大多数情况下,我们只需要 int 一种整型即可,它可以用于循环计数器(for 循环中控制循环次数的变量)、数组和切片的索引,以及任何通用目的的整型运算符,通常 int 类型的处理速度也是最快的。

除此之外,go还支持一些其他整形

类型 说明
byte 和 uint8 是等价类型type byte = uint8,byte 类型一般用于强调数值是一个原始的数据(一个字节,也可以代表ASCII 码的一个字符)而不是一个小的整数
rune 和 int32 类型是等价的type rune = int32,通常用于表示一个 Unicode 码点(字符)
uintptr 没有指定具体的 bit 大小但是足以容纳指针,uintptr 类型只有在底层编程时才需要,特别是Go语言和C语言函数库或操作系统接口相交互的地方

2、浮点型

Go语言提供了两种精度的浮点数 float32float64,它们的算术规范由 IEEE754 浮点数国际标准定义,该浮点数规范被所有现代的 CPU 支持。

这些浮点数类型的取值范围可以从很微小到很巨大。浮点数取值范围的极限值可以在 math 包中找到:

一个 float32 类型的浮点数可以提供大约 6 个十进制数的精度,而 float64 则可以提供约 15 个十进制数的精度,通常应该优先使用 float64 类型,因为 float32 类型的累计计算误差很容易扩散,并且 float32 能精确表示的正整数并不是很大。

如果是64位操作系统,则浮点数自动类型推导默认数据类型为float64

float精度丢失的问题
a := 1129.6
fmt.Println(a * 100) // 112959.99999999999

m1 := 8.2
m2 := 3.8
fmt.Println(m1 - m2) // 4.3999999999999995

使用第三方包https://github.com/shopspring/decimal进行解决

go get -u github.com/shopspring/decimal
a := decimal.NewFromFloat(1129.6)
i := decimal.NewFromInt(100)
r1 := a.Mul(i)
fmt.Println(r1) // 112960

m1 := decimal.NewFromFloat(8.2)
m2 := decimal.NewFromFloat(3.8)
r2 := m1.Sub(m2)
fmt.Println(r2) // 4.4

3、复数

Go语言中复数的类型有两种,分别是 complex128(64 位实数和虚数)和 complex64(32 位实数和虚数),其中 complex128 为复数的默认类型。

复数的值由三部分组成 RE + IMi,其中RE是实数部分,IM 是虚数部分,REIM均为 float 类型,而最后的i是虚数单位。

声明格式:

/*
name 为复数的变量名,complex128 为复数的类型,“=”后面的 complex 为Go语言的内置函数用于为复数赋值,x、y 分别表示构成该复数的两个 float64 类型的数值,x 为实部,y 为虚部
*/
var name complex128 = complex(x, y)
//获取实部
real(name)
//获取虚部
imag(name)

字符串类型

一个字符串是一个不可改变的字节序列,字符串可以包含任意的数据,但是通常是用来包含可读的文本,字符串是 UTF-8 字符的一个序列(当字符为 ASCII 码表上的字符时则占用 1 个字节,其它字符根据需要占用 2-4 个字节)。

UTF-8 是一种被广泛使用的编码格式,由于该编码对占用字节长度的不定性,在Go语言中字符串也可能根据需要占用 1 至 4 个字节

字符串是一种值类型,且值不可变,即创建某个文本后将无法再次修改这个文本的内容,在golang中,字符串是字节的定长切片(byte slice),如果真要修改字符串中的字符,将 string 转为[]byte修改后,再转为 string 即可。

字节、字符长度

字符串所占的字节长度可以通过函数 len() 来获取

package main

import (
	"fmt"
)

func main() {
	x := "abcde"

	fmt.Println(len(x)) //5
	fmt.Println(x[0])   //97
}

注意:方法len()计算的是字节长度,由于ASCII码中1字符等于1字节,可以等价字符长度。如果是Unicode字符,比如字符串"你好",使用len()计算所占用的字节长度就是6,而不是2。

因为在Go里面,汉字使用的是UTF-8编码,每个汉字占用3个字节,每个数字、字母、符号占用1个字节。

如果需要计算Unicode字符长度,那么就需要使用utf8包下的RuneCountInString()函数

package main

import (
	"fmt"
	"unicode/utf8"
)

func main() {
	x := "你好"

	fmt.Println(len(x)) //6
	fmt.Println(utf8.RuneCountInString(x)) //2
}

也可以将字符串转为rune数组

package main

import (
	"fmt"
)

func main() {
	a := "你好go"
	runes := []rune(a)
	fmt.Println(len(runes))
}

多行字符串

package main

import (
	"fmt"
)

func main() {
    //使用反引号进行定义
	x := `abcdefg
    hijklmn
    opqrst
    uvwxyz
    `

	fmt.Println(x)
}

遍历字符

对于ASCII编码,直接使用下标进行循环遍历

package main

import (
	"fmt"
)

func main() {
	x := "abcde"
	for i := 0; i < len(x); i++ {
		fmt.Printf("%c\n", x[i])
	}
}

对于Unicode编码,需要使用for...range...,或者转为rune数组后遍历

package main

import (
	"fmt"
)

func main() {
	x := "你好"
	for _, c := range x {
		fmt.Printf("%c\n", c)
	}
}

修改字符串

注意:字符串是不可变的字符序列,即使修改后,也是重新分配了内存空间,生成了新的字符串!

s := "big"
sb := []byte(s)
sb[0] = 'p'
s = string(sb)
fmt.Println(s)

z := "白萝卜"
zr := []rune(z)
zr[0] = '红'
z = string(zr)
fmt.Println(z)

字符串拼接

Go语言拼接字符串有五种方法,分别是如下:

使用+号拼接,要求+号两边都是string类型

a := "hello"
b := " world"
c := a + b

使用fmt.Sprint()拼接,可以实现任意数据类型的拼接

a := "hello"
b := 1234
c := fmt.Sprint(a, b)

使用strings.Join函数拼接

strings.Join([]string{"hello", "world"}, "-")

使用bytes.Buffer,性能比上面的方式好

a := "hello"
b := "world"

var bt bytes.Buffer
bt.WriteString(a)
bt.WriteString(b)

c := bt.String()

使用strings.Builder,性能比bytes.Buffer还要好,官方推荐该方法

a := "hello"
b := "world"

var sb strings.Builder
sb.WriteString(a)
sb.WriteString(b)

c := sb.String()

字符类型

Go语言中字符的声明方式如下:

var c1 byte = 'x'
var c2 rune = '你'
c3 := '好'
fmt.Printf("%c,%c,%c\n", c1, c2, c3)

Go语言的字符有以下两种:

//十进制字符
var ch1 byte = 65
fmt.Printf("%c\n", ch1) //A
//两位十六进制,使用单引号,并且加\x前缀
var ch2 byte = '\x41'
fmt.Printf("%c\n", ch2) //A
//三位八进制,使用单引号,并且加\前缀
var ch3 byte = '\101'
fmt.Printf("%c\n", ch3) //A

Unicode(Utf8)编码,在文档中,一般使用格式 U+hhhh 来表示,其中 h 表示一个 16 进制数

在书写 Unicode 字符时,需要在 16 进制数之前加上前缀\u或者\U。因为 Unicode 至少占用 2 个字节,所以我们使用 int16 或者 int 类型来表示。如果需要使用到 4 字节,则使用\u前缀,如果需要使用到 8 个字节,则使用\U前缀。

var ch1 int16 = '\u4f60' //你
var ch2 rune = '\u597d'  //好
fmt.Printf("%c%c\n", ch1, ch2)

基本数据类型转换

在必要以及可行的情况下,一个类型的值可以被转换成另一种类型的值。由于Go语言不存在隐式类型转换,因此所有的类型转换都必须显式的声明

package main

import (
	"fmt"
)

func main() {
	var a int = 10
	var b byte = byte(a)
	fmt.Println(b) // 10

	s := "hello"
	sb := []byte(s)
	fmt.Println(sb) // [104 101 108 108 111]
}

注意: 类型转换只能在定义正确的情况下转换成功,例如从一个取值范围较小的类型转换到一个取值范围较大的类型(将 int16 转换为 int32)。当从一个取值范围较大的类型转换到取值范围较小的类型时(将 int32 转换为 int16 或将 float32 转换为 int),会发生精度丢失(截断)的情况。

注意: Go中没有自动类型提升,例如int和float相加,需要先把int类型显式的转换为float类型,然后再相加。

类型别名和类型定义

//类型定义
type NewType Type
//类型别名
type TypeAlias = Type

区别

//类型定义
type NewInt int

//类型别名
type MyInt = int

func main() {
    var a NewInt
    var b MyInt

    fmt.Printf("type of a:%T\n", a) // type of a:main.NewInt
    fmt.Printf("type of b:%T\n", b) // type of b:int
}

a的类型是main.NewInt,表示main包下定义的NewInt类型。

b的类型是intMyInt类型只会在代码中存在,编译完成时并不会有MyInt类型。

总结: 类型定义type NewType Type是定义了一个新的类型;类型别名type TypeAlias = Type只是给原来的类型起了个别名,在编译时还是以原来的类型为准

值类型和引用类型

运算符

算术运算符

运算符 描述 实例(A=10;B=20)
+ 相加 A + B 输出结果 30
- 相减 A - B 输出结果 -10
* 相乘 A * B 输出结果 200
/ 相除 B / A 输出结果 2
% 求余 B % A 输出结果 0
++ 自增 A++ 输出结果 11
-- 自减 A-- 输出结果 9

注意: golang中的++--,只可以写在变量后面;并且是作为单独语句出现而不是表达式,所以不可以运算后赋值

关系运算符

运算符 描述 实例(A=10;B=20)
== 检查两个值是否相等,如果相等返回 True 否则返回 False。 A == B为 False
!= 检查两个值是否不相等,如果不相等返回 True 否则返回 False。 A != B 为 True
> 检查左边值是否大于右边值,如果是返回 True 否则返回 False。 A > B为 False
< 检查左边值是否小于右边值,如果是返回 True 否则返回 False。 A < B为 True
>= 检查左边值是否大于等于右边值,如果是返回 True 否则返回 False。 A >= B为 False
<= 检查左边值是否小于等于右边值,如果是返回 True 否则返回 False。 A <= B 为 True

逻辑运算符

运算符 描述 实例(A=true;B=false)
&& 逻辑 AND 运算符。 如果两边的操作数都是 True,则条件 True,否则为 False。 A && B 为 False
|| 逻辑 OR 运算符。 如果两边的操作数有一个 True,则条件 True,否则为 False。 `A
! 逻辑 NOT 运算符。 如果条件为 True,则逻辑 NOT 条件 False,否则为 True。 !B为 True

赋值运算符

运算符 描述 实例
= 简单的赋值运算符,将一个表达式的值赋给一个左值 C = A + B 作用为 A + B 表达式结果赋值给 C
+= 相加后再赋值 C += A 等效于 C = C + A
-= 相减后再赋值 C -= A 等效于 C = C - A
*= 相乘后再赋值 C *= A 等效于 C = C * A
/= 相除后再赋值 C /= A 等效于 C = C / A
%= 求余后再赋值 C %= A 等效于 C = C % A
<<= 左移后赋值 C <<= 2 等效于 C = C << 2
>>= 右移后赋值 C >>= 2 等效于 C = C >> 2
&= 按位与后赋值 C &= 2 等效于 C = C & 2
^= 按位异或后赋值 C ^= 2 等效于 C = C ^ 2
|= 按位或后赋值 `C

位运算符

位运算符用于整数在内存中的二进制位进行操作

p q p & q p | q p ^ q
0 0 0 0 0
0 1 0 1 1
1 1 1 1 0
1 0 0 1 1
运算符 描述 实例(A=60;B=13)
& 按位与。 其功能是参与运算的两数各对应的二进位相与。 A & B结果为 12, 二进制为 0000 1100
| 按位或。 其功能是参与运算的两数各对应的二进位相或 A | B结果为 61, 二进制为 0011 1101
^ 按位异或。 其功能是参与运算的两数各对应的二进位相异或,当两对应的二进位相异时,结果为1。 A ^ B结果为 49, 二进制为 0011 0001
<< 左移运算符。左移n位就是乘以2的n次方。 其功能把<<左边的运算数的各二进位全部左移若干位,由<<右边的数指定移动的位数,高位丢弃,低位补0。 A << 2 结果为 240 ,二进制为 1111 0000
>> 右移运算符。右移n位就是除以2的n次方。 其功能是把>>左边的运算数的各二进位全部右移若干位,>>右边的数指定移动的位数。 A >> 2 结果为 15 ,二进制为 0000 1111

其他运算符

运算符 描述 实例
& 返回变量存储地址 &a:获取变量的实际地址。
* 指针变量。 *a:对指针变量a,解引用

nil

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

面试题

类型比较问题