03 Go语言数据类型、变量、常量
文章目录
3.1 Go的数据类型
数据类型的分类
值类型与引用类型
- 值类型:变量中直接存储数据值。
- 引用类型:变量中存储了地址,这个地址指向的内存空间是真正的数据值。
下图a为值类型,p为引用类型:
Go中哪些是值类型,哪些是引用类型?
- 基本数据类型、数组、结构体是值类型。
- 其他的数据类型都是引用类型(指针、切片、函数、接口、Map)。
数据类型的零值
在Go中,数据类型都有默认值。如果程序员没有赋初值,就会保留默认值。默认值也叫零值。
数据类型 | 零值 |
---|---|
布尔型 | false |
数值型 | 0 |
字符串型 | “” |
数组、结构体 | 其所有元素或数据成员取零值 |
引用类型 | nil |
零值机制保证所有的变量是良好定义的,不存在未初始化变量。
例如下面的代码:
package main
import "fmt"
func main() {
var a bool
var b int
var c string
var d *int
fmt.Println(a)
fmt.Println(b)
fmt.Println(c)
fmt.Println(d)
}
// 程序执行后输出:
// false
// 0
//
// <nil>
3.2 标识符的命名规则
标识符用于命名变量、常量、函数、类型等程序实体。
25个关键字(不能用来命名):
break default func interface select
case defer go map struct
chan else goto package switch
const fallthrough if range type
continue for import return var
30多个预定义标识符(可以用来命名,但有冲突的风险):
-
预定义常量
true false iota nil
-
预定义类型
int int8 int16 int32 int64 uint uint8 uint16 uint32 uint64 uintptr float32 float64 comlex128 complex64 bool byte rune string error
-
预定义函数
make len cap new append copy close delete complex real imag panic recover
标识符命名规则
- 开头是字母或下划线
- 后跟任意数量的字母、数字或下划线
- 区分大小写
举例:
package main
func main() {
// 合法的标识符
var abc123 int
var Abc123 int
var _abc123 int
var _Abc123 int
// 不合法的标识符
// var break int // 编译错误:不能用关键字作为标识符
// var 123abc int // 编译错误:不能以数字开头
}
3.3 变量声明
变量声明是指定义一个变量并且为变量指定一个名称。
通用格式
var 变量名 数据类型 = 初始化表达式
数据类型可以省略,此时变量的类型将由初始化表达式的类型决定:
var 变量名 = 初始化表达式
初始化表达式可以省略,此时变量的值为对应类型的零值:
var 变量名 数据类型
举例:
var sa string = "hello-a"
var sb string // 初始化为 ""
var sc = "hello-c"
简短格式
变量的类型由初始化表达式的类型决定:
变量名 := 初始化表达式
举例:
sd := "hello-d"
一次声明多个变量
// 标准格式
var 变量名1, 变量名2, ... 数据类型 = 初始化表达式1, 初始化表达式2, ...
var 变量名1, 变量名2, ... 数据类型
var 变量名1, 变量名2, ... = 初始化表达式1, 初始化表达式2, ...
// 简短格式
变量名1, 变量名2, ... := 初始化表达式1, 初始化表达式2, ...
举例:
// 标准格式
var se, sf, sg string = "hello-e", "hello-f", "hello-g"
var sh, si, sj string // // 初始化为 "", "", ""
var sk, sl, sm = "hello-k", "hello-l", "hello-m"
// 简短格式
sn, so := "hello-n", "hello-o"
一次声明多个变量(大括号方式),常用于声明全局变量
var (
变量名1 数据类型 = 初始化表达式1
变量名2 数据类型
变量名3 = 初始化表达式3
)
举例:
var (
sa string = "hello-a"
sb string
sc = "hello-c"
)
全局变量&局部变量&形式参数
Go语言中变量可以在三个地方声明:
- 在函数外声明的变量称为全局变量(包级别变量)
- 在函数内声明的变量称为局部变量
- 函数的形参和返回值变量成为形式参数
示例:
package main
import "fmt"
// 全局变量
var baseNum int = 5
func main() {
// 局部变量
var v1 int = 3
var v2 int = 4
var sum int
sum = add(v1, v2)
fmt.Println(sum) // 输出 12
}
// 函数的参数和返回值也是局部变量
func add(a int, b int) (c int) {
c = a + b + baseNum
return
}
简短格式or通用格式?
局部变量的声明和初始化主要使用简短格式。
通用格式通常是为那些类型跟初始化表达式类型不一致的局部变量保留的,或者用于不设定变量初始值的情况。例如:
package main
func main() {
// 初始化表达式的值和变量的类型不一致时,必须用通用格式指定变量类型
var a float32 = 35
// 不设定初始值时,必须用通用格式
var b int
_ = a
_ = b
}
3.4 变量声明注意
在同一个词法块中,不可以重复声明相同的变量名
比如下面的代码,会引起编译错误:
func sample() {
var s1 string
var s1 string // 重复声明s1
fmt.Println(s1)
}
使用简短格式时不必声明所有左边变量,但必须至少声明一个新变量
如果一些变量已经在同一个代码块中声明,那么对于那些变量,简短声明的行为等同于赋值。比如下面代码:
func sample() {
var s1 string
s1, s2 := "hello1", "hello2" // 仅声明s2。变量s1已经在之前声明,此处对s1仅执行赋值操作。
fmt.Println(s1, s2)
}
简短声明最少需要声明一个新变量,否则,代码无法编译通过。比如下面代码:
func sample() {
var s1, s2 string
s1, s2 := "hello1", "hello2" // !!! 未声明任何新变量,导致编译错误
fmt.Println(s1, s2)
}
局部变量声明后必须使用
局部变量声明后必须使用,否则编译会报错;导入的包也必须使用,否则编译也会报错。
但是,全局变量声明后可以不使用。
例如:
package main
import (
"fmt"
"net/http" // 编译报错:导入了net/http后没有使用
)
var xyz int = 9 // 全局变量声明后可以不使用
func main() {
var abc int = 5
var def int = 6 // 编译报错:声明def后未使用
fmt.Println(abc)
}
3.5 变量的作用域
全局变量
全局变量可以是导出的或非导出的:
-
导出全局变量:如果全局变量是以大写字母开头的,表示该变量被导出,该变量可以在其他包中使用。
-
非导出全局变量:如果全局变量不是以大写字母开头,该变量可以在本包内部使用。
示例1(非导出全局变量):
package main
import "fmt"
/* 非导出全局变量 */
var g int = 100
func main() {
fmt.Printf("g = %d\n", g)
}
示例2(导出全局变量):
apple/apple.go
package apple
/* 导出全局变量 */
var G int = 200
sample2.go
package main
import "fmt"
import "apple"
func main() {
fmt.Printf("G = %d\n", apple.G)
}
局部变量、形式参数
形式参数等同于局部变量。
局部变量的作用域从声明该变量开始,到该变量所处的代码块结束。
局部变量只能在声明它的代码块的词法域中使用,而且需要先声明后使用。
示例:
package main
import "fmt"
func main() {
fmt.Println(a) // 编译错误:变量a在此处无效
var a int = 5
fmt.Println(a)
{
var b int = 6
fmt.Println(b)
}
fmt.Println(b) // 编译错误:变量b在此处无效
}
作用域嵌套
如果在局部变量作用域中的嵌套代码块声明了同名的局部变量,那么在内层代码块中内层变量将屏蔽外层变量。
例如:
package main
import "fmt"
func main() {
var a int = 100
{
var a int = 200
fmt.Printf("inner a = %d\n", a) // 输出 200
}
fmt.Printf("outer a = %d\n", a) // 输出 100
}
如果局部变量与全局变量同名,那么在局部变量的作用域中,将会屏蔽全局变量。
例如:
package main
import "fmt"
var a int = 100
func main() {
var a int = 200
fmt.Printf("a = %d\n", a) // 输出 200
}
3.6 指针型变量
指针的值是一个变量的地址,指向了实际存放变量的内存位置。
使用指针,可以在不知道变量名字的情况下,间接读取或更新变量的值。
如果一个变量声明为var x int
,那么使用表达式p:= &x
可得到一个指向x变量的指针p,它的类型是*int
。变量p的值是变量x的地址。访问p指向的变量写成:*p
。
x := 1
p := &x
fmt.Println(*p) // 1
*p = 2
fmt.Println(*p) // 2
指针类型的零值是nil。如果p != nil
结果为true
,说明p指向一个变量。
指针是可比较的,当且仅当两个指针指向同一个变量或者两者都是nil时,才相等。
传递一个指针参数给函数,能够让函数更新间接传递的变量值,例如下面代码:
func incr(p *int) int {
*p++ // 递增p指向的值,p自身保持不变
return *p
}
v := 1
incr(&v)
fmt.Println(incr(&v)) // 3
在Go中,函数返回局部变量的地址是非常安全的。下面的代码中,调用f产生的局部变量v在调用返回后依然存在,指针p依然引用它:
var p = f()
func f() *int {
v := 1
return &v
}
3.7 new() 函数
另一种创建变量的方式是使用内置的new函数。
表达式new(T)创建一个T类型变量,初始化为T类型的零值,并返回其地址(地址类型为*T)。
p := new(int)
fmt.Println(*p) // 0
*p = 2
fmt.Println(*p) // 2
new(T)的作用与下面函数相同(设T为某一数据类型):
func NewT() *T {
var t T
return &t
}
因此,在Go语言中new()函数不经常使用。
3.8 变量的生命周期
生命周期指在程序执行的过程中变量存在的时间段。
包级别变量的生命周期是整个程序的执行时间。
局部变量有一个动态的生命周期:每次执行声明语句时创建一个新的实体,变量一直生存直到它变得不可访问,这是它占用的存储空间被回收。
例如下面程序:
package main
import "fmt"
func main() {
var s1 = "hello"
for _, c := range s1 {
d := c - 'a'
fmt.Println(d)
}
}
变量c在循环开始时创建,在循环退出后销毁;变量d在每次迭代中创建,在每次迭代完成后销毁。
注:实际上,在变量变为不可访问之后,并不一定是立即回收的。变量真正在什么时刻被回收,取决于变量是在栈还是堆上分配,以及垃圾回收器的运行机制。
3.9 常量
常量是一种表达式,它可以保证在编译阶段就计算出表达式的值,并不需要等到运行时。
所有常量本质上都是基本类型:布尔型、数字型或字符串。
常量声明
常量声明定义了常量名。它看起来与变量类似,但它的值不可以被修改。如果,要表示数学常量,像圆周率,在Go中用常量比变量合适。
常量声明的格式:
const 常量名 数据类型 = 初始化表达式
const 常量名 = 初始化表达式 // 编译器根据初始化表达式推断数据类型
const 常量名1, 常量名2, ... = 初始化表达式1, 初始化表达式2, ...
举例:
const LENGTH int = 10
const WIDTH int = 5
const PI = 3.1415
const a, b, c = 1, false, "str" //多重赋值
常量的计算在编译时就完成,对常量进行数学运算、逻辑运算和比较运算的结果依然是常量,常量的类型转换结果仍然是常量,某些内置函数(比如len、cap、real、imag、complex、unsafe.Sizeof)的返回值同样是常量。
同时声明一组常量:
const (
Unknown = 0
Female = 1
Male = 2
)
const (
a = 1 //1
b //1 <- 省略右侧的赋值表达式表示复用前面一个常量的值和类型
c = 2 //2
d //2
)
常量生成器iota
iota,特殊常量,可以认为是一个可以被编译器修改的常量。
iota在const关键字出现时将被重置为0(const 内部的第一行之前),const中每新增一行常量声明将使iota计数一次(iota可理解为const语句块中的行索引)。
// 第一个iota等于0,每当新增一行时,iota的值都会自动加1
const (
a = iota //0
b //1
c //2
d = "ha" //独立值,iota += 1
e //"ha" iota += 1
f = 100 //iota +=1
g //100 iota +=1
h = iota //7,恢复计数
i //8
)
3.10 类型声明
类型声明语句定义一种新的命名类型。
从某个已有类型定义一个新的类型,它和已有类型使用同样的底层类型,但作为一种新的类型区别于原类型。
语法格式:
type 新类型名 底层类型名
类型声明通常出现在包级别,这里命名的类型在整个包中可见,如果名字是导出的(以大写字母开头),其他的包也可以访问它。
示例:
type Velocity float64
type AccelerateRate float64
type Time float64
type IntArr4 [4]int
type IntArr4x4 [4][4]int
type MapStrStr map[string]string
新的命令类型并不等同于原类型,它们不能兼容,这样他们就不会在无意中混用:
package main
import "fmt"
type Velocity float64
func (v Velocity)Println() {
fmt.Printf("%v\n", float64(v))
}
func PrintlnVelocity(v Velocity) {
fmt.Printf("%v\n", float64(v))
}
func main() {
var a Velocity = 15.6
var b float64 = 15.6
a.Println()
PrintlnVelocity(a)
_ = b
//b.Println() // 编译错误
//PrintlnVelocity(b) // 编译错误
}
对于每个类型T,都有一个对应的类型转换操作T(x)将值x转换为类型T。如果两个类型具有相同的底层类型或二者都是指向相同底层类型变量的指针,则二者是可以相互转换的。
package main
import "fmt"
type Velocity float64
func main() {
var a Velocity = 15.6
var b float64 = 15.6
fmt.Printf("%v, %v\n", a, float64(a))
fmt.Printf("%v, %v\n", b, Velocity(b))
}
Copyright@2022 , 359152155@qq.com