go语言学习笔记

go语言学习笔记

五月 23, 2022

#go语言学习笔记
###1、go语言代码规范
####常用命令:
运行 go run file
编译 go build file

只有当某个函数需要被外部包调用的时候才使用大写字母开头,并遵循 Pascal 命名法;否则就遵循骆驼命名法,即第一个单词的首字母小写,其余单词的首字母大写。

####格式化输出:

类型 说明
%d int变量
%x,%o,%b 十六进制、八进制、二进制的int变量
%f,%g,%e 单精度浮点型、双精度浮点型、科学计数法表示
%t 布尔变量,true或false
%c rune(unicode标点),Go语言特有的unicode字符类型
%s 字符串
%q 带双引号的字符串或单引号的rune
%v 会将任意变量以易读的形式输出
%T 打印变量类型
%% 字符型百分比类型(%字符本身,类似C中的\转义)
%p 指针的格式化标识符

###2、常量
常量使用关键字 const 定义,用于存储不会改变的数据。
存储在常量中的数据类型只可以是布尔型、数字型(整数型、浮点型和复数)和字符串型。
常量的定义格式:const identifier [type] = value
例如

1
const Pi float = 3.14159

在 Go 语言中,你可以省略类型说明符 [type],因为编译器可以根据变量的值来推断其类型。

  • 显式类型定义: `const b string = "abc"`
    
    • 隐式类型定义: `const b = "abc"`
      
      未定义类型的常量会在必要时刻根据上下文来获得相关类型。
      1
      2
      3
      var n int
      // 无类型的数字型常量 “5” 它的类型在这里变成了 int
      f(n + 5)
      常量的值必须是能够在编译时就能够确定的。因为在编译期间自定义函数均属于未知,因此无法用于常量的赋值,但内置函数可以使用,如:len()
      • 正确的做法:`const c1 = 2/3`
        
        • 错误的做法:`const c2 = getNumber() `// 引发构建错误: `getNumber() used as value`
          

数字型的常量是没有大小和符号的,并且可以使用任何精度而不会导致溢出。不过需要注意的是,当常量赋值给一个精度过小的数字型变量时,可能会因为无法正确表达常量所代表的数值而导致溢出,这会在编译期间就引发错误。

Go 中不允许不同类型之间的混合使用,但是对于常量的类型限制非常少,因此允许常量之间的混合使用

1
2
3
4
const Ln2 = 0.693147180559945309417232121458176568075500134360
const Log2E = 1/Ln2 // this is a precise reciprocal
const Billion = 1e9 // float constant
const hardEight = (1 << 100) >> 97

常量也允许使用并行赋值的形式:

1
2
3
4
5
6
const beef, two, c = "eat", 2, "veg"
const Monday, Tuesday, Wednesday, Thursday, Friday, Saturday = 1, 2, 3, 4, 5, 6
const (
Monday, Tuesday, Wednesday = 1, 2, 3
Thursday, Friday, Saturday = 4, 5, 6
)

常量还可以用作枚举:

1
2
3
4
5
const (
Unknown = 0
Female = 1
Male = 2
)

###3、变量
声明变量的一般形式是使用var关键字:var identifier type

1
2
3
var a int
var b bool
var str string

也可以改写成这种形式:

1
2
3
4
5
var (
a int
b bool
str string
)

当一个变量被声明之后,系统自动赋予它该类型的零值:int 为 0,float 为 0.0,bool 为 false,string 为空字符串,指针为 nil。
作用域:
* 一个变量(常量、类型或函数)在程序中都有一定的作用范围,称之为作用域。如果一个变量在函数体外声明,则被认为是全局变量,可以在整个包甚至外部包(被导出后)使用,不管你声明在哪个源文件里或在哪个源文件里调用该变量。
* 在函数体内声明的变量称之为局部变量,它们的作用域只在函数体内,参数和返回值变量也是局部变量。
* if 和 for 这些结构中声明的变量的作用域只在相应的代码块内。

尽管变量的标识符必须是唯一的,但你可以在某个代码块的内层代码块中使用相同名称的变量,则此时外部的同名变量将会暂时隐藏(结束内部代码块的执行后隐藏的外部同名变量又会出现,而内部同名变量则被释放),你任何的操作都只会影响内部代码块的局部变量。

声明与赋值(初始化)语句也可以组合起来:

1
2
3
4
5
var identifier [type] = value
var a int = 15
var i = 5
var b bool = false
var str string = "Go says hello to the world!"

未定义类型的常量会在必要时刻根据上下文来获得相关类型:

1
2
3
var a = 15
var b = false
var str = "Go says hello to the world!"

1
2
3
4
5
6
7
var (
a = 15
b = false
str = "Go says hello to the world!"
numShips = 50
city string
)

