Go语言基础语法讲解与学习
1 Go语言基础知识介绍
介绍Go语言之前,我们先了解一下有哪些开源项目是Go语言开发的,其中就包括 Docker、Go-Ethereum、Thrraform 和 Kubernetes。
Go语言经过了十几年的发展与成长,已经得到了不断的完善和优化。所以对于像我们这样想学习goalng的入门初学者来说,只要保持正确的学习方向和路径,我们的学习成果一定能够得到证明我们的努力和付出是值得的!
另外说下学习golang的一些个人感受:golang在某些地方很像C语言,所以如果你已经掌握了C语言或其它编程语言,那么你能够很快的掌握golang的一些基础运用。并且你会在对golang的一些指针操作、内存分配以及多协程处理上有更深的认识和体会。但是在日后的项目设计上,建议可以先尝试以golang的设计思想去实现一些功能设计,而不要代入C的一些设计思想。如果你没有学习过C语言或者其他编程语言,golang作为你的第一入门语言来说也不会非常困难,因为它的语法和规则以及设计思想已经帮助你解决了很多问题,而你只需要把大部分精力用来进行代码设计。但是,请不要放弃对底层原理的学习和探索,这样你才能够对golang掌握的更加熟练。
1.1 Go语言设计的由来
Go语言出自 Ken Thompson 和 Rob Pike、Robert Griesemer 之手,他们都是计算机科学领域的重量级人物。
-
Ken Thompson
贝尔实验室 Unix 团队成员,C语言、Unix 和 Plan 9 的创始人之一,在 20 世纪 70 年代,设计并实现了最初的 UNIX 操作系统,仅从这一点说,他对计算机科学的贡献怎么强调都不过分。他还与 Rob Pike 合作设计了 UTF-8 编码方案。
-
Rob Pike
Go语言项目总负责人,贝尔实验室 Unix 团队成员,除帮助设计 UTF-8 外,还帮助开发了分布式多用户操作系统 Plan 9、Inferno 操作系统和 Limbo 编程语言,并与人合著了《The Unix Programming Environment》,对 UNIX 的设计理念做了正统的阐述。
-
Robert Griesemer
就职于 Google,参与开发 Java HotSpot 虚拟机,对语言设计有深入的认识,并负责 Chrome 浏览器和 Node.js 使用的 Google V8 JavaScript 引擎的代码生成部分。
其实Golang的诞生初衷是为了满足Google本身的需求,但是这门语言的设计却融合了Go语言团队多年的经验和对编程语言设计的深入认识。设计团队借鉴了 Pascal、Oberon 和C语言的设计智慧,同时让Go语言具备动态语言的便利性。
Go语言的所有设计者都说,设计Go语言是因为 C++ 给他们带来了挫败感。在 Google I/O 2012 的 Go 设计小组见面会上,Rob Pike 是这样说的:
我们做了大量的 C++ 开发,厌烦了等待编译完成,尽管这是玩笑,但在很大程度上来说也是事实。
1.2 Go 是编译型语言
Go 使用编译器来编译代码。编译器将源代码编译成二进制(或字节码)格式;在编译代码时,编译器检查错误、优化性能并输出可在不同平台上运行的二进制文件。要创建并运行 Go 程序,程序员必须执行如下步骤。
(1)使用文本编辑器创建 Go 程序;
(2)保存文件;
(3)编译程序;
(4)运行编译得到的可执行文件。
这不同于 Python、Ruby 和 JavaScript 等语言,它们不包含编译步骤。Go 自带了编译器,因此无须单独安装编译器。
1.3 Go工程结构
一个Go语言项目的目录一般包含以下三个子目录:
- src 目录:放置项目和库的源文件;
- pkg 目录:放置编译后生成的包/库的归档文件;
- bin 目录:放置编译后生成的可执行文件。
三个目录中我们需要重点关注的是 src 目录,其他两个目录了解即可,下面来分别介绍一下这三个目录。
src 目录
用于以包(package)的形式组织并存放 Go 源文件,这里的包与 src 下的每个子目录是一一对应。例如,若一个源文件被声明属于 log 包,那么它就应当保存在 src/log 目录中。
并不是说 src 目录下不能存放 Go 源文件,一般在测试或演示的时候也可以把 Go 源文件直接放在 src 目录下,但是这么做的话就只能声明该源文件属于 main 包了。正常开发中还是建议大家把 Go 源文件放入特定的目录中。
包是Go语言管理代码的重要机制,其作用类似于Java中的 package 和 C/C++ 的头文件。Go 源文件中第一段有效代码必须是package <包名>
的形式,如 package hello。
另外需要注意的是,Go语言会把通过go get
命令获取到的库源文件下载到 src 目录下对应的文件夹当中。
pkg 目录
用于存放通过go install
命令安装某个包后的归档文件。归档文件是指那些名称以“.a”结尾的文件。
该目录与 GOROOT 目录(也就是Go语言的安装目录)下的 pkg 目录功能类似,区别在于这里的 pkg 目录专门用来存放项目代码的归档文件。
编译和安装项目代码的过程一般会以代码包为单位进行,比如 log 包被编译安装后,将生成一个名为 log.a 的归档文件,并存放在当前项目的 pkg 目录下。
bin 目录
与 pkg 目录类似,在通过go install
命令完成安装后,保存由 Go 命令源文件生成的可执行文件。在类 Unix 操作系统下,这个可执行文件的名称与命令源文件的文件名相同。而在 Windows 操作系统下,这个可执行文件的名称则是命令源文件的文件名加 .exe 后缀。
2 Go基本语法
2.1 变量
数学课中我们知道变量表示没有固定值且可改变的数,计算机课中我们又了解到变量是一段或多段用来存储数据的内存。
作为静态类型的Go语言来说,Go的变量总是有固定的数据类型,类型决定了变量内存的长度和存储格式。我们可以修改变量的变量值,但是无法更改变量的变量类型。
-
定义
关键字
var
用来定义变量,类型放在变量名的后面,例如:var x int // go会在定义的时候自动初始化变量,int类型默认为0 var y = true // 也可在定义的时候进行赋值,这里会自动推断出bool类型,并赋值ture var a,b,c string // 同时定义多个相同类型的变量 var i,str = 1, "string" // 同时定义并初始化多个不同类型的变量 // 一般会在多个变量需要定义时采用组的形式进行定义 var ( x int y = true a,b,c string i,str = 1, "string" )
上面是一般的定义形式,我们可以放在开头进行变量定义。除了上面的定义方式外,golang还提供了一种简短模式进行变量定义,例如:
func main() { y := true i, str = 1, "string" }
这种定义方式通过
:=
来进行定义,但是使用的时候一定需要主要以下几点:(1)定义变量同时显式初始化。
(2)不能提供数据类型。
(3)只能在函数内部使用
我们在使用上面这种定义方式的时候一定需要弄清楚变量的作用域,因为有时候变量名称相同,但是表示的作用域和含义是不同的。例如:
var gX = 100 // 先定义一个int类型的全局变量gX,值是100 func main () { fmt.Println(&gX, gX) // 打印gX变量的地址和值 gX := "str" fmt.Println(&gX, gX) } /* 输出: 0xac041 100 0xc630040a10 "str" 对比内存地址和值就可以发现上面两个gX变量虽然名字相同,但是却是两个不同的变量。 */
golang里面还有一种匿名变量,当我们遇到一些没有名称的变量、类型或者方法的时候就能使用匿名变量来增强代码的灵活性。
匿名变量的特点就是一个下划线
_
,_
本身就是一个特殊的标识符,可以称作空白标识符。它可以像其他标识符那样用于变量的声明或赋值(任何类型都可以赋值给它),但任何赋给这个标识符的值都将被抛弃,因此这些值不能在后续的代码中使用,也不可以使用这个标识符作为变量对其它变量进行赋值或运算。使用匿名变量时,只需要在变量声明的地方使用下画线替换即可。例如:func getNumber()(int,int){ return 1, 100 } func main() { num1, _ := getNumber() _, num100 := getNumber() fmt.Println(num1, num100) } /* 输出: 1 100 这里的getNumber函数是拥有两个整数的返回值,所以每次调用的时候会返回1和100两个数值。 当我们只想使用其中某一个数值时,我们就可以使用匿名变量来接收其中一个数值。 */
注意:匿名变量不占用内存空间,也不会分配内存。匿名变量与匿名变量之间也不会因为多次声明而无法使用。
-
作用域
一个变量(常量、类型或函数)在程序中都有一定的作用范围,称之为作用域。
了解变量的作用域对我们学习Go语言来说是比较重要的,因为Go语言会在编译时检查每个变量是否使用过,一旦出现未使用的变量,就会报编译错误。如果不能理解变量的作用域,就有可能会带来一些不明所以的编译错误。
我们可以根据变量的位置不同,分为局部变量、全局变量、形式参数三个类型。
-
局部变量
声明在函数体内的变量我们称之为局部变量,它的作用域只在函数体内部,函数的参数和返回值变量都属于局部变量。了解C知识就知道,局部变量不是一直存在的,它只定义在它的函数被调用后存在,函数调用结束后这个局部变量就会被销毁掉。例如:
package main import ( "fmt" ) // main函数体外部 func main() { // main函数体内部 // 我们在main函数体内部声明两个变量x,y并赋值 x := 0 y := 1 // 声明一个num变量并将x和y的和赋值给num num := x + y fmt.Printf(" x = ",x," y = ",y," num = ",num) } /* 输出: x = 0 y = 1 num = 1 这里面的x,y,num三个变量都属于局部变量 */
-
全局变量
在函数体外声明的变量称之为全局变量,全局变量只需要在一个源文件中定义,就可以在所有源文件中使用,当然,不包含这个全局变量的源文件需要使用“import”关键字引入全局变量所在的源文件之后才能使用这个全局变量。
全局变量声明必须以 var 关键字开头,如果想要在外部包中使用全局变量的首字母必须大写。
package main import ( "fmt" ) // main函数体外部 // 声明一个全局变量num var num int func main() { // main函数体内部 // 我们在main函数体内部声明两个变量x,y并赋值 x := 0 y := 1 // 给全局num变量进行赋值 num = x + y fmt.Printf(" x = ",x," y = ",y," num = ",num) } /* 输出: x = 0 y = 1 num = 1 这里面的x,y两个变量属于局部变量,num属于全局变量 */
Go语言程序中全局变量与局部变量名称可以相同,但是函数体内的局部变量会被优先考虑。
-
形式参数
在定义函数时函数名后面括号中的变量叫做形式参数(简称形参)。形式参数只在函数调用时才会生效,函数调用结束后就会被销毁,在函数未被调用时,函数的形参并不占用实际的存储单元,也没有实际值。
形式参数会作为函数的局部变量来使用。
package main import ( "fmt" ) //全局变量 a var a int = 33 func main() { //局部变量 a 和 b var a int = 1 var b int = 0 fmt.Printf("main() 函数中 a = %d\n", a) fmt.Printf("main() 函数中 b = %d\n", b) c := sum(a, b) fmt.Printf("main() 函数中 c = %d\n", c) } func sum(a, b int) int { fmt.Printf("sum() 函数中 a = %d\n", a) fmt.Printf("sum() 函数中 b = %d\n", b) num := a + b return num } /* 输出: main() 函数中 a = 1 main() 函数中 b = 0 sum() 函数中 a = 1 sum() 函数中 b = 0 main() 函数中 c = 1 */
-
2.2 常量
常量表示运行时恒定并且不可改变的值,通常是一些字面量。Go语言中使用关键字const
来定义常量,用来存储这些不会改变的数据。
常量值必须是在编译期就可以确定的字符、字符串、数字或布尔值。例如:
// 显式类型定义:
const a string = "Hello"
// 隐式类型定义的时候编译器可以根据变量值来推断其类型
// 隐式类型定义:
const a = "Hello"
// 常量可以在其赋值表达式中涉及计算过程,但是所有用于计算的值必须在编译期间就能获得。
正确做法:
const b = 1+1
错误做法:
const b = getNumber()
常量间的所有算术运算、逻辑运算和比较运算的结果也是常量,对常量的类型转换操作或以下函数调用都是返回常量结果:len
、cap
、real
、imag
、complex
和 unsafe.Sizeof
。
-
iota常量生成器
Go语言并没有明确意义上的
enum
枚举定义,不过我们可以借助iota标识符来实现一组自增常量值。例如:const ( x = iota // 0 y // 1 z // 2 ) const ( _ = iota // 0 KB = 1 << (10 * iota) // 1 << (10 * 1) MB // 1 << (10 * 2) GB // 1 << (10 * 3) )
我们也可以在多个常量定义中使用多个iota,它们各自单独计数,但是需要确保每行常量的列数相同。例如:
const ( _, _ = iota,iota*10 //0, 0*10 a, b //1, 1*10 c, d //2, 2*10 )
我们如果希望中断iota的自增,我们就需要显式恢复。同时如果后续又想使用iota,则后续自增是按照行序递增的,不像C语言的enum那样按上一取值递增。例如:
const ( a = iota // 0 b // 1 c = 100 // 100 d // 100(iota自增中断,值同上一行常量右值表达式) e = iota // 4(恢复iota自增,值按照行序递增,包括c、d) f // 5 )
2.3 命名
这里给一些命名建议:
- Go的命名区分大小写,并且首字母大小写决定了其作用域。
- 一般使用驼峰命名法。
- 局部变量优先使用短名称。
- 命名一般是由字母或者下划线开始,由多个字母、数字和下划线组合而成。
- 不要使用保留关键字。
- 不建议使用与预定义常量、类型、内置函数相同的名字。
- 专有名词一般全部大写。
2.4 基本类型
类型 | 长度(字节) | 默认值 | 说明 |
---|---|---|---|
bool | 1 | false | 布尔类型,真用true表示,假用false表示 |
byte | 1 | 0 | 字节类型,可以看作是一个8位二进制数表示的无符号整数类型,uint8 |
int,uint | 4,8 | 0 | 默认整数类型,根据平台32位或64位不同,长度也不同 |
int8,uint8 | 1 | 0 | -128~127,0~255 |
int16,uint16 | 2 | 0 | -32768~32767,0~65535 |
int32,uint32 | 4 | 0 | -21亿~21亿,0~42亿 |
int64,uint64 | 8 | 0 | |
float32 | 4 | 0.0 | 由32位二进制数表示的浮点数类型 |
float64 | 8 | 0.0 | 由64位二进制数表示的浮点数类型,默认浮点数类型 |
complex64 | 8 | 0.0+0.01 | 由64位二进制数表示的复数类型,float32类型的实部和虚部联合表示 |
complex128 | 16 | 0.0+.0.01 | 由128位二进制数表示的复数类型,float64类型的实部和虚部联合表示 |
rune | 4 | 0 | unicode code point, int32 |
uintptr | 4,8 | 0 | 足以存储指针的uint |
string | - | “” | 字符串,默认为空字符串,而非NULL |
array | 数组 | ||
struct | 结构体 | ||
function | nil | 函数 | |
interface | nil | 接口 | |
map | nil | 字典,引用类型 | |
slice | nil | 切片,引用类型 | |
channel | nil | 通道,引用类型 |
-
引用类型
引用类型特指
slice
、map
、channel
这三种预定义类型。 引用类型拥有更复杂的存储结构,除了分配内存以外,它们还须初始化一系列的属性,例如:指针、长度,甚至包含哈希分布、数据队列等。
内置函数
new
可以按照指定类型长度分配零值内存,返回指针,并不关心类型内部构造和初始化方式。而引用类型必须使用make
函数创建,编译器会将make
转换为目标类型专用的创建函数(或指令),以确保完成全部内存分配和相关属性初始化。 -
整数类型
Go语言的数值类型分为以下几种:整数、浮点数、复数,其中每一种都包含了不同大小的数值类型,例如有符号整数包含
int8
、int16
、int32
、int64
等,每种数值类型都决定了对应的大小范围和是否支持正负符号。 大多数情况下,我们只需要 int 一种整型即可,它可以用于循环计数器(for 循环中控制循环次数的变量)、数组和切片的索引,以及任何通用目的的整型运算符,通常 int 类型的处理速度也是最快的。
-
浮点数类型
Go语言提供了两种精度的浮点数
float32
和float64
,它们的算术规范由IEEE754
浮点数国际标准定义,该浮点数规范被所有现代的 CPU 支持。 一个
float32
类型的浮点数可以提供大约 6 个十进制数的精度,而float64
则可以提供约 15 个十进制数的精度,通常应该优先使用float64
类型,因为float32
类型的累计计算误差很容易扩散,并且float32
能精确表示的正整数并不是很大。 -
复数类型
Go语言中复数的类型有两种,分别是
complex128
(64 位实数和虚数)和complex64
(32 位实数和虚数),其中complex128
为复数的默认类型。 复数的值由三部分组成
RE + IMi
,其中RE
是实数部分,IM
是虚数部分,RE
和IM
均为float
类型,而最后的i
是虚数单位。 对于一个复数
z := complex(x, y)
,可以通过Go语言的内置函数real(z)
来获得该复数的实部,也就是 x;通过imag(z)
获得该复数的虚部,也就是 y。 -
布尔类型
一个布尔类型的值只有两种:
true
或false
。if 和 for 语句的条件部分都是布尔类型的值,并且==
和<
等比较操作也会产生布尔型的值。 布尔型无法参与数值运算,也无法与其他类型进行转换。
-
字符串类型
字符串是一种值类型,且值不可变,即创建某个文本后将无法再次修改这个文本的内容,更深入地讲,字符串是字节的定长数组。
可以使用双引号
""
来定义字符串,字符串中可以使用转义字符来实现换行、缩进等效果,常用的转义字符包括:\n
:换行符\r
:回车符\t
:tab 键\u
或\U:Unicode
字符\\
:反斜杠自身
-
字符类型
字符串中的每一个元素叫做“字符”,在遍历或者单个获取字符串元素时可以获得字符。
Go语言的字符有以下两种:
- 一种是
uint8
类型,或者叫byte
型,代表了 ASCII 码的一个字符。 - 另一种是
rune
类型,代表一个UTF-8
字符,当需要处理中文、日文或者其他复合字符时,则需要用到rune
类型。rune
类型等价于int32
类型。
- 一种是
-
类型转换
Go语言不存在隐式类型转换,因此所有的类型转换都必须显式的声明:
valueOfTypeB = typeB(valueOfTypeA)
。 只有相同底层类型的变量之间可以进行相互转换(如将
int16
类型转换成int32
类型),不同底层类型的变量相互转换时会引发编译错误(如将bool
类型转换为int
类型)。
2.5 关键字
关键字即是被Go语言赋予了特殊含义的单词,也可以称为保留字。
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 |
之所以刻意地将Go语言中的关键字保持的这么少,是为了简化在编译过程中的代码解析。和其它语言一样,关键字不能够作标识符使用。
-
标识符
标识符是指Go语言对各种变量、方法、函数等命名时使用的字符序列,标识符由若干个字母、下划线
_
、和数字组成,且第一个字符必须是字母。通俗的讲就是凡可以自己定义的名称都可以叫做标识符。 标识符的命名需要遵守以下规则:
-
由 26 个英文字母、0~9、
_
组成; -
不能以数字开头,例如
var 1num int
是错误的; -
Go语言中严格区分大小写;
-
标识符不能包含空格;
-
不能以系统保留关键字作为标识符,比如 break,if 等等。
命名标识符时还需要注意以下几点:
-
标识符的命名要尽量采取简短且有意义;
-
不能和标准库中的包名重复;
-
为变量、函数、常量命名时采用驼峰命名法,例如
stuName
、getVal
;
Go语言中还存在着一些特殊的标识符,叫做预定义标识符,如下表所示:
-
append | bool | byte | cap | close | complex | complex64 | complex128 | uint16 |
---|---|---|---|---|---|---|---|---|
copy | false | float32 | float64 | imag | int | int8 | int16 | uint32 |
int32 | int64 | iota | len | make | new | nil | panic | uint64 |
println | real | recover | string | true | uint | uint8 | uintptr |
预定义标识符一共有 36 个,主要包含Go语言中的基础数据类型和内置函数,这些预定义标识符也不可以当做标识符来使用。
2.6 运算符
运算符是用来在程序运行时执行数学或逻辑运算的,在Go语言中,一个表达式可以包含多个运算符,当表达式中存在多个运算符时,就会遇到优先级的问题,此时应该先处理哪个运算符呢?这个就由Go语言运算符的优先级来决定的。
Go语言有几十种运算符,被分成十几个级别,有的运算符优先级不同,有的运算符优先级相同,请看下表。
优先级 | 分类 | 运算符 | 结合性 |
---|---|---|---|
1 | 逗号运算符 | , | 从左到右 |
2 | 赋值运算符 | =、+=、-=、*=、/=、 %=、 >=、 <<=、&=、^=、|= | 从右到左 |
3 | 逻辑或 | || | 从左到右 |
4 | 逻辑与 | && | 从左到右 |
5 | 按位或 | | | 从左到右 |
6 | 按位异或 | ^ | 从左到右 |
7 | 按位与 | & | 从左到右 |
8 | 相等/不等 | ==、!= | 从左到右 |
9 | 关系运算符 | <、<=、>、>= | 从左到右 |
10 | 位移运算符 | <<、>> | 从左到右 |
11 | 加法/减法 | +、- | 从左到右 |
12 | 乘法/除法/取余 | *(乘号)、/、% | 从左到右 |
13 | 单目运算符 | !、*(指针)、& 、++、–、+(正号)、-(负号) | 从右到左 |
14 | 后缀运算符 | ( )、[ ]、-> | 从左到右 |
注意:优先级值越大,表示优先级越高。
2.7 小结
掌握基本的语法知识是学习一门语言的必经之路,基础的知识只要细细思考和琢磨就会引发出多种多样的思路和问题。我们探索和解决这些问题的过程就是在巩固基础与深入学习,基础打得牢才能事半功倍。
本文参考并借鉴了雨痕大佬的《Go语言学习笔记》,有兴趣的可以去看看这本书。