Go语言基础(一)
目录
入门
Hello World
package main
import "fmt"
func init() {
fmt.Println("init method")
}
func main() {
fmt.Println("Hello, world!")
}
程序中每个代码文件里的 init 函数都会在 main 函数执行前调用。
所有用 Go 语言编译的可执行程序都必须有一个名叫 main 的包。
包管理
-
同目录下的源码文件的代码包声明语句要一致。也就是说,它们要同属于一个代码包。这对于所有源码文件都是适用的。
-
源码文件声明的包名可以与其所在目录的名称不同,只要这些文件声明的包名一致就可以。
-
名称的首字母为大写的程序实体才可以被当前包外的代码引用,否则它就只能被当前包内的其他代码引用。
-
在 Go 1.5 及后续版本中,我们可以通过创建internal代码包让一些程序实体仅仅能被当前模块中的其他代码引用。这被称为 Go 程序实体的第三种访问权限:模块级私有。
-
如果我们在当前源码文件中导入了其他代码包,那么引用其中的程序实体时,是需要以限定符为前缀的。所以程序在找代表变量未加限定符的名字(即标识符)的时候,是不会去被导入的代码包中查找的。
导入包
// 导入单个包
import "fmt"
// 导入多个包,每行一个
import (
"fmt"
"strings"
)
编译器会首先查找 Go 的安装目录,然后才会按顺序查找 GOPATH 变量里列出的目录。
如果编译器查遍 GOPATH 也没有找到要导入的包,那么在试图对程序执行 run 或者 build的时候就会出错。
当你导入了一个不在代码里使用的包时,Go 编译器会编译失败,并输出一个错误。
重命名导入
重命名导入是指,在 import 语句给出的包路径的左侧定义一个名字,将导入的包命名为新名字。
import (
"fmt"
myfmt "mylib/fmt"
)
go命令
Go语言有一套完整的操作命令(Go命令详解),非常实用,掌握它可以事半功倍。直接输入go,给出了全部命令,如下所示:
go 命令go build:此命令用于编译指定的源码文件或代码包及依赖包。
go install:go install 跟 go build 类似,只是多做了一件事就是安装编译后的文件到指定目录。
go get:go get命令用于动态获取远程代码包及其依赖包,并进行编译和安装。执行命令后一般会下载在GOPATH的src目录下。
go mod:go mod 是 go modules的简写,用于对go包的管理。
go run:go run用于编译并运行源码文件,由于包含编译步骤,所以go build参数都可用于go run,在go run 中只接受go源码文件而不接受代码包。
go clean:go clean命令用于删除执行其他命令时产生的文件或目录。
go fmt:go fmt用于检查并格式化成go语言的规范格式。
go list:go list 会列出当前安装的包。
go version:go version 可以查看当前go的版本。
go env:go env 可以查看当前go的环境变量。
命令行参数
flag库是Go语言标准库之一,提供了命令行参数解析的能力。扩展阅读:Go Flag 使用教程
// flag包提供了两种绑定参数的方法
type(...args) typePoint
typeVar(typePoint, ...args)
type
type函数支持: bool int int64 uint uint64 string float64 duration
函数 flag.type 接受 3 个参数:
第 1 个参数是用于存储该命令参数值的地址,具体到这里就是在前面声明的变量name的地址了,由表达式&name表示。
第 2 个参数是为了指定在未追加该命令参数时的默认值,这里是everyone。
第 3 个函数参数,即是该命令参数的简短说明了,这在打印命令说明时会用到。
示例
func main(){
boolVal := flag.Bool("testBool", false, "testBool is bool type.")
flag.Parse()
// 如果使用 -testBool作为参数,控制台将会打印 true, 否则打印 false
fmt.println(boolVal)
}
typeVar
typeVar函数支持: bool int int64 uint uint64 string float64 duration
函数 flag.typeVar 接受 4 个参数:
第 1 个参数是用于存储该命令参数值的地址,具体到这里就是在前面声明的变量name的地址了,由表达式&name表示。
第 2 个参数是为了指定该命令参数的名称,这里是name。
第 3 个参数是为了指定在未追加该命令参数时的默认值,这里是everyone。
第 4 个函数参数,即是该命令参数的简短说明了,这在打印命令说明时会用到。
示例
func main(){
flag.BoolVar(&config.BoolValue, "bool", false, "This is bool value.")
flag.IntVar(&config.IntValue, "int", 0, "This is int value.")
flag.Int64Var(&config.Int64Value, "int64", 0, "This is int64 value.")
flag.UintVar(&config.UintValue, "uint", 0, "This is uint value.")
flag.Uint64Var(&config.Uint64Value, "uint64", 0, "This is uint64 value.")
flag.StringVar(&config.StringValue, "string", "", "This is string value.")
flag.Float64Var(&config.Float64Value, "float64", 0, "This is float64 value")
flag.DurationVar(&config.DurationValue, "duration", time.Second * 0, "This is duration value.")
flag.Var(&config.MyValue, "myValue", "This is my value.")
}
传入参数
// 传入参数
go run demo2.go -name="Robert"
// 查看参数说明
go run demo2.go --help
变量
变量的声明和定义
我们直接通过示例来看一下声明和定义变量的几种常用方式:
// 声明并初始化一个变量
var m int = 10
//声明初始化多个变量
var i, j, k = 1, 2, 3
//多个变量的声明(注意小括号的使用)
var(
no int
name string
)
//声明时不指明类型,通过初始化值来推导
var b = true//bool型
//一种简单的方式等价于varstrstring="Hello"
str := "Hello"
//Go中有一个特殊的变量_任何赋给它的值将被丢弃
_, Ret:= 2, 3
Go的编译器对声明却未使用的变量会报错
类型推断
类型推断是一种编程语言在编译期自动解释表达式类型的能力。如下面的代码所示:
// Go 编译器可以根据变量值的初始值自动推断出变量的类型
str := "Hello"
Go 语言的类型推断可以明显提升程序的灵活性,使得代码重构变得更加容易,同时又不会给代码的维护带来额外负担(实际上,它恰恰可以避免散弹式的代码修改),更不会损失程序的运行效率。
变量重声明
变量重声明其实算是一个语法糖(或者叫便利措施)。它允许我们在使用短变量声明时不用理会被赋值的多个变量中是否包含旧变量。
-
由于变量的类型在其初始化时就已经确定了,所以对它再次声明时赋予的类型必须与其原本的类型相同,否则会产生编译错误。
-
变量的重声明只可能发生在某一个代码块中。
-
变量的重声明只有在使用短变量声明时才会发生,否则也无法通过编译。如果要在此处声明全新的变量,那么就应该使用包含关键字var的声明语句,但是这时就不能与同一个代码块中的任何变量有重名了��。
-
被“声明并赋值”的变量必须是多个,并且其中至少有一个是新的变量。这时我们才可以说对其中的旧变量进行了重声明。
代码示例
func main(){
var err error
// 这里的n是新的变量,而err则是重新声明的变量
n, err := io.WriteString(os.Stdout, "Hello, everyone!\n")
fmt.Printf(string(n))
fmt.Println(err)
}
变量作用域
Q:下面这段代码能否编译通过?如果能,运行输出结果是什么?
package main
import "fmt"
var block = "package"
func main() {
block := "function"
{
block := "inner"
fmt.Printf("The block is %s.\n", block)
}
fmt.Printf("The block is %s.\n", block)
}
A:可以编译通过,运行输出结果为:
The block is inner.
The block is function.
对于不同的代码块来说,其中的变量重名没什么大不了,照样可以通过编译。即使这些代码块有直接的嵌套关系也是如此。
变量引用规则
-
首先,代码引用变量的时候总会最优先查找当前代码块中的那个变量。注意,这里的“当前代码块”仅仅是引用变量的代码所在的那个代码块,并不包含任何子代码块。
-
其次,如果当前代码块中没有声明以此为名的变量,那么程序会沿着代码块的嵌套关系,从直接包含当前代码块的那个代码块开始,一层一层地查找。
-
一般情况下,程序会一直查到当前代码包代表的代码块。如果仍然找不到,那么 Go 语言的编译器就会报错了。
值传递和引用传递
Go 语言里不存在像 Java 等编程语言中令人困惑的“传值或传引用”问题。在 Go 语言中,我们判断所谓的“传值”或者“传引用”只要看被传递的值的类型就好了。
如果传递的值是引用类型的,那么就是“传引用”。如果传递的值是值类型的,那么就是“传值”。
数组、切片和映射
数组和切片的共同点是都属于集合类的类型,并且,它们的值也都可以用来存储某一种类型的值(或者说元素)。
不过,它们最重要的不同是:数组类型的值(以下简称数组)的长度是固定的,而切片类型的值(以下简称切片)是可变长的。
数组的长度在声明它的时候就必须给定,并且之后不会再改变。
我们其实可以把切片看做是对数组的一层简单的封装,因为在每个切片的底层数据结构中,一定会包含一个数组。数组可以被叫做切片的底层数组,而切片也可以被看作是对数组的某个连续片段的引用。
数组(Array)
声明和初始化
声明数组时需要指定内部存储的数据的类型,以及需要存储的元素的数量,这个数量也称为数组的长度:
// 声明一个包含 5 个元素的整型数组
var array [5]int
// 声明一个包含 5 个元素的整型数组,用具体值初始化每个元素
array := [5]int{10, 20, 30, 40, 50}
// 声明一个整型数组,用具体值初始化每个元素
// 容量由初始化值的数量决定
array := [...]int{10, 20, 30, 40, 50}
// 声明一个有 5 个元素的数组
// 用具体值初始化索引为 1 和 2 的元素,其余元素保持零值
array := [5]int{1: 10, 2: 20}
一旦声明,数组里存储的数据类型和数组长度就都不能改变了。如果需要存储更多的元素, 就需要先创建一个更长的数组,再把原来数组里的值复制到新数组里。
使用数组
使用数组
数组变量的类型包括数组长度和每个元素的类型。只有这两部分都相同的数组,才是类型相同的数组,才能互相赋值。
// 声明第一个包含 4 个元素的字符串数组
var array1 [4]string
// 声明第二个包含 5 个元素的字符串数组
// 使用颜色初始化数组
array2 := [5]string{"Red", "Blue", "Green", "Yellow", "Pink"}
// 将 array2 复制给 array1
array1 = array2
Compiler Error:
cannot use array2 (type [5]string) as type [4]string in assignment
数组指针
// 声明第一个包含 3 个元素的指向字符串的指针数组
var array1 [3]*string
// 声明第二个包含 3 个元素的指向字符串的指针数组
// 使用字符串指针初始化这个数组
array2 := [3]*string{new(string), new(string), new(string)}
// 使用颜色为每个元素赋值
*array2[0] = "Red"
*array2[1] = "Blue"
*array2[2] = "Green"
// 将 array2 复制给 a
多维数组
// 声明一个二维整型数组,两个维度分别存储 4 个元素和 2 个元素
var array [4][2]int
// 使用数组字面量来声明并初始化一个二维整型数组
array := [4][2]int{{10, 11}, {20, 21}, {30, 31}, {40, 41}}
// 声明并初始化外层数组中索引为 1 个和 3 的元素
array := [4][2]int{1: {20, 21}, 3: {40, 41}}
// 声明并初始化外层数组和内层数组的单个元素
array := [4][2]int{1: {0: 20}, 3: {1: 41}}
在函数间传递数组
根据内存和性能来看,在函数间传递数组是一个开销很大的操作。在函数之间传递变量时, 总是以值的方式传递的。如果这个变量是一个数组,意味着整个数组,不管有多长,都会完整复 制,并传递给函数。虽然 Go 语言自己会处理这个复制操作,不过还有一种更好且更 有效的方法来处理这个操作。可以只传入指向数组的指针,这样只需要复制 8 字节的数据而不是 8 MB 的内存数据到栈上。
// 分配一个需要 8 MB 的数组
var array [1e6]int
// 将数组的地址传递给函数 foo
foo(&array)
// 函数 foo 接受一个指向 100 万个整型值的数组的指针
func foo(array *[1e6]int) {
...
}
这个操作会更有效地利用内存,性能也更好。不过要意识到,因为现在传递的是指针, 所以如果改变指针指向的值,会改变共享的内存。如你所见,使用切片能更好地处理这类共 享问题。
切片(Slice)
切片是一个很小的对象,对底层数组进行了抽象,并提供相关的操作方法。
切片有3个字段,这 3 个字段分别是指向底层数组的指针、切片访问的元素的个数(即长度)和切片允许增长 到的元素个数(即容量)。
创建和初始化
// 创建一个字符串切片,其长度和容量都是 5 个元素
slice := make([]string, 5)
// 创建一个整型切片,其长度为 3 个元素,容量为 5 个元素
slice := make([]int, 3, 5)
// 创建字符串切片
// 其长度和容量都是 5 个元素
slice := []string{"Red", "Blue", "Green", "Yellow", "Pink"}
// 创建一个整型切片
// 其长度和容量都是 3 个元素
slice := []int{10, 20, 30}
// 创建字符串切片
// 使用空字符串初始化第 100 个元素
slice := []string{99: ""}
如果基于这个切片创建新的切片,新 切片会和原有切片共享底层数组,也能通过后期操作来访问多余容量的元素。
不允许创建容量小于长度的切片
如果在[]运算符里指定了一个值,那么创建的就是数组而不是切片。只有不指定值 的时候,才会创建切片
nil切片
有时,程序可能需要声明一个值为 nil 的切片(也称 nil 切片)。只要在声明时不做任何初 始化,就会创建一个 nil 切片。
在 Go 语言里,nil 切片是很常见的创建切片的方法。nil 切片可以用于很多标准库和内置 函数。在需要描述一个不存在的切片时,nil 切片会很好用。例如,函数要求返回一个切片但是 发生异常的时候。
// 创建 nil 整型切片
var slice []int
// 使用 make 创建空的整型切片
slice := make([]int, 0)
// 使用切片字面量创建空的整型切片
slice := []int{}
不管是使用 nil 切片还是空切片,对其调用内置函数 append、len 和 cap 的效果都是 一样的。
使用切片创建切片
// 创建一个整型切片
// 其长度和容量都是 5 个元素
slice := []int{10, 20, 30, 40, 50}
// 创建一个新切片
// 其长度为 2 个元素,容量为4个元素
newSlice := slice[1:3]
// 修改 newSlice 索引为 1 的元素
// 同时也修改了原来的 slice 的索引为 2 的元素
newSlice[1] = 35
// 修改 newSlice 索引为 3 的元素
// 这个元素对于 newSlice 来说并不存在
newSlice[3] = 45
Runtime Exception:
panic: runtime error: index out of range
在创建切片时,还可以使用之前我们没有提及的第三个索引选项。第三个索引可以用来控制 新切片的容量。其目的并不是要增加容量,而是要限制容量。可以看到,允许限制新切片的容量 为底层数组提供了一定的保护,可以更好地控制追加操作。
// 创建字符串切片
// 其长度和容量都是 5 个元素
source := []string{"Apple", "Orange", "Plum", "Banana", "Grape"}
// 将第三个元素切片,并限制容量
// 其长度为 1 个元素,容量为 2 个元素
slice := source[2:3:4]
切片增长
相对于数组而言,使用切片的一个好处是,可以按需增加切片的容量。Go 语言内置的 append 函数会处理增加长度时的所有操作细节。
// 创建一个整型切片
// 其长度和容量都是 5 个元素
slice := []int{10, 20, 30, 40, 50}
// 创建一个新切片
// 其长度为 2 个元素,容量为 4 个元素
newSlice := slice[1:3]
// 使用原有的容量来分配一个新元素
// 将新元素赋值为 60
newSlice = append(newSlice, 60)
append 操作之后的底层数组:
因为 newSlice 在底层数组里还有额外的容量可用,append 操作将可用的元素合并到切片 的长度,并对其进行赋值。由于和原始的 slice 共享同一个底层数组,slice 中索引为 3 的元 素的值也被改动了。 如果切片的底层数组没有足够的可用容量,append 函数会创建一个新的底层数组,将被引 用的现有的值复制到新数组里,再追加新的值。
// 创建一个整型切片
// 其长度和容量都是 4 个元素
slice := []int{10, 20, 30, 40}
// 向切片追加一个新元素
// 将新元素赋值为 50
newSlice := append(slice, 50)
当这个 append 操作完成后,newSlice 拥有一个全新的底层数组,这个数组的容量是原来的两倍,如下图所示:
函数 append 会智能地处理底层数组的容量增长。在切片的容量小于 1000 个元素时,总是 会成倍地增加容量。一旦元素个数超过 1000,容量的增长因子会设为 1.25,也就是会每次增加 25% 的容量。随着语言的演化,这种增长算法可能会有所改变。
迭代切片
当迭代切片时,关键字 range 会返回两个值。第一个值是当前迭代到的索引位置,第二个 值是该位置对应元素值的一份副本。
// 创建一个整型切片
// 其长度和容量都是 4 个元素
slice := []int{10, 20, 30, 40}
// 使用 range 迭代每一个元素,并显示其值
for index, value := range slice {
fmt.Printf("Index: %d Value: %d\n", index, value)
}
// 使用传统的 for 循环对切片进行迭代
// 从第三个元素开始迭代每个元素
for index := 2; index < len(slice); index++ {
fmt.Printf("Index: %d Value: %d\n", index, slice[index])
}
range 创建了每个元素的副本,而不是直接返回对该元素的引用
函数 len 返回切片的长度,函数 cap 返回切片的容量。
在函数间传递切片
在函数间传递切片就是要在函数间以值的方式传递切片。由于切片的尺寸很小,在函数间复 制和传递切片成本也很低。
在 64 位架构的机器上,一个切片需要 24 字节的内存:指针字段需要 8 字节,长度和容量字段分别需要 8 字节。由于与切片关联的数据包含在底层数组里,不属于切片本身,所以将切片复制到任意函数的时候,对底层数组大小都不会有影响。复制时只会复制切片本身,不会涉及底层数组。
映射(Map)
映射是一个存储键值对的无序集合。
创建和初始化
// 创建一个映射,键的类型是 string,值的类型是 int
dict := make(map[string]int)
// 创建一个映射,键和值的类型都是 string
// 使用两个键值对初始化映射
dict := map[string]string{"Red": "#da1337", "Orange": "#e95a22"}
切片、函数以及包含切片的结构类型这些类型由于具有引用语义, 不能作为映射的键,使用这些类型会造成编译错误
使用映射
// 创建一个空映射,用来存储颜色以及颜色对应的十六进制代码
colors := map[string]string{}
// 将 Red 的代码加入到映射
colors["Red"] = "#da1337"
可以通过声明一个未初始化的映射来创建一个值为 nil 的映射(称为 nil 映射 )。nil 映射 不能用于存储键值对,否则,会产生一个语言运行时错误
// 通过声明映射创建一个 nil 映射
var colors map[string]string
// 将 Red 的代码加入到映射
colors["Red"] = "#da1337"
Runtime Error:
panic: runtime error: assignment to entry in nil map
判断key是否存在
方法一:
// 获取键 Blue 对应的值
value, exists := colors["Blue"]
// 这个键存在吗?
if exists {
fmt.Println(value)
}
方法二:
// 获取键 Blue 对应的值
value := colors["Blue"]
// 这个键存在吗?
if value != "" {
fmt.Println(value)
}
迭代映射
迭代映射里的所有值和迭代数组或切片一样,使用关键字 range,但对映射来说,range 返回的不是索引和值,而是键值对。
// 创建一个映射,存储颜色以及颜色对应的十六进制代码
colors := map[string]string{
"AliceBlue": "#f0f8ff",
"Coral": "#ff7F50",
}
// 显示映射里的所有颜色
for key, value := range colors {
fmt.Printf("Key: %s Value: %s\n", key, value)
}
删除元素
如果想把一个键值对从映射里删除,就使用内置的 delete 函数
// 删除键为 Coral 的键值对
delete(colors, "Coral")
在函数间传递映射
在函数间传递映射并不会制造出该映射的一个副本。实际上,当传递映射给一个函数,并对 这个映射做了修改时,所有对这个映射的引用都会察觉到这个修改