不过自动推断类型并不是任何时候都适用的,当你想要给变量的类型并不是自动推断出的某种类型时,你还是需要显式指定变量的类型,
例如:
var n int64 = 2
注意:var a 这种语法是不正确的,因为编译器没有任何可以用于自动推断类型的依据
变量的类型也可以在运行时实现自动推断,例如:

1
2
3
4
5
var (
HOME = os.Getenv("HOME")
USER = os.Getenv("USER")
GOROOT = os.Getenv("GOROOT")
)

这种写法主要用于声明包级别的全局变量,当你在函数体内声明局部变量时,应使用简短声明语法 :=,例如:

1
a := 1
  • 局部变量声明却没有在相同的代码块中使用它,会得到编译错误 `a declared and not used`。
    
    • 定义变量 a 之前使用它,则会得到编译错误 `undefined: a`。
      
      • 全局变量是允许声明但不使用。
        

空白标识符 _ 也被用于抛弃值,如值 5 在:_, b = 5, 7 中被抛弃。_ 实际上是一个只写变量,你不能得到它的值。

同一类型的多个变量可以声明在同一行,多变量可以在同一行进行赋值,如:

1
2
var a, b, c int
a, b, c = 5, 7, "abc"

或者

1
a, b, c := 5, 7, "abc"

右边的这些值以相同的顺序赋值给左边的变量,所以 a 的值是 5, b 的值是 7,c 的值是 “abc”。这被称为 并行 或 同时 赋值

####init 函数
变量除了可以在全局声明中初始化,也可以在 init 函数中初始化。这是一类非常特殊的函数,它不能够被人为调用,而是在每个包完成初始化后自动执行,并且执行优先级比 main 函数高。

每个源文件都只能包含一个 init 函数。初始化总是以单线程执行,并且按照包的依赖关系顺序执行。

###4、基本类型和运算符
Go 是强类型语言,不会进行隐式转换,任何不同类型之间的转换都必须显式说明Go 不存在像 C 那样的运算符重载,表达式的解析顺序是从左至右

####布尔类型 bool
var b bool = true 布尔型的值只可以是常量 true 或者 false。

两个类型相同的值可以使用 与 &&或 ||非 !相等 == 或者 不等 != <<=>>= 运算符来进行比较并获得一个布尔型的值。

####整型 int 和浮点型 float
Go 也有基于架构的类型,例如:intuintuintptr

这些类型的长度都是根据运行程序所在的操作系统类型所决定的:
* intuint 在 32 位操作系统上,它们均使用 32 位(4 个字节),在 64 位操作系统上,它们均使用 64 位(8 个字节)。
* uintptr 的长度被设定为足够存放一个指针即可。

Go 语言中没有 float 类型。(Go语言中只有 float32float64)没有double类型。

与操作系统架构无关的类型都有固定的大小,并在类型的名称中就可以看出来:
#####整数:
* int8(-128 -> 127)
* int16(-32768 -> 32767)
* int32(-2,147,483,648 -> 2,147,483,647)
* int64(-9,223,372,036,854,775,808 -> 9,223,372,036,854,775,807)

#####无符号整数:
* uint8(0 -> 255)
* uint16(0 -> 65,535)
* uint32(0 -> 4,294,967,295)
* uint64(0 -> 18,446,744,073,709,551,615)

#####浮点型(IEEE-754 标准):
* float32(+- 1e-45 -> +- 3.4 * 1e38)
* float64(+- 5 1e-324 -> 107 1e308)

float32 精确到小数点后 7 位float64 精确到小数点后 15 位。应该尽可能地使用 float64,因为 math 包中所有有关数学运算的函数都会要求接收这个类型。

int 型是计算最快的一种类型。整型的零值为 0,浮点型的零值为 0.0。

可以通过增加前缀 0 来表示 8 进制数(如:077),增加前缀 0x 来表示 16 进制数(如:0xFF),以及使用 e 来表示 10 的连乘(如: 1e3 = 1000,或者 6.022e23 = 6.022 x 1e23)。

类型转换 type(identifier)

1
2
aint32 = int32(aFloat32) 从取值范围较大的类型转换为取值范围较小的类型时转换时,小数点后的数字将被丢弃
a := uint64(0) 来同时完成类型转换和赋值操作

#####复数
Go 拥有以下复数类型:

1
2
complex64 (32 位实数和虚数)
complex128 (64 位实数和虚数)

复数使用 re+imI 来表示,其中 re 代表实数部分,im 代表虚数部分,I 代表根号负 1。
示例:

1
2
3
var c1 complex64 = 5 + 10i
fmt.Printf("The value is: %v", c1)
// 输出: 5 + 10i

