go语言学习笔记
#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
3var n int
// 无类型的数字型常量 “5” 它的类型在这里变成了 int
f(n + 5)len()
正确的做法:`const c1 = 2/3`
错误的做法:`const c2 = getNumber() `// 引发构建错误: `getNumber() used as value`
数字型的常量是没有大小和符号的,并且可以使用任何精度而不会导致溢出。不过需要注意的是,当常量赋值给一个精度过小的数字型变量时,可能会因为无法正确表达常量所代表的数值而导致溢出,这会在编译期间就引发错误。
Go 中不允许不同类型之间的混合使用,但是对于常量的类型限制非常少,因此允许常量之间的混合使用
1 | const Ln2 = 0.693147180559945309417232121458176568075500134360 |
常量也允许使用并行赋值的形式:
1 | const beef, two, c = "eat", 2, "veg" |
常量还可以用作枚举:
1 | const ( |
###3、变量
声明变量的一般形式是使用var
关键字:var identifier type
1 | var a int |
也可以改写成这种形式:
1 | var ( |
当一个变量被声明之后,系统自动赋予它该类型的零值:int 为 0,float 为 0.0,bool 为 false,string 为空字符串,指针为 nil。
作用域:
* 一个变量(常量、类型或函数)在程序中都有一定的作用范围,称之为作用域。如果一个变量在函数体外声明,则被认为是全局变量,可以在整个包甚至外部包(被导出后)使用,不管你声明在哪个源文件里或在哪个源文件里调用该变量。
* 在函数体内声明的变量称之为局部变量,它们的作用域只在函数体内,参数和返回值变量也是局部变量。
* if 和 for 这些结构中声明的变量的作用域只在相应的代码块内。
尽管变量的标识符必须是唯一的,但你可以在某个代码块的内层代码块中使用相同名称的变量,则此时外部的同名变量将会暂时隐藏(结束内部代码块的执行后隐藏的外部同名变量又会出现,而内部同名变量则被释放),你任何的操作都只会影响内部代码块的局部变量。
声明与赋值(初始化)语句也可以组合起来:
1 | var identifier [type] = value |
未定义类型的常量会在必要时刻根据上下文来获得相关类型:
1 | var a = 15 |
或
1 | var ( |
不过自动推断类型并不是任何时候都适用的,当你想要给变量的类型并不是自动推断出的某种类型时,你还是需要显式指定变量的类型,
例如:var n int64 = 2
注意:var a
这种语法是不正确的,因为编译器没有任何可以用于自动推断类型的依据
变量的类型也可以在运行时实现自动推断,例如:
1 | var ( |
这种写法主要用于声明包级别的全局变量
,当你在函数体内声明局部变量
时,应使用简短声明语法 :=
,例如:
1 | a := 1 |
局部变量声明却没有在相同的代码块中使用它,会得到编译错误 `a declared and not used`。
定义变量 a 之前使用它,则会得到编译错误 `undefined: a`。
全局变量是允许声明但不使用。
空白标识符 _
也被用于抛弃值,如值 5 在:_, b = 5, 7
中被抛弃。_
实际上是一个只写变量,你不能得到它的值。
同一类型的多个变量可以声明在同一行,多变量可以在同一行进行赋值,如:
1 | var a, b, c int |
或者
1 | a, b, c := 5, 7, "abc" |
右边的这些值以相同的顺序赋值给左边的变量,所以 a 的值是 5, b 的值是 7,c 的值是 “abc”。这被称为 并行 或 同时 赋值
。
####init 函数
变量除了可以在全局声明中初始化,也可以在 init
函数中初始化。这是一类非常特殊的函数,它不能够被人为调用,而是在每个包完成初始化后自动执行,并且执行优先级比 main
函数高。
每个源文件都只能包含一个 init
函数。初始化总是以单线程执行,并且按照包的依赖关系顺序执行。
###4、基本类型和运算符Go
是强类型语言,不会进行隐式转换,任何不同类型之间的转换都必须显式说明
。Go
不存在像 C
那样的运算符重载,表达式的解析顺序是从左至右
。
####布尔类型 boolvar b bool = true
布尔型的值只可以是常量 true 或者 false。
两个类型相同的值
可以使用 与 &&
、或 ||
、非 !
、相等 ==
或者 不等 !=
、<
、<=
、>
、>=
运算符来进行比较并获得一个布尔型的值。
####整型 int 和浮点型 float
Go 也有基于架构的类型,例如:int
、uint
和 uintptr
。
这些类型的长度都是根据运行程序所在的操作系统类型所决定的:
* int
和 uint
在 32 位操作系统上,它们均使用 32 位(4 个字节),在 64 位操作系统上,它们均使用 64 位(8 个字节)。
* uintptr
的长度被设定为足够存放一个指针即可。
Go 语言中没有 float
类型。(Go语言中只有 float32
和 float64
)没有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 | aint32 = int32(aFloat32) 从取值范围较大的类型转换为取值范围较小的类型时转换时,小数点后的数字将被丢弃 |
#####复数
Go 拥有以下复数类型:
1 | complex64 (32 位实数和虚数) |
复数使用 re+imI
来表示,其中 re
代表实数部分,im
代表虚数部分,I
代表根号负 1。
示例:
1 | var c1 complex64 = 5 + 10i |
如果 re
和 im
的类型均为 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]
。
字符串拼接符+
。两个字符串s1
和s2
可以通过s := s1 + s2
拼接在一起。s2
追加在s1
尾部并生成一个新的字符串s
。
拼接的简写形式 +=
也可以用于字符串:
1 | s := "hel" + "lo," |
在循环中使用加号 +
拼接字符串并不是最高效的做法,更好的办法是使用函数 strings.Join()
,有没有更好的办法了?有!使用字节缓冲bytes.Buffer
拼接更加给力。
####指针
Go 语言为程序员提供了控制数据结构的指针的能力;但是,你不能进行指针运算。
程序在内存中存储它的值,每个内存块(或字)有一个地址,通常用十六进制数表示,如:0x6b0820
或 0xf84001d7f0
。
Go 语言的取地址符是 &
,放到一个变量前使用就会返回相应变量的内存地址。在指针类型前面加上 *
号(前缀)来获取指针所指向的内容。
地址可以存储在一个叫做指针的特殊数据类型中,可以这样声明它:
1 | var p *type |
一个指针变量可以指向任何一个值的内存地址 它指向那个值的内存地址,在 32 位机器上占用 4 个字节,在 64 位机器上占用 8 个字节,并且与它所指向的值的大小无关。
当一个指针被定义后没有分配到任何变量时,它的值为 nil
。
###5、控制结构
####结构和分支结构
* if-else
结构
* switch
结构
* select
结构,用于 channel
的选择
可以使用迭代或循环结构来重复执行一次或多次某段代码(任务):
* for (range)
结构
一些如 break
和 continue
这样的关键字可以用于中途改变循环的状态。
此外,你还可以使用 return
来结束某个函数的执行,或使用 goto
和标签来调整程序的执行位置。
####if-else
结构if
是用于测试某个条件(布尔型或逻辑型)的语句,如果该条件成立,则会执行 if
后由大括号括起来的代码块,否则就忽略该代码块继续执行后续的代码。
1 | if condition { |
如果存在第二个分支,则可以在上面代码的基础上添加 else
关键字以及另一代码块,这个代码块中的代码只有在条件不满足时才会执行。if
和 else
后的两个代码块是相互独立的分支,只可能执行其中一个。
1 | if condition { |
如果存在第三个分支,则可以使用下面这种三个独立分支的形式:
1 | if condition1 { |
关键字 if
和 else
之后的左大括号 {
必须和关键字在同一行,如果你使用了 else-if
结构,则前段代码块的右大括号 }
必须和 else-if
关键字在同一行。这两条规则都是被编译器强制规定的。
####switch 结构
switch 结构接受任意形式的表达式:
1 | switch var1 { |
变量 var1
可以是任何类型,而 val1
和 val2
则可以是同类型的任意值。类型不被局限于常量或整数,但必须是相同的类型;或者最终结果为相同类型的表达式。前花括号 {
必须和 switch
关键字在同一行。
您可以同时测试多个可能符合条件的值,使用逗号分割它们,例如:case val1, val2, val3
。
每一个 case
分支都是唯一的,从上至下逐一测试,直到匹配为止。
一旦成功地匹配到某个分支,在执行完相应代码后就会退出整个 switch
代码块,也就是说您不需要特别使用 break
语句来表示结束。如果在执行完每个分支的代码后,还希望继续执行后续分支的代码,可以使用 fallthrough
关键字来达到目的。
可选的 default
分支可以出现在任何顺序,但最好将它放在最后。
####for 结构
基于计数器的迭代
基于计数器的迭代,基本形式为:
1 | for 初始化语句; 条件语句; 修饰语句 {} |
三部分组成的循环的头部,它们之间使用分号;
相隔,但并不需要括号 ()
将它们括起来。
例:
1 | package main |
还可以在循环中同时使用多个计数器:
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 与 continuebreak
的作用范围为该语句出现后的最内部的结构,它可以被用于任何形式的 for
循环(计数器、条件判断等)。但在 switch
或 select
语句中,break
语句的作用结果是跳过整个代码块,执行后续的代码。
关键字 continue
忽略剩余的循环体而直接进入下一次循环的过程,但不是无条件执行下一次循环,执行之前依旧需要满足循环的判断条件。且关键字 continue
只能被用于 for
循环中。
标签与 gotofor
、switch
或 select
语句都可以配合标签(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 里面函数重载是不被允许的。
函数不能在其它函数里面声明(不能嵌套),不过我们可以通过使用匿名函数
来破除这个限制。
任何一个有返回值(单个或多个)的函数都必须以 return
或 panic
结尾。