Golang基础语法 | 青训营笔记

63 阅读18分钟

这是我参与「第三届青训营 -后端场」笔记创作活动的第1篇笔记 image.png

1. Golang语法简介

1.1. Go语言关键字

下表为Go中的关键字或保留字,这些保留字不能用作常量或变量或任何其他标识符名称。

在Go语言中还存在着一些特殊的标识符,叫做预定义标识符,如下表所示:

预定义标识符一共有 36 个,主要包含Go语言中的基础数据类型和内置函数,这些预定义标识符也不可以当做标识符来使用。

1.2. Go标记

Go 程序可以由多个标记组成,可以是关键字,标识符,常量,字符串,符号。如以下 GO 语句由 6 个标记组成:fmt.Println("Hello, World!"),6个标记依次是:

  • fmt
  • .
  • Println
  • (
  • "Hello, World!"
  • )

1.3. 行分隔符

在 Go 程序中,一行代表一个语句结束。每个语句不需要像 C 家族中的其它语言一样以分号 ; 结尾,因为这些工作都将由 Go 编译器自动完成。

如果你打算将多个语句写在同一行,它们则必须使用 ; 人为区分,但在实际开发中我们并不鼓励这种做法。

以下为两个语句:

fmt.Println("Hello, World!")
fmt.Println("Hello,Golang!")

1.4. 注释

Go中有两种注释,// 单行注释/* 多行注释 */

1.5. 标识符

标识符用来命名变量、类型等程序实体,标识符是由字母、数字、下划线_组成的序列,但是第一个字符必须是字母或下划线而不能是数字。

1.6. Go语言的空格

Go 语言中变量的声明必须使用空格隔开,如:var age int;

1.7. 可见性规则

Go语言中,使用大小写来决定该常量、变量、类型、接口、结构或函数是否可以被外部包所调用。

当标识符(包括常量、变量、类型、函数名、结构字段等等)以一个大写字母开头,如:Group1,那么使用这种形式的标识符的对象就可以被外部包的代码所使用(程序需要先导入这个包),这被称为导出(类似Java语言中的 public);标识符如果以小写字母开头,则对包外是不可见的,但是他们在整个包的内部是可见并且可用的(类似Java中语言中的 protected )。

2. 数据类型、变量、常量

2.1. 数据类型

数据类型用于声明函数和变量。数据类型的出现是为了把数据按所需存储内容的大小不同分配内存,需要用大数据的时候才需要申请大内存,充分利用内存。

Go语言中有丰富的数据类型,除了基本的整型、浮点型、布尔型、字符串等基本数据类型外,还有数组、切片、结构体、函数、map、通道(channel)等派生类型,Go 语言的基本类型和其他语言大同小异。

2.1.1. 基本数据类型

  1. 布尔类型

布尔型的值只可以是常量 true 或者 false,如:var flag bool = true

  1. 数字类型
  • 整型
unit8无符号 8 位整型 (0 到 255)
unit16无符号 16 位整型 (0 到 65535)
unit32无符号 32 位整型 (0 到 4294967295)
unit64无符号 64 位整型 (0 到 18446744073709551615)
int8有符号 8 位整型 (-128 到 127)
int16有符号 16 位整型 (-32768 到 32767)
int32有符号 32 位整型 (-2147483648 到 2147483647)
int64有符号 64 位整型 (-9223372036854775808 到 9223372036854775807)

特殊整型:

unit32位操作系统上就是uint32,64位操作系统上就是uint64
int32位操作系统上就是int32,64位操作系统上就是int64
unitptr无符号整型,用于存放一个指针,uintptr 类型只有在底层编程时才需要,特别是Go语言和C语言函数库或操作系统接口相交互的地方
byte等价 uint8,一般用于强调数值是一个原始的数据而不是一个小的整数
rune等价 int32,通常用于表示一个 Unicode 码点
  • 浮点型
float32IEEE-754 32位浮点型数
float64IEEE-754 64位浮点型数
complex6432 位实数和虚数
complex12864 位实数和虚数

float32和float64

float32 类型的浮点数可以提供大约 6 个十进制数的精度,而 float64 则可以提供约 15 个十进制数的精度,通常应该优先使用 float64 类型,因为 float32 类型的累计计算误差很容易扩散,并且 float32 能精确表示的正整数并不是很大。浮点数取值范围的极限值可以在 math 包中找到:

  • 常量math.MaxFloat32 表示 float32 能取到的最大数值,大约是 3.4e38;
  • 常量math.MaxFloat64 表示 float64 能取到的最大数值,大约是 1.8e308;

复数