如果 reim 的类型均为 float32,那么类型为 complex64 的复数 c 可以通过以下方式来获得:

1
c = complex(re, im)

函数 real(c)imag(c) 可以分别获得相应的实数和虚数部分。

如果你对内存的要求不是特别高,最好使用 complex128 作为计算类型,因为相关函数都使用这个类型的参数。

#####运算符与优先级
有些运算符拥有较高的优先级,二元运算符的运算方向均是从左至右。下表列出了所有运算符以及它们的优先级,由上至下代表优先级由高到低:

优先级 运算符
7 ^ !
6 * / % << >> & &^
5 + - | ^
4 == != < <= >= >
3 <-
2 &&
1 ||

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

在书写 Unicode 字符时,需要在 16 进制数之前加上前缀 \u 或者 \U
因为 Unicode 至少占用 2 个字节,所以我们使用 int16 或者 int 类型来表示。如果需要使用到 4 字节,则会加上 \U 前缀;前缀 \u 则总是紧跟着长度为 4 的 16 进制数,前缀 \U 紧跟着长度为 8 的 16 进制数。

####bool字符串

字符串是 UTF-8 字符的一个序列。字符串是一种值类型,且值不可变,即创建某个文本后你无法再次修改这个文本的内容;更深入地讲,字符串是字节的定长数组。

Go 支持以下 2 种形式的字面值:
* 解释字符串:该类字符串使用双引号括起来,其中的相关的转义字符将被替换,这些转义字符包括:
* \n:换行符
* \r:回车符
* \t:tab 键
* \u\U:Unicode 字符
* \\:反斜杠自身
* 非解释字符串:该类字符串使用反引号括起来,支持换行,例如:

