变量 #
Go语言是静态类型语言,因此变量(variable)是有明确类型的,编译器也会检查变量类型的正确性。在数学概念中,变量表示没有固定值且可改变的数。但从计算机系统实现角度来看,变量是一段或多段用来存储数据的内存。
变量声明 #
Go语言的一个重要特性是所有变量都有默认值,也就是在声明变量时,自动对变量对应的内存区域进行初始化操作,称为"零值"。
Go的零值机制消除了"未初始化变量"的概念,提高了程序安全性,减少了常见错误。
| 类型 | 零值 |
|---|---|
| 整数类型 | 0 |
| 浮点类型 | 0.0 |
| 布尔类型 | false |
| 字符串 | "" (空字符串) |
| 指针、函数、接口、切片、管道、映射 | nil |
变量一旦声明,就必须使用,不然编译报错!
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
}
作用域和声明周期 #
- 函数外定义的变量称为全局变量
- 在函数体外声明的变量称之为全局变量
- 全局变量声明必须以 var 关键字开头
- 首字母小写的全局变量只对当前包可见,类似于
protected - 首字母大写的全局变量对所有包可见,类似于
public
- 函数内定义的变量称为局部变量
- 函数的参数和返回值变量都属于局部变量
- 局部变量不是一直存在的,它只在定义它的函数被调用后存在,函数调用结束后这个局部变量就会被销毁。
- 生命周期是动态的,从创建这个变量的声明语句开始,到这个变量不再被引用为止
- 函数定义中的变量称为形式参数
- 在定义函数时函数名后面括号中的变量叫做形式参数(简称形参)。形式参数只在函数调用时才会生效,函数调用结束后就会被销毁,在函数未被调用时,函数的形参并不占用实际的存储单元,也没有实际值。
- 形式参数会作为函数的局部变量来使用。
常量 #
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
)
数据类型 #
- 布尔型:
bool- 布尔型的值只可以是常量
true或者false,不可参与数值运算,也无法进行类型转换。 - 固定占用1字节。
- 布尔型的值只可以是常量
- 数字类型:
int、uint、int8、int16、int32、int64、uint8、uint16、uint32、uint64、byte(uint8)、rune(int32)、float32、float64、complex128、complex256- 整型 int 和浮点型 float32、float64,Go 语言支持整型和浮点型数字,并且支持复数,其中位的运算采用补码。
- 其中
byte代表一个字节,也是ASCII的一个字符;rune代表Unicode的一个字符
- 字符串类型:
string- 字符串就是一串固定长度的字符连接起来的字符序列。Go 的字符串是由单个字节连接起来的。Go 语言的字符串的字节使用
UTF-8编码标识 Unicode 文本。
- 字符串就是一串固定长度的字符连接起来的字符序列。Go 的字符串是由单个字节连接起来的。Go 语言的字符串的字节使用
- 派生类型
- 指针类型:
*int - 数组类型:
[10]int - 结构体类型:
struct - Channel 类型:
chan int - 函数类型:
func - 切片类型:
[]int - 接口类型:
interface - Map 类型:
map
- 指针类型:
数字类型 #
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语言函数库或操作系统接口相交互的地方 |
字面量 #
func main() {
// 十进制
decimal := 42
// 二进制
binary := 0b101010 // 从Go 1.13开始支持
// 八进制
octal := 0o52 // 从Go 1.13开始推荐使用0o前缀
oldOctal := 052 // 传统写法,不推荐
// 十六进制
hex := 0x2A
fmt.Println(decimal, binary, octal, oldOctal, hex) // 都会打印出42
}
2、浮点型 #
Go语言提供了两种精度,它们的算术规范由 IEEE754 浮点数国际标准定义,该浮点数规范被所有现代的 CPU 支持。
| 类型 | 描述 | 精度 |
|---|---|---|
float32 |
32位浮点数 | 约7位十进制精度 |
float64 |
64位浮点数 | 约15位十进制精度 |
这些浮点数类型的取值范围可以从很微小到很巨大。浮点数取值范围的极限值可以在 math 包中找到:
- 常量
math.MaxFloat32表示float32能取到的最大数值,大约是3.4e38; - 常量
math.MaxFloat64表示float64能取到的最大数值,大约是1.8e308; float32和float64能表示的最小值分别为1.4e-45和4.9e-324。
如果是64位操作系统,则浮点数自动类型推导默认数据类型为float64。
字面量 #
func main() {
// 十进制表示
pi := 3.14159
// 科学计数法
avogadro := 6.022e23 // 6.022 × 10²³
plank := 6.626e-34 // 6.626 × 10⁻³⁴
fmt.Println(pi, avogadro, plank)
}
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
a.Add(b):加法,a + ba.Sub(b):减法,a - ba.Mul(b):乘法,a * ba.Div(b):除法,a / ba.Cmp(b)比较大小,a > b返回值> 0;a = b返回值= 0;a < b返回值< 0
3、复数 #
| 类型 | 描述 |
|---|---|
complex64 |
由两个float32组成的复数 |
complex128 |
由两个float64组成的复数 |
其中 complex128 为复数的默认类型。
复数的值由三部分组成 RE + IMi,其中RE是实数部分,IM是虚数部分,RE 和IM均为 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语言的字符有以下两种:
type byte = uint8:代表了 ASCII 码的一个字符。type rune = int32:代表一个 UTF-8 字符,当需要处理中文、日文或者其他复合字符时,则需要用到 rune 类型。
//十进制字符
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的类型是int。MyInt类型只会在代码中存在,编译完成时并不会有MyInt类型。
总结: 类型定义type NewType Type是定义了一个新的类型;类型别名type TypeAlias = Type只是给原来的类型起了个别名,在编译时还是以原来的类型为准
值类型和引用类型 #
- 值类型:基本数据类型,int,float,bool,string,数组,struct
- 特点:变量直接存储值,内存通常在栈中分配,栈在函数调用完会被释放
- 引用类型:指针,slice,map,chan等都是引用类型
- 特点:变量存储的是一个地址,这个地址存储最终的值。内存通常在堆上分配,通过GC回收。
运算符 #
算术运算符 #
| 运算符 | 描述 | 实例(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。
不同类型中的表现 #
- 指针:
nil指针不指向任何内存地址。 - 切片:
nil切片的长度和容量均为0,没有底层数组。 - 映射:
nil映射没有键值对,读取操作返回零值,写入操作会导致panic。 - 通道:
nil通道无法用于发送或接收操作。 - 函数:
nil函数调用会导致panic。 - 接口:
nil接口不包含任何具体值,动态类型和值均为nil。
特性 #
- nil 是
map、slice、pointer、channel、func、interface的零值 - nil 标识符是不能比较的:
nil == nil是错误的 - nil 不是关键字或保留字:标识符可以用
nil,但是最好不要这么做 - nil 没有默认类型:无法使用
fmt.Printf("%T",nil),来获取类型 - 不同类型 nil 的指针是一样的:所有类型为
nil时,指针都是0x0,可以使用fmt.Printf("%p",variables)查看指针
面试题 #
类型比较问题 #
- 比较只限于同类型,此处说的可比较是使用
==进行比较 - 基本类型(int、float、string、bool、complex)可以比较
- 接口,如果实际值的类型不相同,或者不可比较,就会引起panic;除此之外可比较
- struct,字段个数、类型、顺序相同;字段类型是可比较的类型,那么就可以比较
- 数组,数组长度相同、类型相同、类型可比较,就可以比较
- 指针,可以比较
- slice、map,不可比较
- channel,只要通道类元素类型相同,就可以比较
- 对于不可比较的类型,最常用的是
reflect.DeepEqual()函数