Go(1)之基本使用
Author:Once Day Date:2023年1月8日
漫漫长路,有人对你微笑过嘛…
参考文档:
1.概述
Go语言由来自google的一群大佬编写,即Robert Griesemer、Rob Pike、Ken Thompson。
Go语言适合构建基础设施类软件,如网络服务器、程序员使用的工具和系统等。
Go是一个开源项目,所以其编译器、库和工具的源代码是人人即可免费取得的。
Go语言是类C语言,继承了表达式语法、控制流语句、基本数据类型、按值调用的形参传递和指针。
Go语言收到了CSP(通信顺序进程,Communicating Sequential Process)的启发。
Go语言的核心特点就是简单性,有很多基础的特性,如垃圾回收、包系统、一等公民函数、词法作用域、系统调用接口等,但重载、泛型、类等复杂特性,就不太会增加。
安装Go有很多方法,在Linux下安装很简单(root权限):
apt install golang-go
其他平台安装请参考:
安装完成后, 在命令行输入一下命令查看:
go version //查看版本
go help //查看帮助
使用的go版本最好在1.5
以上:
onceday->~:# go version
go version go1.18.1 linux/amd64
2. 基础使用
开发环境使用腾讯云Linux服务器,使用vscode远程SSH连接开发(具体过程可自行百度)。
vscode需要安装go
支持插件,插件市场直接搜索即可,可能会提示要安装配套支持包,同意即可。
可参考以下文档:
需要配置代理:
go env -w GO111MODULE=on
go env -w GOPROXY=https://goproxy.cn,direct
注意,如果vscode的go插件一直提示要安装配套支持包,那么可能是上面代理配置不成功,需重新配置代理,然后重启vscode,再试一试。
首先来输出一个经典的hello world
:
package main
import "fmt"
func main() {
fmt.Println("hello, world!")
}
然后在命令运行:
ubuntu->go-code:$ go run helloworld.go
hello, world!
Go原生支持Unicode
,因此可以处理所有国家的语言。
可以再进一步把它编译成二进制程序:
ubuntu->go-code:$ go build helloworld.go
ubuntu->go-code:$ ll
total 1732
drwxrwxr-x 2 ubuntu ubuntu 4096 Jan 8 23:31 ./
drwxr-x--- 11 ubuntu ubuntu 4096 Jan 8 23:08 ../
-rwxrwxr-x 1 ubuntu ubuntu 1758417 Jan 8 23:42 helloworld*
-rw-rw-r-- 1 ubuntu ubuntu 74 Jan 8 23:29 helloworld.go
用file可以查看该文件的具体信息:
ubuntu->go-code:$ file helloworld
helloworld: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), statically linked, Go BuildID=z9i3RzrVufV2SEsr681w/mhtdOnbVF3mshdHAs_fP/rfCmQ8VmCzmj-XWabnsZ/mBuD7qqCOWorc7GsomZ5, not stripped
gobuild
,这就表明其是由Go语言构建的可执行程序。
3. 变量和声明
Go程序的基础组成有以下几个部分:包声明,引入包,函数,变量,语句/表达式,注释。
除注释外,各类命名都遵守一个简单的规则:名称的开头是一个字母(Unicode中的字符即可)或下划线,后面可以跟任意数量的字符、数字和下划线,并区分大小写。
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
还有三十多个内置的预声明的常量、类型和函数:
常量:true、false、iota、nil
类型:int、int8、int16、int32、int64、uint、uint8、uint16、uint32、uint64、uintptr、float32、float64、complex128、complex64、bool、byte、rune、string、error
函数:make、len、cap、new、append、copy、close、delete、complex、real、imag、panic、recover
预声明的名称是可以覆盖的。但是这样可能会产生冲突的风险。
对于局部作用域的变量,Go语言习惯使用短命令。
对于单词组合的名称,Go语言使用驼峰式的风格,不太采用下划线的方式。
3.1 声明和作用域范围
如果一个实体在函数中声明,它只在函数局部有效,如果声明在函数外,将对包里面所有的源文件都可见。
实体的第一个字母的大小写决定其可见性是否跨包。如果名称以大写字母开头,它是导出的,意味着对包外是可见和可访问的。包名总是由小写字母组成。
GO程序存储在一个或多个以.go为后缀的文件里,每一个文件以package声明开头,表明文件属于哪个包。
GO语言的类型、变量、常量、函数等声明是不分先后的,因此在源文件中都是全局可见。
局部声明仅仅是在声明所在的函数内可见,并且可能对于函数中的一小块区域可见。
函数的声明包含一个名字、一个参数列表(由函数的调用者提供的变量)、一个可选的返回值列表,以及函数体。如果函数不返回任何内容,返回值列表可以忽略。
func main () {
var a = 1
return a
}
3.2 变量声明
var声明创建一个具体类型的变量,然后给它附加一个名字,设置它的初始值。通用形式如下:
var name type = expression
类型和表达式只能省略一个,不能都省略,变量类型将由两者指定。
未赋值的变量都默认初始值为零值:
- 数字类型为0。
- 布尔类型为false。
- 字符串为“”。
- 接口和引用类型(slice、指针、map、通道、函数)等为nil。
- 复合数据结构,零值是所有成员的零值。
注意,Go里面不存在未初始化变量。
包级别变量在main开始之前初始化,局部变量初始化和声明过程在函数执行期间执行。
可以采用短变量声明形式来声明和初始化局部变量。
name := expression
多个变量可以以短变量声明的方式来声明和初始化:
i, j := 0, 1
需要和下面的多重赋值区分开:
i, j = j, i
下面是一个短变量声明的例子:
f, err := os.Open(name)
短变量声明不需要声明所有在左边的变量,如果某个变量已声明,那么对于此变量,段声明等同于赋值。
in, err := os.Open(in_name)
out, err := os.Open(out_name)
in, err := os.Open(in_name2)
如上所示三行代码,第三个将出现错误,无法编译通过,因为短变量声明至少需要声明一个变量。
注意,只有同一个词法块中的变量才会生效,外层的声明将被忽略。
3.3 指针
指针的值是一个变量的地址,一个指针指示值所保存的位置。所有的变量都有地址,但并不是所有的值都有地址。如下:
x := 1
p := &x // p 是整型指针,指向x
使用指针如下:
fmt.Println(*p)
指针可以比较,且只在指向同一个变量或两者都是nil下才安全。
与C语言不同的是,函数返回局部变量的地址是安全的,但每次调用返回的地址都不一样。
func f() *int {
v := 1
return &v
}
对于上面的函数,每次调用返回一个新的地址。
Go语言具有垃圾回收功能,因此会自动标记变量的使用情况,每次使用变量的地址或者复制一个指针,就创建了一个新的标记。
3.4 new函数
表达式new(type)创建一个未命名的type类型的变量,初始化为T类型的零值,并返回其地址(地址类型为*T)。
p := new(int)
fmt.Println(*p)
对于一些特殊的类型,如struct {}
,new函数会返回相同的地址。
new函数是一个预声明的函数,并非关键字,因此可以被覆盖声明。
3.5 变量声明周期
包级别变量的生命周期是整个程序的执行时间。局部变量拥有一个动态的生命周期:每次执行声明语句时创建一个新的实体,变量一直生存到它变得不可访问,这时占用的空间会被回收。
如果局部变量使用指针拓展了作用域,那么就可能从栈变量变成堆变量,会带来额外的内存负担。
对于长生命周期的变量,如果内部保持了短生命周期的变量,就会组织垃圾回收器回收短生命周期的对象空间。
3.6 赋值
除了基础的=
赋值外,也只是组合赋值符,如*=
,+=
等。也包括v++
,v--
等自增和自减操作。
多重赋值允许几个变量一次性赋值:
x, y = y, x
如上面的表达式交换了x
和y
之间的值。
一般多重赋值用于接收返回多个值的函数,如下:
f, err = os.Open("foo.txt")
如果两个值能进行比较,那么它们之间是需要可以赋值的。
3.7 类型声明
可以使用type声明定义一个新的命名类型,和某个已有的类型使用同样的底层类型。
type name underlying-type
命名类型的底层类型决定了它的结构和表达式,以及支持的内部操作集合,这些内部操作与直接使用底层类型的情况相同。
不同命名类型的值不能直接比较。
3.8 包和文件
每一个包给它的声明提供独立的命名空间。Go里面包对外可见的标识符以大写字母开头。
在Go程序里,每一个包通过称为导入路径(import path)的唯一字符串来标识。
import (
"fmt"
"os"
"./tool/foo"
)
对于上面的导入路径,每个包也有一个包名。包名以短名字的方式出现在包的声明中,其按约定匹配导入路径的最后一段。
导入的包如果没有使用,那么触发错误,因此需要确保导入是实际使用的。
包的初始化会按照一定的次序执行,首先是包级别的变量,这些变量会按照依赖和声明顺序来初始化。
一个含有多个go文件的包,go工具会按照内部排序的结果来初始化,如果一些变量需要复杂初始化过程,那么可以使用init
函数。
func init() {/****/}
init
函数不能被调用和被引用,在每一个文件里,当程序启动时,init函数会按照它们声明的顺序自动执行。
在main函数执行前,所有包会初始化完全。
3.9 作用域
声明的作用域和声明周期不一样,一般声明的作用域是词法块,最显式的就是大括号围起来的代码段。
一个声明的词法块决定声明的作用域大小。
- 在全局块中声明的变量,对整个程序可见。
- 在包级别声明的变量,可以在同一个包里的任何文件引用。
- 导入的包是文件级别,可以在同一个文件里引用。
- 局部声明仅在对应的大括号中使用。
编译器遇到一个引用名字时,会从最内层的封闭词法块寻找其声明,未找到会提示undeclared name
,注意,内层声明会覆盖外层声明。
当在if/for/switch
等控制语句中使用短声明时,需要格外注意变量的作用域,避免报错和覆盖。
if/for
的控制体和执行体(循环体)是词法块,因此需要分别注意其作用域。switch
的case
语句也是单独的词法块。
4. 数据类型
Go 语言的数据类型主要分为四种:基础类型(basic type),聚合类型(aggregate type),引用类型(reference type),接口类型(interface type)。这里只介绍前两种基本类型。
4.1 整数(basic type)
类型 | 占用大小 | 概述 |
---|---|---|
int/uint | 32位/64位 | 根据(32位/64位)平台确定,或者最有效率的大小 |
int8/uint8 | 8位 | 固定大小 |
int16/uint16 | 16位 | 固定大小 |
int32/uint32 | 32位 | 固定大小 |
int64/uint64 | 64位 | 固定大小 |
rune | 32位 | int32类型同义词,专用于unicode码点 |
byte | 8位 | uint8同义词,强调一个值是原始字节数据 |
uintptr | 32位/64位/… | 可以恰好存放下一个指针 |
有符号数使用补码表示,对于int/uint/uintptr
这些不确定大小的类型,需要显示转换为其他整数类型。
二元算术操作符包含以下内容,可适用于整数、浮点数、复数。
算术符 | 优先级 |
---|---|
* / % << >> & &^ | 1 |
`+ - | ^` |
== != < <= > >= | 3 |
&& | 4 |
` |
可以看到有五类优先级的运算符,但是实际使用还是以()
为最优选择。
下面是一些有关整数的描述:
-
同级别运算满足做结合律,从左往右依次计算。
-
取模运算符
%
仅能用于整数,其行为因编程语言而异,取模余数的正负号总是与被除数一致。 -
除法
/
的行为和数据类型有关,整数除法的商总是舍去小数部分,浮点数除法的商会得到小数。 -
如果整数运算的结果所需位数超过了该类型的范围,即产生溢出,那么溢出的高位会直接丢失,无任何报错。
-
^45
表示按位取反,12^21
表示异或,即一元操作符合二元操作符有不同含义。 -
&^
是按位清除操作,x&^y
,将x
中y
对应位为1
的位置为0,其他位保持不变。 -
移位运算中,
x>>n
,操作数n
必须为无符号数,对于有符号数右移,使用符号位补空位,即算术右移。 -
浮点数转换为小数,会舍弃小数部分,趋零截尾(整数向下,负数向上)。
-
整数可以写成十进制,八进制
0666
,十六进制0xdeadbeef
。
4.2 浮点数(basic type)
Go语言使用IEEE 754标准的浮点数,支持float32
和float64
两种。float32的有效位数约为6位,float64的有效数字约为15位。
可以使用科学计数法表示数字,6.201e23
,4.123e-25
。
当执行错误运算时,如x/0
,会报错,并返回NaN
,NaN
无法比较,它总是与任何值不相等。
4.3 复数(complex)(basic type)
Go有两种大小的复数,complex64和complex128,分别由float32和float64构成。
var x complex128 = complex(1, 2) //1 + 2i
y := 1 + 2i
浮点数或十进制整数后面紧接着写字母i,如3.14159i
,它就变成了一个虚数,表示实部为0的复数。
4.4 布尔值(boolean)(basic type)
有两种值true
和假false
, if和for语句里的条件就是布尔值。
布尔运算具有短路规则,如果运算符左边的操作数已经能直接确定总体结果,则右边的操作数不会计算到内。
布尔值无法直接隐式转换为数值,也不能使用int(b)形式。如下可以转换:
if b {
return 1
}
return 0
4.5 字符串(basic type)
字符串是不可变的字节序列,可以包含任何数据,如0值字节。
len(s)
返回字符串的字节数。s[i]
访问s的第i
个字符。字符串从0开始计数。如果越界访问将触发错误。
支持切片操作,如s[0:5]
返回从[0, 5)
区间内的字符(不包含右边界),这个操作将产生一个新的字符串。
s := "hello, world"
s[:5] // "hello"
s[7:] // "world"
s[:] // "hello, world"
加号(+)运算符可连接两个字符串生成一个新的字符串。
“new" + s[:6]
字符串可以使用比较运算符做比较,如==
和<
,比较运算按字节进行,结果服从本身字典排序。
字符串不可改变,因此两个字符串可以共用同一块内存,复制字符串的开销较小。
字符串的值成为字符串字面量string literal
。
十六进制的转义字符写为\xhh
格式,大小皆可,且必须是两位。八进制为\ooo
,必须为三位八进制数字。
原生的字符串字面量的书写形式为``,即两个反引号。原生的字符串字面量可以展开多行,但是会删除回车符,保留换行符,确保同一字符串在所有平台上的值都有相同。
对于非ASCII字符,其单字符所占字节数大于1个字节。range
支持字符串遍历,能自动识别多字节字符。
for i, r := range "hello, 世界" {
xxxxxxxxxxxx;
}
字符串操作主要有四个包:
strings
,对字符串操作的基本函数。bytes
,操作字节slice类型。strconv
,转换布尔值,整数,浮点数为对应的字符串形式,或反向转化。unicode
,判断文字符号值特性的函数。
bytes中为了高效处理字节slice提供了Buffer类型,Buffer起初为空,其大小随着各类型数据的写入而增长。添加任意文字符号的UTF-8编码,最好使用bytes.Buffer
的WriteRune
方法。追加ASCII字符,使用writeByte
。
4.6 常量(basic type)
常量可以在编译阶段就计算出表达式的值,并不需要等到运行时。如下:
const (
e = 2.718....
pi = 3.14159......
)
常量操作数,即数学、逻辑、比较运算的结果依然是常量。常量声明可以同时指定类型和值,如果没有显式指定类型,则类型根据右边的表达式推断。
常量的声明可以使用常量生成器iota
,即常见的enumeration
枚举类型。
const (
Sunday = iota
Monday
TuesDay
Wednesday
Thursday
Friday
Saturday
)
上面即从0开始枚举,也可以使用i << iota
按位开始枚举。
Go里面具有从属类型待定的常量,其可表示比基本类型更高的精度,至少达到256位。
这些从属类型待定的常量有六种:无类型布尔,无类型整数,无类型文字符号,无类型浮点数,无类型复数,无类型字符串。
无类型常量可以暂时维持更高的精度,与类型已确定的常量相比,还能写进更多表达式而无需转换类型。
只有常量才是无类型的,赋值会导致隐式的类型转换。如果变量类型无法表示常量的原值(允许舍入取整),那么编译器将报错。
如果变量声明中,没有显示指定类型,无类型变量会隐式转换为该变量的默认类型。
所以在转换时,最好还是显示的指定转换的类型。
4.7 数组(aggregate type)
数组具有固定的长度且拥有零个或者多个相同的数据类型元素的序列。数组可以通过索引访问。从0开始为第一个元素。
默认情况下,数组的每个元素都初始化为对应类型的零值。也可以使用下面的初始化方法:
var q [3]int = [3]int{1, 2, 3}
var p [3]int = [3]int{1, 2}
下面的定义方法可以由初始化数组的元素个数决定数组长度:
q := [...]int{1, 2, 3}
数组的长度是数组类型的一部分,所以[3]int和[4]int是两种不同的数组类型。数组的长度必须是常量表达式。
也可以按照下面给定元素顺序的方法初始化数组:
var p [3]int = [3]int{0:1, 2:2}
如果元素类型可以比较,那数组也可以来比较(==/!=),需要两个数组的元素完全相同才会相等。
特别注意,Go里面在函数参数里传递数组,被当成值传递,即复制一份副本。
可以通过指针来传递数组参数。
func zero(ptr *[32]byte) { ... }
4.8 可变序列slice(aggregate type)
slice表示一个拥有相同类型元素的可变长度的序列。slice通常写成[]T
,其中类型都是T。类似于没有长度限制的数组。
Slice有三个属性:指针、长度、容量。
- slice会有一个底层数组,用于实际存储数据。
- 指针会指向数组的第一个可以从slice中访问的元素。
- 长度是值slice中元素的个数,不能超过slice的容量。
- 容量的大小通常是指从slice的起始元素到底层数组的最后一个元素之间的元素个数。
Go的内置函数len
和cap
返回slice的长度和容量。一个底层数组可以对应多个slice。
num := [...]int{1, 2, 3, 4, 5, 6, 7, 8, 9}
上面的声明是一个数组声明,其有9个元素,该数组可以用来做slice的底层数组。
下面是创建多个基于该数组的slice对象:
s1 := num[0:5]
s2 := num[2:7]
......
s1的起始元素是num[0],长度是5,cap容量是9。
s2的起始元素是num[2],长度是5,cap容量是7。
如果slice的引用超过了被引用对象的容量,即cap(s),那么会导致程序宏机,如果slice的引用超出了被引用对象的长度,即len(s),那么最终slice会比slice长。
slice包含了指向数组元素的指针,所以将一个slice传递给函数的时候,可以在函数内部修改底层数组的元素。
下面是声明了一个slice字面量(没有指定长度):
s := []int{0, 1, 2, 3, 4, 5}
slice无法做比较,不能使用==
来判断两个slice是否拥有相同的元素。所以slice作比较,一般需要自定义函数来实现。
slice唯一可以和nil作比较,如:
if summer == nil { /* ... */ }
slice类型的零值是nil,值为nil的slice没有对应的底层数组,其容量和长度都是0。
需要注意,slice不为nil时,也可能容量和长度为0,即[]int{}
和make([]int, 3)[3:]
。
可使用类型转换表达式生成指定类型的nil值,如下面三种情况下,slice都是nil。
var s []int
s = nil
s = []int(nil)
值为nil的silce和其他长度为0的slice没有啥区别,Go函数以相同的方式对待长度为0的slice。
内置函数make可以用来创建一个具有指定元素类型、长度和容量的slice,其中容量可以忽略,在这种情况下,slice的长度和容量相等。
make([]T, len)
make([]T, len, cap) //等同于 make([]T, cap)[:len]
内置append
函数用来将元素追加到slice后面,如果原有的底层数组容量不够使用,那么会分配一个新的足够容量的数组来存储,并复制原来的元素。因此需要向下面这样使用,要考虑底层数组可能变换的可能性:
y = append(x, new_elememt)
对于有可能改变slice的长度或者容量的场景,抑或是使得slice指向不同的底层数组,都需要更新slice变量。
slice除了对底层数组是间接引用之外,还包含长度和容量的信息,因此是一个聚合数据结构。
4.9 散列表map(aggregate trype)
散列表map是一个拥有键值对元素的无序集合。
键对应的值可以通过键来获取、更新或移除,无论这个散列表有多大,这些操作基本是通过常量时间的键比较就可以完成。
在Go语言里,map类型为map[K]V
,其中K和V是字典的键和值对应的数据类型。map中所有的键都拥有相同的数据类型,同时所有的值也都拥有相同的数据类型,但是键的类型和值的类型不一定相同。
键的类型必须可以通过==
来比较的数据类型,如slice
等类型无法直接作为键。
有下面两种创建map的方式:
ages := make(map[string]int)
ages := map[string]int{
"alice": 31,
"charlie": 34,
}
空的map可以用map[string]int{}
来表示。
访问通过下标来访问:
ages["alice"] = 34
ages["charlie"] = 34
使用内置函数delete
来从字典里根据键删除一个元素:
delete(ages, 'alice')
对于map来说,即使键不存在,也是安全的,返回的是对应类型的零值。
map的地址一般无法被获取,因为map的增长可能会导致已有元素被重新散列到新的存储位置,这样可能使得获取的地址无效。
可以使用for循环(结合range关键字)来遍历map中所有键和对应的值。
forname, age := range ages {
fmt.Printf("%s\t%d\n", name, age)
}
如下直接声明的map变量,其值是零值(nil),即没有对应的散列表:
var ages map[string]int
大部分map操作都可以在map的零值nil上执行,包括查找元素,删除元素,获取map元素个数(len),执行range循环。但是想零值map中设置元素会导致错误。
map的判断操作应该如下:
if age, ok := ages["bob"];
......
第二个值ok
是布尔值,用来报告元素是否存在,map不能直接比较,必须通过自定义函数比较,因此需要考虑元素不存在的情况。
虽然slice,数组,结构体等聚合数据无法直接比较,没法做map的键,但是可以通过转化字符串,来进行二次映射。
4.10 结构体(struct)
结构体是将零个或者多个任意类型的命名变量组合在一起的聚合数据类型,每个变量都叫做结构体的成员。
type Employee struct {
ID int
Name string
money int
age,weight int
......
}
结构体的成员通过点号来访问(包括结构体指针),如Employee.ID
。
结构体的成员变量定义顺序非常重要,不同顺序的结构体变量,是不同的结构体类型。
如果一个结构体成员变量名称是首字母大写的,那么这个变量是可以导出的。对于Employee
来说,只有ID
和Name
是可以导出的。
在结构体中,只能定义相同类型的结构体指针,从而创建递归数据结构。
没有任何成员变量的结构体称为空结构体,写作struct{}
,没有长度和信息,但可以用作某类bool值的替代。
结构体类型值可以通过结构体字面量来设置,即通过设置结构体的成员变量来设置。
type Point struct{ X, Y int}
p := Point{1, 2}
这种初始化方式必须按照定义的顺序来初始化,也可以使用指定部分成员名称的方式初始化。
p := Point(X:1, Y:2)
即使是不指定名称,按顺序初始化结构体,也无法绕过不可导出变量的限制,即小写变量。
结构体通常通过指针的方式使用,如下:
pp := &Point{1, 2}
结构体嵌套和匿名成员:
type Point struct{ X, Y int}
type Circle struct {
Point
Radius int
}
var c Circle
Go允许定义不带名称的结构体成员,只是一种语法糖,本质是使用类型替代了,并允许访问时直接用c.X
来访问。匿名成员是否允许导出由它们类型名字是否大写来决定。
可以以%#v
来按照类似Go语法的方式输出对象,其中可以包含成员变量的名字。