在计算机中,复数是由两个浮点数表示的,其中一个表示实部(real),一个表示虚部(imag)。
Go语言中复数的类型有两种,分别是 complex128(64 位实数和虚数)和 complex64(32 位实数和虚数),其中 complex128 为复数的默认类型。复数的值由三部分组成 RE + IMi,其中 RE 是实数部分,IM 是虚数部分,RE 和 IM 均为 float 类型,而最后的 i 是虚数单位。声明复数的语法格式如下:

var name complex128 = complex(x, y)

其中 name 为复数的变量名,complex128 为复数的类型,“=”后面的 complex 为Go语言的内置函数用于为复数赋值,x、y 分别表示构成该复数的两个 float64 类型的数值,x 为实部,y 为虚部。上面的复数声明也可以使用简短声明的形式:name := complex(x, y)

  1. 字符串类型
  • 字符串定义

字符串就是一串固定长度的字符连接起来的字符序列,Go 的字符串是由单个字节连接起来的,使用 UTF-8 编码(当字符为 ASCII 码表上的字符时则占用 1 个字节,其它字符根据需要占用 2-4 个字节)标识 Unicode 文本,使用双引号""来定义字符串,字符串中可以使用转义字符来实现换行、缩进等效果。

str := "Hello Golang!"

  • 字符串转义字符

Go 语言的字符串常见转义符包含回车、换行、单双引号、制表符等,如下表所示:

\r回车符
\n换行符
\t制表符
'单引号
"双引号
\反斜杆
  • 字符串拼接符+

可以通过"+"将两个字符串进行拼接,字符串s1和s2拼接后将返回一个新字符串s:

str := "abc" + 
"def"

注:因为编译器会在行尾自动补全分号,所以拼接字符串用的加号“+”必须放在第一行末尾。

  • 定义多行字符串

在Go语言中,使用双引号书写字符串的方式是字符串常见表达方式之一,被称为字符串字面量(string literal),这种双引号字面量不能跨行,如果想要在源码中嵌入一个多行字符串时,就必须使用`反引 号, 代码如下:

func main() {
	s := `Where   