1
`This is a raw string \n` 中的 `\n\` 会被原样输出。

字符串的内容(纯字节)可以通过标准索引法来获取,在中括号 [] 内写入索引,索引从 0 开始计数:
* 字符串 str 的第 1 个字节:str[0]
* 第 i 个字节:str[i - 1]
* 最后 1 个字节:str[len(str)-1]

需要注意的是,这种转换方案只对纯 ASCII 码的字符串有效。

注意事项:获取字符串中某个字节的地址的行为是非法的,例如:&str[i]
字符串拼接符 +。两个字符串 s1s2 可以通过 s := s1 + s2 拼接在一起。s2 追加在 s1 尾部并生成一个新的字符串 s

拼接的简写形式 += 也可以用于字符串:

1
2
3
s := "hel" + "lo,"
s += "world!"
fmt.Println(s) //输出 “hello, world!”

在循环中使用加号 + 拼接字符串并不是最高效的做法,更好的办法是使用函数 strings.Join(),有没有更好的办法了?有!使用字节缓冲bytes.Buffer拼接更加给力。

####指针
Go 语言为程序员提供了控制数据结构的指针的能力;但是,你不能进行指针运算。
程序在内存中存储它的值,每个内存块(或字)有一个地址,通常用十六进制数表示,如:0x6b08200xf84001d7f0

Go 语言的取地址符是 &,放到一个变量前使用就会返回相应变量的内存地址。在指针类型前面加上 * 号(前缀)来获取指针所指向的内容。
地址可以存储在一个叫做指针的特殊数据类型中,可以这样声明它:

1
2
3
4
var p *type
例:
var intP *int
然后使用 intP = &i1 是合法的,此时 intP 指向 i1。

一个指针变量可以指向任何一个值的内存地址 它指向那个值的内存地址,在 32 位机器上占用 4 个字节,在 64 位机器上占用 8 个字节,并且与它所指向的值的大小无关。

当一个指针被定义后没有分配到任何变量时,它的值为 nil

###5、控制结构
####结构和分支结构
* if-else 结构
* switch 结构
* select 结构,用于 channel 的选择

可以使用迭代或循环结构来重复执行一次或多次某段代码(任务):
* for (range) 结构

一些如 breakcontinue 这样的关键字可以用于中途改变循环的状态。

此外,你还可以使用 return 来结束某个函数的执行,或使用 goto 和标签来调整程序的执行位置。

####if-else 结构
if 是用于测试某个条件(布尔型或逻辑型)的语句,如果该条件成立,则会执行 if 后由大括号括起来的代码块,否则就忽略该代码块继续执行后续的代码。

1
2
3
if condition {
// do something
}

如果存在第二个分支,则可以在上面代码的基础上添加 else 关键字以及另一代码块,这个代码块中的代码只有在条件不满足时才会执行。if else 后的两个代码块是相互独立的分支,只可能执行其中一个。

1
2
3
4
5
if condition {
// do something
} else {
// do something
}

如果存在第三个分支,则可以使用下面这种三个独立分支的形式:

1
2
3
4
5
6
7
if condition1 {
// do something
} else if condition2 {
// do something else
} else {
// catch-all or default
}

关键字 ifelse 之后的左大括号 { 必须和关键字在同一行,如果你使用了 else-if 结构,则前段代码块的右大括号 } 必须和 else-if 关键字在同一行。这两条规则都是被编译器强制规定的。

####switch 结构
switch 结构接受任意形式的表达式:

1
2
3
4
5
6
7
8
switch var1 {
case val1:
...
case val2:
...
default:
...
}

变量 var1 可以是任何类型,而 val1 val2 则可以是同类型的任意值。类型不被局限于常量或整数,但必须是相同的类型;或者最终结果为相同类型的表达式。前花括号 { 必须和 switch 关键字在同一行。

您可以同时测试多个可能符合条件的值,使用逗号分割它们,例如:case val1, val2, val3

每一个 case 分支都是唯一的,从上至下逐一测试,直到匹配为止。

一旦成功地匹配到某个分支,在执行完相应代码后就会退出整个 switch 代码块,也就是说您不需要特别使用 break 语句来表示结束。如果在执行完每个分支的代码后,还希望继续执行后续分支的代码,可以使用 fallthrough 关键字来达到目的。

可选的 default 分支可以出现在任何顺序,但最好将它放在最后。

####for 结构
基于计数器的迭代

基于计数器的迭代,基本形式为:

1
for 初始化语句; 条件语句; 修饰语句 {}

三部分组成的循环的头部,它们之间使用分号;相隔,但并不需要括号 () 将它们括起来。
例:

1
2
3
4
5
6
7
package main
import "fmt"
func main() {
for i := 0; i < 5; i++ {
fmt.Printf("This is the %d iteration\n", i)
}
}

还可以在循环中同时使用多个计数器:

1
for i, j := 0, N; i < j; i, j = i+1, j-1 {}

无限循环

条件语句是可以被省略的,如 i:=0; ; i++for { }for ;; { }(;; 会在使用 gofmt 时被移除):这些循环的本质就是无限循环。最后一个形式也可以被改写为 for true { },但一般情况下都会直接写 for { }

for-range 结构

可以迭代任何一个集合(包括数组和 map)一般形式为:for ix, val := range coll { }

要注意的是,val 始终为集合中对应索引的值拷贝,因此它一般只具有只读性质,对它所做的任何修改都不会影响到集合中原有的值(如果 val 为指针,则会产生指针的拷贝,依旧可以修改集合中的原值)。

Break 与 continue
break 的作用范围为该语句出现后的最内部的结构,它可以被用于任何形式的 for 循环(计数器、条件判断等)。但在 switchselect 语句中,break 语句的作用结果是跳过整个代码块,执行后续的代码。

关键字 continue 忽略剩余的循环体而直接进入下一次循环的过程,但不是无条件执行下一次循环,执行之前依旧需要满足循环的判断条件。且关键字 continue 只能被用于 for 循环中。

标签与 goto
forswitchselect 语句都可以配合标签(label)形式的标识符使用,即某一行第一个以冒号(:)结尾的单词(gofmt 会将后续代码自动移至下一行)。

标签的名称是大小写敏感的,为了提升可读性,一般建议使用全部大写字母。

定义但未使用标签会导致编译错误:label … defined and not used

如果您必须使用 goto,应当只使用正序的标签(标签位于 goto 语句之后),但注意标签和 goto 语句之间不能出现定义新变量的语句,否则会导致编译失败。

###6、函数(function)
Go是编译型语言,所以函数编写的顺序是无关紧要的;鉴于可读性的需求,最好把 main() 函数写在文件的前面,其他函数按照一定逻辑顺序进行编写(例如函数被调用的顺序)。

当函数执行到代码块最后一行(} 之前)或者 return 语句的时候会退出,其中 return 语句可以带有零个或多个参数;这些参数将作为返回值供调用者使用。简单的 return 语句也可以用来结束 for 死循环,或者结束一个协程goroutine

Go 里面有三种类型的函数:
* 普通的带有名字的函数
* 匿名函数或者lambda函数
* 方法

除了main()init()函数外,其它所有类型的函数都可以有参数与返回值。函数参数、返回值以及它们的类型被统称为函数签名

函数重载(function overloading)指的是可以编写多个同名函数,只要它们拥有不同的形参与/或者不同的返回值,在 Go 里面函数重载是不被允许的。

函数不能在其它函数里面声明(不能嵌套),不过我们可以通过使用匿名函数来破除这个限制。

任何一个有返回值(单个或多个)的函数都必须以 returnpanic 结尾。