Golang
一、简介
Golang是谷歌在2009年推出的编译性编程语言,可以在不损失应用程序性能的情况下降低代码的复杂性。Google 对 Go 寄予厚望,其设计是让软件充分发挥多核心处理器同步多工的优点,并可解决面向对象程序设计的麻烦。它具有现代的程序语言特色,如垃圾回收,帮助开发者处理琐碎但重要的内存管理问题。Go 的速度也非常快,几乎和 C 或 C++ 程序一样快,且能够快速开发应用程序。
二、入门
包和导入
Go代码是使用包来组织的,包类似于其他语言中的库。一个包由一个或多个.go源文件组成,放在一个文件夹中。每一个源文件开始使用package声明,指明该文件属于哪个包。Go标准库中有100多个包,用来完成输入输出等常规任务。
名为main的包比较特殊,它用来定义一个可执行程序,在main包中,main函数也是特殊的,它是程序开始执行的地方。
声明完package之后需要import程序中使用到的其他包中的函数。在Go中必须精确的声明包,缺失导入或存在不需要的包都会导致编译失败,这样可以防止程序允许时引用不需要的包。但是,有时候,我们必须导人一个包这仅仅是为了利用其副作用:对包级别的变量执行初始化表达式求值,并执行它的 init 函数,为了防止未使用的导入错误,我们必须使用一个重命名导入,它使用一个替代的名字
import _ "包名"
这表示导入的内容为空白标识符通常情况下,空白标识不可被引用
变量
一个声明有一个通用的形式,var 声明创建一个具体类型的变量,然后给它附加一个名字,设置它的初始值类型和表达式部分可以省略一个,但是不能都省略,如果类型省略,它的类型将由初始化表达式决定;如果表达式省略,其初始值对应于类型的零值一一对于数字是0,对于布尔值是 false ,对于字符串是"",对于接口和引用类型( slice、指针、map、通道、函数)是nil。对于一个像数组或结构体这样的复合类型,零值是其所有元素或成员的零值。
var 变量名 变量类型 = 变量初始内容
var 变量名 变量类型
var 变量名 = 变量初始内容
var a = "123"
var b int
var d bool = true
var a, b, c = 1.5, true, "string"
在函数中,一种称作短变量声明的可选形式可以用来声明和初始化局部变量。
变量名 := 变量初始内容
c := 1.15
变量 := "变量"
a, b, c := 1.5, true, "string"
常量
常量是一种表达式,其可以保证在编译阶段就计算出表达式的值,并不需要等到运行时,从而使编译器得以知晓其值。所有常量本质上都属于基本类型:布尔型 、字符串或数字。
常量的声明定义了具名的值,它看起来在语法上与变量类似,但该值恒定,这防止了程序运行过程中的意外或恶意修改。例如,要表示数学常量,像Π、e等
const (
pi = math.Pi
e = math.E
)
若同时声明一组常量,除了第一项之外,其他项在等号右侧的表达式都可以省略,这意味着会复用前面一项的表达式及其类型。
const (
a = 1
b
c = 2.2
d
)
fmt.Println(a, b, c, d)
常量的声明可以使用常量生成器 iota ,它创建一系列相关值,而不是逐个值显式写出。常量声明中,iota从0开始取值,逐项加1。
const (
a = iota //0
b // 1
d // 2
e // 3
)
指针
指针的值是一个变量的地址,一个指针指示值所保存的位置。不是所有的值都有地址,但是所有的变量都有。使用指针,可以在无须知道变量名字的情况下,间接读取或更新变量的值。
如果一个变量声明为 var x int ,表达式&x ( x的地址) 获取一个指向整型变量的指针,它的类型是整型指针(*int )。如果值叫作p ,我们说p指向x,或者p包含x的地址。p指向的变量写成*p。表达式*p获取变量的值,一个整型,因为*p代表一个变量,所以它也可以出现在赋值操作符左边,用于更新变量的值。
x := 1
p := &x
fmt.Println(*p)
*p = 2
fmt.Println(x)
数据类型
复数
Go 备两种大小的复数 complex64和complex128 ,二者分别由 float32和float64构成。内置 complex 函数根据给定的实部和虚部创建复数,而内置的俨eal 函数和 imag 函数则分别提取复数的实部和虚部:
x := complex(1, 2) // 1 + 2i
y := complex(3, 4) // 3 + 4i
fmt.Println(x + y)
fmt.Println(x * y)
fmt.Println(real(x * y))
fmt.Println(imag(x * y))
可以用==或!=判断复数是否相等,若两个复数的实部和虚部都相等,则它们相等。math/cmplx 包提供了复数运算所需的库函数,例如复数的平方根函数和复数的幕函数。
fmt.Println(cmplx.Sqrt(-1))
数组
数组中的每个元素是通过索引来访问的,索引从0到数组长度减1。 Go 内置的函数 len可以返回数组中的元素个数
var a = [...]int{1, 2, 3}
b := [...]int{0, 1, 2, 3, 4, 5, 6}
c := b[1:3]
d := b[:3]
e := b[1:]
var f [4]int
fmt.Println(a)
fmt.Println(b, len(b))
fmt.Println(c)
fmt.Println(d)
fmt.Println(e)
fmt.Println(f)
如果一个数组的元素类型是可比较的,那么这个数组也是可比较的,这样我们就可以直接使用==操作符来比较两个数组,比较的结果是两边元素的值是否完全相同。使用!=来比较两个数组是否不同
a := [3]int{1, 2, 3}
b := [3]int{1, 2, 3}
c := [3]int{0, 1, 2}
d := [4]int{1,2,3}
fmt.Println(a == b, a == c)
slice
slice 表示一个拥有相同类型元素的可变长度的序列 slice 通常写成[ ]T ,其中元素的类型都是T;它看上去像没有长度的数组类型。
数组和 slice 是紧密关联的 slice 是一种轻量级的数据结构,可以用来访问数组的部分或者全部的元素,而这个数组称为 slice 的底层数组。slice有三个属性:指针、长度和容量。指针指向数组的第一个可以从 slice 中访问的元素,这个元素并不一定是数组的第一个元素。长度是指 slice 中的元素个数,它不能超过 slice 的容量。容量的大小通常是从 slice 的起始元素到底层数组的最后一个元素间元素的个数 。Go 的内置函数 len和cap 用来返回 slice 的长度和容量。
a := []int(nil)
b := []int{}
var c []int
d := make([]int, 5, 10)
fmt.Println(a)
fmt.Println(b, len(b), cap(b))
fmt.Println(c)
fmt.Println(d, len(d), cap(d))
内置函数 append 用来将元素追加到 slice 的后面
x, y := []int{}, []int{}
for i := 0; i < 10; i++ {
y = append(x, i)
fmt.Printf("%d cap=%d %v\n", i, cap(y), y)
x = y
}
func appendInt(x []int, y int) []int {
z := []int{}
zlen := len(x) + 1
if zlen <= cap(x) {
//slice仍有增长空间
z = x[:zlen]
} else {
//slice已无空间,重新分配一个新的底层数组,为了达到分摊线性复杂度,容量扩展一倍
zcap := zlen
if zcap < 2*len(x) {
zcap = 2 * len(x)
}
z = make([]int, zlen, zcap)
copy(z, x)
}
z[len(x)] = y
return z
}
每一次 appendint 调用都必须检查 slice 是否仍有足够容量来存储数组中的新元素。如果slice 容量足够,那么它就会定义一个新的 slice (仍然引用原始底层数组),然后将新元素y复制到新的位置,并返回这个新 slice 。输入参数 slice x和函数返回值 slice z拥有相同的底层数组。
如果 slice 的容量不够容纳增长的元素, appendInt 函数必须创建一个拥有足够容量的新的底层数组来存储新元素 ,然后将元素从 slice x复制到这个数组,再将新元素y追加到数组后面。返回值 slice z 将和输入参数 slice x 引用不同的底层数组。
内置的 append 函数使用了比这里 appendInt 更复杂的增长策略。通常情况下,我们不清楚 append 调用会不会导致一次新的内存分配,所以我们不能假设原始的sl ice 和调用append 后的结果 slice 指向同一个底层数组,也无法证明它们就指向不同的底层数组。同样,我们也无法假设旧 slice 上对元素的操作会或会影响新的 slice 元素 所以,通常我们将 append 的调用结果再次赋值给传入 append 函数的 slice。
appendInt 函数只能给 slice 添加一个元素,但是内置的 append 函数可以同时 slice加多个元素,甚至添加另一个 slice 里的所有元素。
x := []int{}
x = append(x, 1, 2, 3)
x = append(x, x...)
fmt.Println(x)
map
map是一个拥有键值对元素的无序集合,在这个集合中,键的值是唯一的,键对应的值可以通过键来获取、更新或移除。无论这个map有多大,这些操作基本上是通过常量时间的键比较就可以完成。
内置函数 make 可以用来创建一个 map,也可以使用 map 的字面量来新建一个带初始化键值对元素的字典
m1 := make(map[string]int)
m2 := map[string]int{
"Bob": 18,
"Alice": 20,
}
m2["Jack"] = 25
可以使用内置函数 delete 字典中根据键移除一个元素。即使键不在 map 中,上面的操作也都是安全的。map 使用给定的键来查找元素,如果对应元素不存在,就返回值类型的零值。
delete(m2, "Jack")
delete(m2, "Mike")
但是map元素不是一个变量,不可以获取它的地址。一个原因是map的增长可能会导致已有元素被重新散
列到新的存储位置,这样就可能使得获取的地址无效。
可以使用 for 循环(结合 range 关键字)来遍历 map中所有的键和对应的值,就像上面遍历 slice 一样。循环语句的连续迭代将会使得变量 name和age 被赋予 map 中的下一对键和值。
map 元素的迭代顺序是不固定的,不同的实现方法会使用不同的散列算法,得到不同的元素顺序。实践中 ,我们认为这种顺序是随机的, 从一个元素开始到后一个元素,依次执行。这个是有意为之的, 这样可以使得程序在不同的散列算法实现下变得健壮。
for name := range m2 {
fmt.Println(name, m2[name])
}
大多数的 map 操作都可以安全地在 map 的零值 nil 上执行,包括查找元素,删除元素,获取 map元素个数,执行range 循环,因为这和空 map 的行为一致。 但是向零值 map中设置元素会导致错误。
通过下标的方式访问 map 中的元素总是会有值。如果键在 map 中,你将得到键对应的值;如果键不在 map 中,你将得到 map 值类型的零值。
m3 := map[string]int{}
m3["Alice"] = 12
age, ok := m3["Alice"]
fmt.Println(age, ok)
age, ok = m3["Bob"]
fmt.Println(age, ok)
fmt.Println(m3)
结构体
结构体是将零个或者多个任意类型的命名变量组合在一起的聚合数据类型。每个变量都叫作结构体的成员
type Student struct {
id int
name string
age int
}//定义一个结构体
var stu Student //声明一个结构体类型的变量
stu.id = 1// 结构体的变量赋值
stu.name = "Alice"
age := &stu.age//通过指针给结构体的变量赋值
*age = 18
结构体的成员变量通常一行写一个,变量的名称在类型的前面,但是相同类型的连续成员变量可以写在一行上。如果struct名称首字母是小写的,这个struct不会被导出。连同它里面的字段也不会导出,即使有首字母大写的字段名。如果struct名称首字母大写,则struct会被导出,但只会导出它内部首字母大写的字段,那些小写首字母的字段不会被导出,这也是Go中封装的基本方式
结构体类型的值可以通过结构体字面量来设置 ,即通过设置结构体的成员变量来设置。有两种格式的结构体字面量,一种格式如下,它要求按照正确的顺序,为每个成员量指定一个值。
stu := Student{1, "Alice", 18}
这会给开发和阅 代码的人增加负担,因为他们必须记住每个成员变的顺序,另外这也使得未来结构体成员变量扩充或者新排列的时候代码维护性差。所以,这种格式一般用在定义结构体类型的包中或者一些明显的成员变量顺序约定的小结构体中。我们用得更多是第二种格式,通过指定部分或者全部成员变量的名称和值来初始化结构体变量。
stu := Student{Name: "Ailce",Id: 1}
Go 存在不同寻常的结构体嵌套机制,这个机制可以让我们将一个命名结构体当作另一个结构体类型的匿名成员使用;并提供了一种方便的语法,使用简单的表达式(比如x.f )就可以代表连续的成员(比如 x.d.e.f)
type Point struct {
X,Y int
}
type Circle struct {
X,Y,R int
}
type Wheel struct {
X,Y,R,Spokes int
}
在需要支持的形状变多之后,我们将意识到它们之间的相似性和重复性 所以我们会重构相同的部分
type Point struct {
X, Y int
}
type Circle struct {
Center Point
R int
}
type Wheel struct {
Circle Circle
Spokes int
}
var w Wheel
w.Circle.Center.X = 5
w.Circle.Center.Y = 5
w.Circle.R = 10
w.Spokes = 20
访问 Wheel 的成员变麻烦了。Go允许我们定义不带名称的结构体成员,只需要指定类型即可;这种结构体成员称做匿名成员。这个结构体成员的类型必须是一个命名类型或者指向命名类型的指针。
type Circle struct {
Point
R int
}
type Wheel struct {
Circle
Spokes int
}
var w Wheel
w.X = 5
w.Y = 5
w.R = 10
w.Spokes = 20
因为“匿名成员”拥有隐式的名字,所以你不能在一个结构体里面定义两个相同类型的匿名成员,否则会引起冲突
函数
每个函数声明都包含一个名字、一个形参列表、一个可选的返回列表以及函数体
func 函数名(形参列表)[返回值列表]{
函数体
}
一个函数能够返回不止一个结果。我们之前已经见过标准包内的许多函数返回两个值,一个期望得到的计算结果与一个错误值,或者一个表示函数调用是否正确的布尔值。
func gcd(x, y int) (int,string){
if x <= 0 || y <= 0{
return 0,"输入参数错误"
}
for y != 0 {
x, y = y, x%y
}
return x,""
}
如果当函数调用发生错误时返回一个附加的结果作为错误值,习惯上将错误值作为最后一个结果返回。
函数变量也有类型,而且它们可以赋给变量或者传递或者从其他函数中返回。函数变量可以像其他函数一样调用。
func square(n int) int {
return n * n
}
f := square
fmt.Println(f(3))
变长函数被调用的时候可以有可变的参数个数,最令人熟知的例子就是 fmt.Println。Println 需要在开头提供一个固定的参数,后续便可以接受任意数目的参数。在参数列表最后的类型名称之前使用省略号“…”表示声明 个变长函数,调用这个函数的时候可以传递该类型任意数目的参数。
func sum(vals ...int) int {
sum := 0
for _,val := range vals{
sum += val
}
return sum
}
方法
方法的声明和普通函数的声明类似,只是在函数名字前面多了一个参数,这个参数把这个方法绑定到这个参数对应的类型上。
func (方法接收者 方法接收者类型) 方法名(形参列表) 返回值列表{
方法体
}
package main
import "fmt"
type User struct {
Id int
Name string
}
func (u User) Print() {
fmt.Printf("%v : %v", u.Id, u.Name)
}
func main() {
u := User{1, "Ailce"}
u.Print()
}
由于主调函数会复制每一个实参变量,如果函数需要更新 个变量,或者如果一个实参太大而我们希望避免复制整个实参,因此我们必须使用指针来传递变量的地址。这也同样适合用于更新接收者。
func (u *User) SetName(name string) {
u.Name = name
}
p := &u
p.SetName("Bob")
fmt.Println(u.Name)
接口
接口类型是对其他类型行为的概括与抽象 通过使用接口,我们可以写出更加灵活和通用的函数,这些函数不用绑定在一个特定的类型实现上。Go 语言的接口的独特之处在于它是隐式实现。换句话说,对于一个具体的类型,无须声明它实现了哪些接口,只要提供接口所必需的方法即可。这种设计让你无须改变已有类型的实现,就可以为这些类型创建新的接口,对于那些不能修改包的类型,这一点特别有用。
一个对象只要全部实现了接口中的方法,那么就实现了这个接口。一个类型可以同时实现多个接口,而接口间彼此独立,不知道对方的实现。接口类型变量能够存储所有实现了该接口的实例。
package main
import "fmt"
type Phone interface {
Say()
Use()
}
type China interface {
Says()
}
type IPhone struct {
}
type HuaWei struct {
}
func (iPhone IPhone) Say() {
fmt.Println("iPhone")
}
func (iPhone IPhone) Use() {
fmt.Println("Very Good!!")
}
func (huaWei HuaWei) Say() {
fmt.Println("遥遥领先!!!")
}
func (huaWei HuaWei) Use() {
fmt.Println("遥遥领先!!!")
}
func (huaWei HuaWei) Says() {
fmt.Println("中国品牌")
}
func main() {
var phone Phone // 接口类型变量
phone = new(IPhone)
phone.Say()
phone.Use()
phone = new(HuaWei)
phone.Say()
phone.Use()
huaWei := new(HuaWei)
huaWei.Says()
huaWei.Use()
huaWei.Say()
}
另外,我们还可以通过组合已有接口得到的新接口
package main
import "fmt"
type Phone interface {
Say()
Use()
}
type ChinaPhone interface {
Phone
Says()
}
type IPhone struct {
}
type HuaWei struct {
}
func (iPhone IPhone) Say() {
fmt.Println("iPhone")
}
func (iPhone IPhone) Use() {
fmt.Println("Very Good!!")
}
func (huaWei HuaWei) Say() {
fmt.Println("遥遥领先!!!")
}
func (huaWei HuaWei) Use() {
fmt.Println("遥遥领先!!!")
}
func (huaWei HuaWei) Says() {
fmt.Println("中国品牌")
}
func main() {
var phone Phone
phone = new(IPhone)
phone.Say()
phone.Use()
phone = new(HuaWei)
phone.Say()
phone.Use()
var chinaPhone ChinaPhone
chinaPhone = new(HuaWei)
chinaPhone.Says()
chinaPhone.Use()
chinaPhone.Say()
}
空接口是指没有定义任何方法的接口,因此任何类型都实现了空接口,空接口类型的变量可以存储任意类型的变量。
var x interface{}
s := "string"
x = s
fmt.Println(x)
i := 15
x = i
fmt.Println(x)
使用空接口实现可以接收任意类型的函数参数。比如fmt.Println()
func Println(a ...interface{}) (n int, err error) {
return Fprintln(os.Stdout, a...)
}
使用空接口实现可以保存任意值的字典
people := map[string]interface{}{}
people["name"] = "Ailce"
people["age"] = 18