are    		  
you   		  
\n   		  
`
	fmt.Println(s)
}

image.png 两个反引号间的字符串将被原样赋值到 str 变量中,在这种方式下,反引号间换行将被作为字符串中的换行,但是所有的转义字符均无效,文本将会原样输出。

  1. 字符类型(byte和rune)

字符串中的每一个元素叫做“字符”,在遍历或者单个获取字符串元素时可以获得字符。
Go语言的字符有以下两种:

  • 一种是 uint8 类型,或者叫 byte 型,代表了 ASCII 码的一个字符。
  • 另一种是 rune 类型,代表一个 UTF-8 字符,当需要处理中文、日文或者其他复合字符时,则需要用到 rune 类型。rune 类型等价于 int32 类型。

byte 类型是 uint8 的别名,对于只占用 1 个字节的传统 ASCII 编码的字符来说,完全没有问题,例如 var ch byte = 'A',字符使用单引号括起来。

Go语言同样支持 Unicode(UTF-8),因此字符同样称为 Unicode 代码点或者 runes,并在内存中使用 int 来表示。在文档中,一般使用格式 U+hhhh来表示,其中 h 表示一个 16 进制数。在书写 Unicode 字符时,需要在 16 进制数之前加上前缀\u或者\U。因为 Unicode 至少占用 2 个字节,所以我们使用 int16 或者 int 类型来表示。如果需要使用到 4 字节,则使用\u前缀,如果需要使用到 8 个字节,则使用\U前缀。

var ch int = '\u0041'
var ch2 int = '\u03B2'
var ch3 int = '\U00101234'
fmt.Printf("%d - %d - %d\n", ch, ch2, ch3) // integer
fmt.Printf("%c - %c - %c\n", ch, ch2, ch3) // character
fmt.Printf("%X - %X - %X\n", ch, ch2, ch3) // UTF-8 bytes
fmt.Printf("%U - %U - %U", ch, ch2, ch3)   // UTF-8 code point

输出:

65 - 946 - 1053236
A - β - r
41 - 3B2 - 101234
U+0041 - U+03B2 - U+101234

格式化说明符%c用于表示字符,当和字符配合使用时,%v或%d会输出用于表示该字符的整数,%U输出格式为 U+hhhh 的字符串。
Unicode 包中内置了一些用于测试字符的函数,这些函数的返回值都是一个布尔值,如下所示(其中 ch 代表字符):

  • 判断是否为字母:unicode.IsLetter(ch)
  • 判断是否为数字:unicode.IsDigit(ch)
  • 判断是否为空白符号:unicode.IsSpace(ch)

2.1.2. 派生类型

  1. 数组类型
  2. 指针类型
  3. 结构体类型
  4. 通道(Channel)类型
  5. 函数类型
  6. 切片类型
  7. 接口类型
  8. map类型

以上这些派生数据类型将在后续章节详细介绍。

2.1.3. 数据类型转换

Go语言中只有强制类型转换,没有隐式类型转换,使用强制类型转换只有相同底层类型的变量之间可以进行相互转换(如将 int16 类型转换成 int32 类型),不同底层类型的变量相互转换时会引发编译错误(如将 bool 类型转换为 int 类型),语法格式:类型(变量)

var a float64 = 1.1
var b = int(a) // 将a强制转换为int类型,会有精度丢失,不进行四舍五入
func main() {
	fmt.Println(b)
}

注:Go中强制类型转换的语法和其他语言略有不同,其他语言进行类型转换的语法格式一般为:(类型)变量

2.2. 变量

Go语言是静态类型语言(强类型语言),因此变量(variable)是有明确类型的,要求在使用变量之前必须声明数据类型,在编译时编译器会检查变量类型的正确性。

强类型语言:强制数据类型定义的语言。也就是说,一旦一个变量被指定了某个数据类型,如果不经过强制转换,那么它就永远是这个数据类型。

2.2.1. 变量声明及其初始化

  1. 一般形式变量声明

Go语言声明变量的一般形式是使用 var 关键字:var name type;其中,var 是声明变量的关键字,name 是变量名,type 是变量的类型。如果没有在声明变量时进行初始化,则变量初始化为默认值。

注:Go语言和许多编程语言不同,它在声明变量时将变量的类型放在变量的名称之后。这样做的好处就是可以避免像C语言中那样含糊不清的声明形式,例如:int* a, b; 。其中只有 a 是指针而 b 不是。如果你想要这两个变量都是指针,则需要将它们分开书写。而在 Go 中,则可以和轻松地将它们都声明为指针类型:var a, b *int

可以在声明变量时不显式声明变量类型,而是由编译器进行自动类型判定:var name = value;编译器会根据变量值自动进行类型判定。

  1. 简短变量声明

除 var 关键字外,还可使用更加简短的变量定义和初始化语法:名字 := 表达式,需要注意的是,简短模式(short variable declaration)有以下限制:

  • 定义变量,同时显式初始化。
  • 不能提供数据类型。
  • 只能用在函数内部。
func main() {
   x := 100
   a, s := 1, "abc"
}

因为简洁和灵活的特点,简短变量声明被广泛用于大部分的局部变量的声明和初始化。var 形式的声明语句往往是用于需要显式指定变量类型地方,或者只是声明变量,变量稍后会被重新赋值而无需初始化。简短变量声明的形式在实际开发中使用的也非常普遍。

注:由于使用了:=,而不是赋值的=,因此推导声明写法的左值变量必须是没有定义过的变量。若该变量被定义过,将会发生编译错误。简短变量声明是使用变量的首选形式,使用操作符 := 可以高效地创建一个新的变量,也称之为初始化声明。

  1. 多变量声明

如果觉得每次声明变量都要使用 var 比较烦琐,可以使用 Go 提供的批量定义变量的方法:

// 一般用于声明全局变量
var (
    a int
    b string
    c []float32
    d func() bool
    e struct {
        x int
    }
)

// 声明类型相同的多个变量, 非全局变量
var name1, name2, name3 type
name1, name2, name3 = v1, v2, v3

// 和 python 很像,不需要显式声明类型,自动推断
var name1, name2, name3 = v1, v2, v3 

// 出现在 := 左侧的变量不能是已经被声明过的,否则会导致编译错误
name1, name2, name3 := v1, v2, v3 
package main

var x, y int
var (  // 这种因式分解关键字的写法一般用于声明全局变量
    a int
    b bool
)

var c, d int = 1, 2
var e, f = 123, "hello"

//这种不带声明格式的只能在函数体中出现
//g, h := 123, "hello"

func main(){
    g, h := 123, "hello"
    println(x, y, a, b, c, d, e, f, g, h)
}

2.2.2. Golang多重赋值(并行赋值)

使用传统方法进行变量值交换时,我们通常有两种方法:

var a int = 100
var b int = 200
var temp int
temp = a
a = b
b = temp
// 或者
var a int = 100
var b int = 200
a ^= b
b ^= a
a ^= b

在Go语言中,提供了多重赋值功能,变量多重赋值是指多个变量同时赋值,使用这个特性,可以轻松完成变量值交换:

var a int = 100
var b int = 200
b, a = a, b
// 四个值同样可以进行交换
a, b, c, d = b, c, a, d

多重赋值时,变量的左值和右值按从左到右的顺序赋值。多重赋值在Go语言的错误处理和函数返回值中会大量的被使用。

多重赋值的底层原理,其实是编译器在栈上创建了一个临时变量 temp 存储需要被交换的值,然后使用临时变量按顺序赋值,示例如下:

a := 1
b := 2
a, b, a = b, a, b
// 相当于
a := 1
b := 2
aTemp = a
bTemp = b
a, b, a = bTemp, aTemp, bTemp

如上面代码所示,在使用多重赋值时,赋值顺序对结果是没有影响的,其结果仍然是 a = 2, b = 1, a = 2

多重赋值经典应用:LeetCode 206. 反转链表

/*
 * @lc app=leetcode id=206 lang=golang
 *
 * [206] Reverse Linked List
 */
/**
 * Definition for singly-linked list.
 * type ListNode struct {
 *     Val int
 *     Next *ListNode
 * }
 */
func reverseList(head *ListNode) *ListNode {
    var tail *ListNode
    for head != nil {
        head.Next, tail, head = tail, head, head.Next
    }
    return tail
}

2.2.3. 匿名变量

Go语言中使用未定义的变量是不被允许的,编译时,程序会报错,有时候函数可能返回多个值,但有些值我们是不需要使用的,即进行多重赋值时,如果需要忽略某个值,可以使用 Go 语言匿名变量来处理,可以极大地增强代码的灵活性。

Go 语言匿名变量的特点是一个下画线 _ 。它可以像其他标识符那样用于变量的声明或赋值(任何类型都可以赋值给它),但任何赋给这个标识符的值都将被抛弃,因此这些值不能在后续的代码中使用,也不可以使用这个这个标识符作为变量对其它变量的进行赋值或运算。使用 Go 语言匿名变量时,只需要在变量声明的地方使用下画线替换即可。

func fun()(int, int, int){
        return 1, 2, 3
}

func main(){
        var v1, _, _ = fun()
        fmt.Println(v1)

        var _, v2, _ = fun()
        fmt.Println(v2)

        var _, _, v3 = fun()
        fmt.Println(v3)
}
//1
//2
//3

注:匿名变量不占用内存空间,不会分配内存。匿名变量与匿名变量之间也不会因为多次声明而无法使用。

2.2.4. 变量作用域

一个变量(常量、类型或函数)在程序中都有一定的作用范围,称之为作用域。
根据变量定义位置的不同,可以分为以下三个类型:

  1. 局部变量

在函数体内声明的变量称之为局部变量,它们的作用域只在函数体内,函数的参数和返回值变量都属于局部变量。局部变量不是一直存在的,它只在定义它的函数被调用后存在,函数调用结束后这个局部变量就会被销毁。

  1. 全局变量

在函数体外声明的变量称之为全局变量,全局变量只需要在一个源文件中定义,就可以在所有源文件中使用,当然,不包含这个全局变量的源文件需要使用import关键字引入全局变量所在的源文件之后才能使用这个全局变量。
全局变量声明必须以 var 关键字开头,如果想要在外部包中使用全局变量,全局变量名首字母必须大写。

  1. 形式参数

在定义函数时函数名后面括号中的变量叫做形式参数(简称形参)。形式参数只在函数调用时才会生效,函数调用结束后就会被销毁,在函数未被调用时,函数的形参并不占用实际的存储单元,也没有实际值。形式参数会作为函数的局部变量来使用。

2.2.5 变量生命周期

变量的生命周期指的是在程序运行期间变量有效存在的时间阶段。
变量的生命周期与变量的作用域有着不可分割的联系:

  • 全局变量:它的生命周期和整个程序的运行周期是一致的;
  • 局部变量:它的生命周期则是动态的,从创建这个变量的声明语句开始,到这个变量不再被引用为止;
  • 形式参数和函数返回值:它们都属于局部变量,在函数被调用的时候创建,函数调用结束后被销毁。

2.3. 常量

2.3.1. 常量声明及其初始化

Go语言中使用关键字 const 定义常量,常量的值在初始化后就不会改变,在编译时被创建,常量类型只能是布尔型、数字型(整数型、浮点型和复数)和字符串型。由于编译时的限制,定义常量的表达式必须在编译时就能求得确定值。常量定义与变量定义类似:const name [type] = value

  • 正确:const a = 1/2
  • 错误:const b = getNumber()

2.3.2. iota常量生成器

iota,特殊常量值,是一个系统定义的可以被编译器修改的常量值,使用iota能简化枚举定义。

iota只能被用在常量的赋值中,在每一个const关键字出现时,被重置为0,然后每出现一个常量,iota所代表的数值会自动增加1,iota可以理解成常量组中常量的计数器,不论该常量的值是什么,只要有一个常量,那么iota 就加1。

示例:定义一个 Weekday 整数类型,然后为一周的每天定义了一个常量,从周日 0 开始。在其它编程语言中,这种类型一般被称为枚举类型。

type Weekday int
const (
    Sunday Weekday = iota // 周日将对应 0,周一为 1,以此类推
    Monday
    Tuesday
    Wednesday
    Thursday
    Friday
    Saturday
)