变量声明语句四大组成部分详解
一 声明语句
声明语句用于定义程序的各种实体对象以及部分或全部的属性,Go语言主要有四种类型的声明语句:var、const、type和func,分别对应变量、常量、类型和函数实体对象的声明。本章我们主要介绍是var
二 变量名与类型
声明变量大致做了三件事
- 1、按照数据类型的规定申请好对应大小的内存空间
- 2、将初始值填充到申请好的内存空间中
- 3、变量名字指向变量的首地址
为何变量名指向的是变量的首地址呢???
要搞清楚这件事,我们必须先了解一下内存空间:
内存空间是以字节为单位连续编址的。通俗的讲就是:字节单元(内存中一个个的字节空间称之为字节单元,又称之为内存单元) 是内存存储的基本单位,显而易见,若想操作内存的某一处,必须为每一个字节单元编号,从0开始一直编号到内存的最后(在32位系统下这个编号是用4bytes(32个bit)表示的,64位系统下是8bytes(64bit)表示的),每一个字节单元对应着一个唯一的编号, 这个编号被称为内存单元的地址,即内存地址。因为字节单元是连续的,所以编号当然也是连续的。
如果把内存单元比喻为小柜子,那么内单元的地址就相当于柜子的编号
强调第一遍:类型很重要!!!
编译器在编译时会为变量分配相应内存空间,而类型则决定了变量占用内存空间的大小和布局,比如int8会占用1字节、int16会占用2字节等,对应范围内的值都可以填充/存储在内存中。
变量的地址!!!
1、变量所占内存空间的首个字节单元的地址,称之为该变量的地址。
2、拿到了变量的地址就可以访问到变量存储在内存中的所有内容,what???
你肯定会有疑问:变量存储的内容可能需要很多内存单元来存放,而变量地址也仅仅只是首个字节单元的地址,
若想访问到变量存储在内存中的所有内容,我们难道不应该拿到该变量所有内存单元的地址吗???答案即将揭晓
强调第二遍:类型真的很重要!!!
因为变量的内存单元都是连续的,所以通过变量的首地址,加上类型规定的字节个数,就可以依次读出变量在内存中存放的数据。
!!!!!!!!!!!!!!!!!!!!!
在此强调一个非常重要的知识:
宽度也是类型的一部分,而且是非常重要的一个部分
例如int32与int64虽然都叫整型,但因其宽度不同则决然不是同一种类型,类似的例子还有:float64与float32,int32与int64,[3]int与[2]int
因为go是强类型语言,区分开类型是非常重要的,所以该点在日后会反复用到,
!!!!!!!!!!!!!!!!!!!!
ps:对于一个变量来说,你可以这么理解
类型就好比是房子,值则房子里的物品,不同的类型就好比是房子有两室一厅、三室一厅的
两个变量相等的基本逻辑就是:首先住的房子得一样,然后再看看房子里放的物品是否一样,只有这两点都满足,你才能说这是两套一样的房子。
说到这里,我们知道,要想访问变量存放的内存数据就需要用到变量的地址,这是程序运行时查找变量数据的本质,但如果我们在开发程序时真的用地址去访问变量,那么开发环节将变的十分恶心,于是,为了方便我们开发,编程语言推出了变量名的概念,变量名指向变量的地址,我们通过变量名访问变量即可。依据上图举例
var x int16 = 32760
int16占用两个柜子存放32760,假设这两个柜子是c0313、c0317
那么:变量名--->首个柜子的编号c0313
去过澡堂子的男人都知道,变量名有点像手牌
三 详解名字这种东西
先说结论:
名字只是地址的一种助记符,编译完毕后根本不存在变量名字这种东西,都是地址
在编译时,编译器会将变量名解析成地址,编译过后,程序在运行时使用的都是内存地址而非变量名,变量名并不占用内存空间。也就是说变量名只存在于我们编写的代码中,它仅仅只是编程语言为了方便我们开发而推出的一种“内存地址的别名”。
详细地讲
1、程序中用变量来存储数据,用函数来定义一段可以重复使用的代码,它们最终都要放到内存中才能供CPU使用。
2、数据和代码都以二进制的形式存储在内存中,计算机无法从格式上区分某块内存到底存储的是数据还是代码。当程序被加载到内存后,操作系统会给不同的内存块指定不同的权限,拥有读取和执行权限的内存块就是代码,而拥有读取和写入权限(也可能只有读取权限)的内存块就是数据。
3、CPU 只能通过地址来取得内存中的代码和数据,程序在执行过程中会告知 CPU 要执行的代码以及要读写的数据的地址。如果程序不小心出错,或者开发者有意为之,在 CPU 要写入数据时给它一个代码区域的地址,就会发生内存访问错误。这种内存访问错误会被硬件和操作系统拦截,强制程序崩溃,程序员没有挽救的机会。
4、CPU 访问内存时需要的是地址,而不是变量名和函数名!变量名和函数名只是地址的一种助记符,当源文件被编译和链接成可执行程序后,它们都会被替换成地址,编译和链接过程的一项重要任务就是找到这些名称所对应的地址。
***注意:此处所讲的地址不能以物理内存地址来理解,因为程序直接操作的其实是虚拟内存而不是真正的物理内存,也就是说在编译时就已经确定的地址其实指的是虚拟内存地址,并且这些地址是根据编译器的地址分配规则分配给变量的,而不是随机分配的。这会牵扯到一个叫"计算机寻址方式"的问题,若想深度理解的话需要自行研究一下汇编语言。***
5、假设变量 a、b、c 在内存中的地址分别是 0X1000、0X2000、0X3000,那么加法运算c = a + b;将会被转换成类似下面的形式:
0X3000 = (0X1000) + (0X2000);
( )表示取值操作,整个表达式的意思是,取出地址 0X1000 和 0X2000 上的值,将它们相加,把相加的结果赋值给地址为 0X3000 的内存
变量名和函数名为我们提供了方便,让我们在编写代码的过程中可以使用易于阅读和理解的英文字符串,不用直接面对二进制地址,那场景简直让人崩溃。
需要注意的是,虽然变量名、函数名、指针名、数组名等名字在本质上是一样的,它们都是地址的助记符。
即:名字->地址->内存中存储的数据or代码。***数据或代码在内存中存放着,我们的程序中用的一切都是地址。***
虽然一切名字对应的都是地址,但在编写代码的过程中,不同的名字还是有区别的:
// 1、变量的名字:
变量的名字->地址->内存中存储的数据
变量的名字对应的是地址,按理说我们拿到变量名进行打印读出来的应该是内存地址才对,但程序中变量的名字享受VIP服务,如果是变量名,程序会自动沿着它对应的内存地址帮我们找到值,所以
var x int = 10
fmt.Println(x) // 按理说得到的应该是变量名x对应的内存地址才对,但其实是顺着它的地址取到了对应的值
类比一下:
门牌号->房间地址->房间中住的人
变量名相当于门牌号,对应的都是地址,上面所述的意思就是说,如果我们拿到的是门牌号,理论上得到的应该是一个地址,但其实当我们亮出门牌号时,就会召唤出房间里的神兽
同理,当我们在程序中亮出了变量名时,就相当于大吼了一声,出来吧神兽!!!
所以虽然变量名对应的是内存地址,但我们通常认为变量名表示的就是数据本身,见到变量名就见到了值。
如果我们想在程序中停止变量名的这种默认召唤神兽的操作,需要用"&变量名"的方式,取到的就是变量名对应的那个地址,亮出该地址,它就是一个地址,程序运行过程中不会自动帮我们召唤神兽,这样我们在想召唤时就召唤
// 2、函数的名字
函数的名字->地址->内存中存储的代码
打印函数名,得到就是一个地址,没有召唤神兽的操作,因为召唤出来的是一堆代码,代码是用来执行的,而不是取出来看的,所以召唤神兽也没啥意义,所以在程序中亮出函数的名字得到就是一个地址而已
四 变量名的命名规则与风格
(1)命名规则:函数名、变量名、常量名、类型名、语句标号和包名等所有名字,必须遵循
1、由于字母、数字、下划线组成
2、只能以字母或下划线开头
3、区分大小写,例如heapSort和Heapsort是两个不同的名字。
4、不能使用go的关键字
5、通常情况下:不建议使用预定义的名字,否则会覆盖
极少数的特殊场景中:重新定义预定义的名字也是有意义的。
但是,大量重新定义它们极容易引起语义混乱,所以通常情况下不要这么做。
Go语言中有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
37多个预定义的名字、或称保留字
内建常量: true false iota nil
内建类型: int int8 int16 int32 int64
uint uint8 uint16 uint32 uint64 uintptr
float32 float64 complex128 complex64
bool byte rune string error
内建函数: make len cap new append copy close delete
complex real imag
panic recover
(2)命名风格
//1、名字的长短问题
名字的长度没有逻辑限制,
但是Go语言的风格是尽量使用短小的名字,对于局部变量(比如函数内的变量)尤其是这样。
通常来说,如果一个名字的作用域比较大,生命周期也比较长,建议使用长的名字。
//2、驼峰体
Go推荐使用驼峰式命名
//3、缩略词
需要注意的是:因为缩略词本身就是对某一个或某一组单词的缩写,即提取某一个或某一组单词的首字母拼接而成,所以我们对待缩略词的每一个单词应该一视同仁,要么同时大写要么同时小写。
比如
像ASCII和HTML这样的缩略词则应该保持整体一致,要么全为大写,要么全为小写,避免使用大小写混合的写法。
例如:
推荐写法:htmlEscape、HTMLEscape或escapeHTML
垃圾写法:escapeHtml。
再举个例子,比如name,写成NaMe,多丑
而且,在go中如果名字的首字母大写代表对外公开(我们将在包相关知识点中介绍),如果想变成私有的,那么首字母需要是小写的,结合缩略词的书写应该保持一致这一规范,举例如下
(1) api是(Application Programming Interface)的缩略词,所以
要么写成私有的:apiClient
要么写成公有的:APIClient,而不要写成ApiClient
(2) id是identification的缩略词所以
要么写成私有的:userID,而不要写成userId
要么写成公有的:UserID,而不要写成UserId
(4) url是(Uniform Resource Location)的缩略词,所以
要么写成私有的:urlArray
要么写成公有的:URLArray,而不要写成UrlArray
//4、命名布尔类型的变量名
若变量类型为 bool 类型,则名称应以 Has, Is, Can 或 Allow 开头
var isExist bool
var hasConflict bool
var canManage bool
var allowGitHook bool
五 初始化表达式
5.1 字面量与数据类型
初始化的表达式可以是字面量或任意的表达式,那什么是字面量呢?
字面量(literal)也称为字面值or字面常量,就是”字面“意思上的量、是指程序中”硬编码“的量,通俗地讲,字面量就是以人类可读形式表示的固定值,有以下几种
666
3.1
3.2+12i
true
"egon"
注意注意注意!!!
”字面“与”硬编码“都是在提醒你,它只是它字面的样子,不要自己意淫它的类型,例如666这个字面量就是一系列阿拉伯数字,你可能会说它难道不是int类型吗,当然不是喽,int类型是编程语言中才有的概念,而阿拉伯数字等人类自然语言的符号早在编程语言诞生之前就已经有了,即便没有编程语言666、3.1,“hello”这些值也都是存在的,比如,你随便找个人让他看看一眼666他都认识,但你跟他讲int类型,除非他是程序员他才能听懂你在讲什么。也就是说,是编程语言将字面量666与数据类型int这两种概念联系到了一起。
综上所述,字面量与数据类型是两件事情,二者的关系是什么呢?举例说明如下
var x int64 = 666
go会先按照int64类型的规定开辟好内存空间,然将字面值放入该空间中。
所以,我们可以把编程语言中的不同数据类型想象成不同尺寸的盒子,把字面量想象成物品。
1、每用var声明一个int64类型,就是造出了一个64号尺寸的"盒子"
2、字面量666可被比喻为"苹果"
再次强调,类型与字面量是两个概念,你总不能说"苹果"是"盒子"吧!!!
在有的语言中,却会直接将字面量与特定的类型混杂在一起,这么做是不好的
比如666这一字面量在C语言中会认为是一个int类型,一个long类型的字面量需要写成666l
在python2中中也出现过long类型需要写成666L,但在python3中就废弃这种标识方式。
因为这是把字面与数据类型这两个概念混在了一起,与”字面“二字的核心主旨相悖的,”字面“反映出的就应该只是它字面的东西,不应该掺杂类型的概念,因为类型这个东西是另外一件单独的事情,比如666,这个后缀l就掺杂进了类型的概念,那么此时它的字面便不那么字面,没有那么的单纯。
go作为新兴编程语言,在这件事上自然不会犯糊涂。
所以,再强调一遍,我们应该将字面值与类型当成两种概念去看。
也可以这么说,字面量都是没有类型的,字面量都是没有类型的,字面量都是没有类型的
例如
666
3.1
3.2+12i
true
"egon"
上述五种字面量属于无类型的量,但如果以后真的这么叫,那我说无类型的量,你猜我说的是"egon"还3.1,所以为了好区分,虽然他们没有类型,但是对于这五种字面量我们通常这么称呼
666 无类型的int
3.1 无类型的float
3.2+12i 无类型的complex
true 无类型的bool
"egon" 无类型的string
5.2 初始化表达式生效时间
1、在包级别声明的变量会在main入口函数执行前就完成初始化,例如
package main
var x int = 3 * 4 // main函数执行前就会计算好3*4的值
func main() {
}
2、局部变量将在声明语句执行到的时间才完成初始化,例如
package main
import "fmt"
func test() {
var y int = 4 * 6
fmt.Println(y)
}
func main() {
test() // 执行函数时才会完成变量y中表达式的初始化
}
5.3 类型推导与零值初始化机制
声明语句所包含的后两个部分:“类型”和“= 表达式”,可以省略其中的一个。
(1) 如果省略的是类型信息,即“类型”,如下所示,那么go将根据初始化表达式来推导变量的类型信息
初始化表达式的结果 推导出的类型
整数 ---------> int类型
浮点数 ---------> float64类型
复数 ---------> complex128类型
true或false ---------> bool类型
"egon" ---------> string类型
// 例1:
var x = 123
fmt.Printf("%T",x) // int
//例2:
var x = 123 + 456
fmt.Printf("%T",x) // int
(2)如果省略的是初始x化表达式,即“= 表达式”,如下所示,那么go将使用零值初始化该变量,这称之为go 语言的”零值初始化机制“
var 变量名字 类型
// 零值对应:
// 1、数值类型的变量对应的零值----------------------------------------->0
// 2、布尔类型的变量对应的零值----------------------------------------->false
// 3、字符串类型的变量对应的零值---------------------------------------> 空字符串
// 4、接口或引用类型(包括slice、map、chan和函数)的变量对应的零值---------->nil
// 5、数组或结构体等聚合类型的变量对应的零值是每个元素或字段都是对应该类型的零值。
// 例1:
var num int
fmt.Println(num) // 0
// 例2:
var s string
fmt.Println(s) // ""
零值初始化机制真的非常优秀
1、首先,它为我们带来的最直观的体验就是代码得到简化
1、再者,它使得Go语言中不存在未初始化的变量,因为哪怕我们不为变量初始化值,该变量也总会有一个初始值。
2、最后,它可以在没有增加额外工作的前提下确保边界条件下的合理行为(如果没有零值机制,我们接下来要执行的操作是数值相加,那么就需要事先判断变量的类型了,如果不判断,就有可能是字符串类型与数值类型相加,进而引发错误)。