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()

常量间的所有算术运算、逻辑运算和比较运算的结果也是常量,对常量的类型转换操作或以下函数调用都是返回常量结果:lencaprealimagcomplex 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

通道,引用类型

  • 引用类型
    引用类型特指slicemapchannel这三种预定义类型。
    引用类型拥有更复杂的存储结构,除了分配内存以外,它们还须初始化一系列的属性,例如:指针、长度,甚至包含哈希分布、数据队列等。
    内置函数new可以按照指定类型长度分配零值内存,返回指针,并不关心类型内部构造和初始化方式。而引用类型必须使用make函数创建,编译器会将make转换为目标类型专用的创建函数(或指令),以确保完成全部内存分配和相关属性初始化。
  • 整数类型
    Go语言的数值类型分为以下几种:整数、浮点数、复数,其中每一种都包含了不同大小的数值类型,例如有符号整数包含 int8int16int32int64 等,每种数值类型都决定了对应的大小范围和是否支持正负符号。
    大多数情况下,我们只需要 int 一种整型即可,它可以用于循环计数器(for 循环中控制循环次数的变量)、数组和切片的索引,以及任何通用目的的整型运算符,通常 int 类型的处理速度也是最快的。
  • 浮点数类型
    Go语言提供了两种精度的浮点数 float32float64,它们的算术规范由 IEEE754 浮点数国际标准定义,该浮点数规范被所有现代的 CPU 支持。
    一个 float32 类型的浮点数可以提供大约 6 个十进制数的精度,而 float64 则可以提供约 15 个十进制数的精度,通常应该优先使用 float64 类型,因为 float32 类型的累计计算误差很容易扩散,并且 float32 能精确表示的正整数并不是很大。
  • 复数类型
    Go语言中复数的类型有两种,分别是 complex128(64 位实数和虚数)和 complex64(32 位实数和虚数),其中 complex128 为复数的默认类型。
    复数的值由三部分组成 RE + IMi,其中 RE 是实数部分,IM 是虚数部分,REIM 均为 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 等等。
    命名标识符时还需要注意以下几点:
  • 标识符的命名要尽量采取简短且有意义;
  • 不能和标准库中的包名重复;
  • 为变量、函数、常量命名时采用驼峰命名法,例如 stuNamegetVal

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

print

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语言学习笔记》,有兴趣的可以去看看这本书。