Go 是一门被设计用来构建简单、高效、可信赖软件的开源程序设计语言。
Go 是一个开源的编程语言,它能让构造简单、可靠且高效的软件变得容易。
Go是从2007年末由Robert Griesemer, Rob Pike, Ken Thompson主持开发,后来还加入了Ian Lance Taylor, Russ Cox等人,并最终于2009年11月开源,在2012年早些时候发布了Go 1稳定版本。现在Go的开发已经是完全开放的,并且拥有一个活跃的社区。
Go 语言特色
简洁、快速、安全
并行、有趣、开源
内存管理、数组安全、编译迅速
Go语言用途
Go 语言被设计成一门应用于搭载 Web 服务器,存储集群或类似用途的巨型中央服务器的系统编程语言。
对于高性能分布式系统领域而言,Go 语言无疑比大多数其它语言有着更高的开发效率。它提供了海量并行的支持,这对于游戏服务端的开发而言是再好不过了。
package main
import "fmt"
func main() {
/* 这是我的第一个简单的程序 */
fmt.Println("Hello, World!")
}
值
Go 拥有多种值类型,包括字符串、整型、浮点型、布尔型等。
package main
import "fmt"
func main() {
fmt.Println("go" + "lang") //字符串可以通过 + 连接。
fmt.Println("1+1 =", 1+1)
fmt.Println("7.0/3.0 =", 7.0/3.0)
fmt.Println(true && false)
fmt.Println(true || false)
fmt.Println(!true)
}
变量
在 Go 中,变量 需要显式声明,并且在函数调用等情况下, 编译器会检查其类型的正确性。
var可以声明 1 个或者多个变量。Go 会自动推断已经有初始值的变量的类型。声明后却没有给出对应的初始值时,变量将会初始化为 零值 。 例如,int
的零值是 0
。:=
语法是声明并初始化变量的简写, 例如 var f string = "short"
可以简写为右边这样,也比较常用的写法,不能用于进行全局变量声明。
package main
import "fmt"
func main() {
var a = "initial"
fmt.Println(a)
var b, c int = 1, 2
fmt.Println(b, c)
var d = true
fmt.Println(d)
var e int
fmt.Println(e)
f := "short"
fmt.Println(f)
}
常量
Go 支持字符、字符串、布尔和数值 常量 。
const
用于声明一个常量。
const
语句可以出现在任何 var
语句可以出现的地方
常数表达式可以执行任意精度的运算
数值型常量没有确定的类型,直到被给定某个类型,比如显式类型转化。
一个数字可以根据上下文的需要(比如变量赋值、函数调用)自动确定类型。 举个例子,这里的 math.Sin
函数需要一个 float64
的参数,n
会自动确定类型。
package main
import (
"fmt"
"math"
)
const s string = "constant"
func main() {
fmt.Println(s)
const n = 500000000
const d = 3e20 / n
fmt.Println(d)
fmt.Println(int64(d))
fmt.Println(math.Sin(n))
}
for循环
for
是 Go 中唯一的循环结构。这里会展示 for
循环的三种基本使用方式。
package main
import "fmt"
func main() {
i := 1
for i <= 3 {
fmt.Println(i)
i = i + 1
}
for j := 7; j <= 9; j++ {
fmt.Println(j)
}
for {
fmt.Println("loop")
break
}
for n := 0; n <= 5; n++ {
if n%2 == 0 {
continue
}
fmt.Println(n)
}
}
if/else分支
if
和 else
分支结构在 Go 中非常直接。
你可以不要 else
只用 if
语句。
在条件语句之前可以有一个声明语句;在这里声明的变量可以在这个语句所有的条件分支中使用。
注意,在 Go 中,条件语句的圆括号不是必需的,但是花括号是必需的。
Go 没有三目运算符, 即使是基本的条件判断,依然需要使用完整的 if
语句。
package main
import "fmt"
func main() {
if 7%2 == 0 {
fmt.Println("7 is even")
} else {
fmt.Println("7 is odd")
}
if 8%4 == 0 {
fmt.Println("8 is divisible by 4")
}
if num := 9; num < 0 {
fmt.Println(num, "is negative")
} else if num < 10 {
fmt.Println(num, "has 1 digit")
} else {
fmt.Println(num, "has multiple digits")
}
}
swith分支结构
switch 是多分支情况时快捷的条件语句。
package main
import (
"fmt"
"time"
)
func main() {
i := 2
fmt.Print("write ", i, " as ")
switch i {
case 1:
fmt.Println("one")
case 2:
fmt.Println("two")
case 3:
fmt.Println("three")
}
switch time.Now().Weekday() {
case time.Saturday, time.Sunday:
fmt.Println("It's the weekend")
default:
fmt.Println("It's a weekday")
}
/*不带表达式的 switch 是实现 if/else 逻辑的另一种方式。
这里还展示了 case 表达式也可以不使用常量。*/
t := time.Now()
switch {
case t.Hour() < 12:
fmt.Println("It's before noon")
default:
fmt.Println("It's after noon")
}
/*类型开关 (type switch) 比较类型而非值。可以用来发现一个接口值的类型。
在这个例子中,变量 t 在每个分支中会有相应的类型。*/
whatAmI := func(i interface{}) {
switch t := i.(type) {
case bool:
fmt.Println("I'm a bool")
case int:
fmt.Println("I'm an int")
default:
fmt.Printf("Don't know type %T\n", t)
}
}
whatAmI(true)
whatAmI(1)
whatAmI("hey")
}
数组
在 Go 中,数组 是一个具有编号且长度固定的元素序列。
可以使用 array[index] = value
语法来设置数组指定位置的值, 或者用 array[index]
得到值。
内置函数 len
可以返回数组的长度。
package main
import "fmt"
func main() {
/*我们创建了一个刚好可以存放 5 个 int 元素的数组 a。
元素的类型和长度都是数组类型的一部分。 数组默认值是零值,
对于 int 数组来说,元素的零值是 0。*/
var a [5]int
fmt.Println("emp:", a)
a[4] = 100
fmt.Println("set:", a)
fmt.Println("get:", a[4])
fmt.Println("len:", len(a))
b := [5]int{1, 2, 3, 4, 5}
fmt.Println("dcl:", b)
var twoD [2][3]int
for i := 0; i < 2; i++ {
for j := 0; j < 3; j++ {
twoD[i][j] = i + j
}
}
fmt.Println("2d: ", twoD)
}
切片
在 Go 程序中,相较于数组,用得更多的是 切片(slice)。我们接着来看切片。
Slice 是 Go 中一个重要的数据类型,它提供了比数组更强大的序列交互方式。
与数组不同,slice 的类型仅由它所包含的元素的类型决定(与元素个数无关)。 要创建一个长度不为 0 的空 slice,需要使用内建函数 make
。
可以和数组一样设置和得到值
除了基本操作外,slice 支持比数组更丰富的操作。比如 slice 支持内建函数 append
, 该函数会返回一个包含了一个或者多个新值的 slice。 注意由于 append
可能返回一个新的 slice,我们需要接收其返回值。
深拷贝:拷贝的是数据本身。值类型的数据默认都是深拷贝:
arry,int,float,string,struct,bool
slice 还可以 copy
。这里我们创建一个空的和 s
有相同长度的 slice——c
, 然后将 s
复制给 c
浅拷贝:拷贝的是数据 地址。导致多个变量指向同一个内存
引用类型的数据,默认都是浅拷贝:slice,map
切片是引用类型数据,直接拷贝的地址
slice 支持通过 slice[low:high]
语法进行“切片”操作。
Slice 可以组成多维数据结构。内部的 slice 长度可以不一致,这一点和多维数组不同。
package main
import "fmt"
func main() {
//我们创建了一个长度为 3 的 string 类型的 slice(初始值为零值)。
s := make([]string, 3)
fmt.Println("emp:", s)
s[0] = "a"
s[1] = "b"
s[2] = "c"
fmt.Println("set:", s)
fmt.Println("get:", s[2])
fmt.Println("len:", len(s))
s = append(s, "d")
s = append(s, "e", "f")
fmt.Println("apd:", s)
c := make([]string, len(s))
copy(c, s)
fmt.Println("cpy:", c)
l := s[2:5] //可以得到一个包含元素 s[2]、s[3] 和 s[4] 的 slice。
fmt.Println("sl1:", l)
l = s[:5] //这个 slice 包含从 s[0] 到 s[5](不包含 5)的元素。
fmt.Println("sl2:", l)
l = s[2:]//这个 slice 包含从 s[2](包含 2)之后的元素。
fmt.Println("sl3:", l)
t := []string{"g", "h", "i"}
fmt.Println("dcl:", t)
twoD := make([][]int, 3)
for i := 0; i < 3; i++ {
innerLen := i + 1
twoD[i] = make([]int, innerLen)
for j := 0; j < innerLen; j++ {
twoD[i][j] = i + j
}
}
fmt.Println("2d: ", twoD)
}
Map
map 是 Go 内建的关联数据类型 (在一些其他的语言中也被称为 哈希(hash) 或者 字典(dict) )。
map:映射,是一种专门用来存储键值对的集合,属于引用类型
存储特点:A:存储的是无序的键值对
B:键不能重复,并且与value值一一对应
map中的key不能重复,如果重复,那么新的value值会覆盖原来的,不会报错
要创建一个空 map,需要使用内建函数 make
:make(map[key-type]val-type)
。
使用典型的 name[key] = val
语法来设置键值对。
使用 name[key]
来获取一个键的值。
内建函数 len
可以返回一个 map 的键值对数量。
内建函数 delete
可以从一个 map 中移除键值对。
当从一个 map 中取值时,还有可以选择是否接收的第二个返回值,该值表明了 map 中是否存在这个键。 这可以用来消除 键不存在
和 键的值为零值
产生的歧义, 例如 0
和 ""
。这里我们不需要值,所以用 空白标识符(blank identifier) _ 将其忽略。
package main
import "fmt"
func main() {
m := make(map[string]int)
m["k1"] = 7
m["k2"] = 13
fmt.Println("map:", m)
v1 := m["k1"]
fmt.Println("v1: ", v1)
fmt.Println("len:", len(m))
delete(m, "k2")
fmt.Println("map:", m)
_, prs := m["k2"]
fmt.Println("prs:", prs)
n := map[string]int{"foo": 1, "bar": 2}
fmt.Println("map:", n)
}
range遍历
range 用于迭代各种各样的数据结构。 让我们来看看如何在我们已经学过的数据结构上使用 range
。
range
在数组和 slice 中提供对每项的索引和值的访问。 上面我们不需要索引,所以我们使用 空白标识符 _
将其忽略。 实际上,我们有时候是需要这个索引的。
range
在字符串中迭代 unicode 码点(code point)。 第一个返回值是字符的起始字节位置,然后第二个是字符本身。
package main
import "fmt"
func main() {
nums := []int{2, 3, 4}
sum := 0
for _, num := range nums {
sum += num
}
fmt.Println("sum:", sum)
for i, num := range nums {
if num == 3 {
fmt.Println("index:", i)
}
}
kvs := map[string]string{"a": "apple", "b": "banana"}
for k, v := range kvs {
fmt.Printf("%s -> %s\n", k, v)
}
for k := range kvs {
fmt.Println("key:", k)
}
/*range在字符串迭代Unicode,第一个值是字符的起始字节位置(ASCII码),依次是下个字符的ASCII码*/
for i, c := range "go" {
fmt.Println(i, c)
}
}
函数 是 Go 的核心。我们将通过一些不同的例子来进行学习它。
Go 需要明确的 return,也就是说,它不会自动 return 最后一个表达式的值
当多个连续的参数为同样类型时, 可以仅声明最后一个参数的类型,忽略之前相同类型参数的类型声明。
Go 函数还有很多其他的特性。 其中一个就是多值返回。
package main
import "fmt"
func plus(a int, b int) int {
return a + b
}
func plusPlus(a, b, c int) int {
return a + b + c
}
func main() {
res := plus(1, 2)
fmt.Println("1+2 =", res)
res = plusPlus(1, 2, 3)
fmt.Println("1+2+3 =", res)
}
多返回值
Go 原生支持 多返回值。 这个特性在 Go 语言中经常用到,例如用来同时返回一个函数的结果和错误信息。
package main
import "fmt"
func vals() (int, int) {
return 3, 7
}
func main() {
a, b := vals()
fmt.Println(a)
fmt.Println(b)
_, c := vals()
fmt.Println(c)
}
变参函数
可变参数函数。 在调用时可以传递任意数量的参数。 例如,fmt.Println
就是一个常见的变参函数。
package main
import "fmt"
\\ 函数接受任意数量的 int 作为参数。
func sum(nums ...int) {
fmt.Print(nums, " ")
total := 0
for _, num := range nums {
total += num
}
fmt.Println(total)
}
func main() {
sum(1, 2)
sum(1, 2, 3)
\\如果你有一个含有多个值的 slice,想把它们作为参数使用, 你需要这样调用 func(slice...)。
nums := []int{1, 2, 3, 4}
sum(nums...)
}
闭包
Go 支持匿名函数, 并能用其构造 闭包。 匿名函数在你想定义一个不需要命名的内联函数时是很实用的。
intSeq
函数返回一个在其函数体内定义的匿名函数。 返回的函数使用闭包的方式 隐藏 变量 i
。 返回的函数 隐藏 变量 i
以形成闭包。
调用 intSeq
函数,将返回值(一个函数)赋给 nextInt
。 这个函数的值包含了自己的值 i
,这样在每次调用 nextInt
时,都会更新 i
的值。
闭包=外部函数的变量+内部函数调用变量
package main
import "fmt"
func intSeq() func() int {
i := 0
return func() int { //匿名函数
i++
return i //闭包
}
}
func main() {
nextInt := intSeq()
fmt.Println(nextInt())
fmt.Println(nextInt())
fmt.Println(nextInt())
newInts := intSeq()
fmt.Println(newInts())
}
递归
Go 支持 递归。
package main
import "fmt"
func fact(n int) int {
if n == 0 {
return 1
}
return n * fact(n-1)
}
func main() {
fmt.Println(fact(7))
}
指针
Go 支持 指针, 允许在程序中通过 引用传递
来传递值和数据结构。
package main
import "fmt"
/*通过两个函数:zeroval 和 zeroptr 来比较 指针 和 值。 zeroval 有一个 int 型参数,所以使用值传递。 zeroval 将从调用它的那个函数中得到一个实参的拷贝:ival。*/
func zeroval(ival int) {
ival = 0
}
/*zeroptr 有一个和上面不同的参数:*int,这意味着它使用了 int 指针。 紧接着,函数体内的 *iptr 会 解引用 这个指针,从它的内存地址得到这个地址当前对应的值。 对解引用的指针赋值,会改变这个指针引用的真实地址的值。*/
func zeroptr(iptr *int) {
*iptr = 0
}
func main() {
i := 1
fmt.Println("initial:", i)
/*zeroval 在 main 函数中不能改变 i 的值, 但是 zeroptr 可以,因为它有这个变量的内存地址的引用。*/
zeroval(i)
fmt.Println("zeroval:", i)
zeroptr(&i)
fmt.Println("zeroptr:", i)
fmt.Println("pointer:", &i)
}
//输出结果:
initial: 1
zeroval: 1
zeroptr: 0
pointer: 0x42131100
结构体
Go 的结构体(struct) 是带类型的字段(fields)集合。 这在组织数据时非常有用。
可以在初始化一个结构体元素时指定字段名字。
省略的字段将被初始化为零值。
&
前缀生成一个结构体指针。
使用.
来访问结构体字段。
也可以对结构体指针使用.
- 指针会被自动解引用。
结构体是可变(mutable)的。
package main
import "fmt"
type person struct {
name string
age int
}
func main() {
fmt.Println(person{"Bob", 20})
fmt.Println(person{name: "Alice", age: 30})
fmt.Println(person{name: "Fred"})
fmt.Println(&person{"tom",25})
fmt.Println(&person{name: "Ann", age: 40})
s := person{name: "Sean", age: 50}
fmt.Println(s.name)
sp := &s
fmt.Println(sp.age)
sp.age = 51
fmt.Println(sp.age)
}
方法
Go 支持为结构体类型定义方法(methods) 。
package main
import "fmt"
type rect struct {
width, height int
}
func (r *rect) area() int { //area 是一个拥有 *rect 类型接收器(receiver)的方法。
return r.width * r.height
}
func (r rect) perim() int { //可以为值类型或者指针类型的接收者定义方法。 这是一个值类型接收者的例子。
return 2*r.width + 2*r.height
}
func main() {
r := rect{width: 10, height: 5}
fmt.Println("area: ", r.area())
fmt.Println("perim:", r.perim())
/*调用方法时,Go 会自动处理值和指针之间的转换。 想要避免在调用方法时产生一个拷贝,或者想让方法可以修改接受结构体的值, 你都可以使用指针来调用方法。*/
rp := &r
fmt.Println("area: ", rp.area())
fmt.Println("perim:", rp.perim())
}
接口
方法签名的集合叫做:接口(Interfaces)。
package main
import (
"fmt"
"math"
)
type geometry interface {
area() float64
perim() float64
}
type rect struct {
width, height float64
}
type circle struct {
radius float64
}
/*在 Go 中实现一个接口,我们只需要实现接口中的所有方法。 这里我们为 rect 实现了 geometry 接口。*/
func (r rect) area() float64 {
return r.width * r.height
}
func (r rect) perim() float64 {
return 2*r.width + 2*r.height
}
func (c circle) area() float64 {
return math.Pi * c.radius * c.radius
}
func (c circle) perim() float64 {
return 2 * math.Pi * c.radius
}
/*如果一个变量实现了某个接口,我们就可以调用指定接口中的方法。 这儿有一个通用的 measure 函数,我们可以通过它来使用所有的 geometry。*/
func measure(g geometry) {
fmt.Println(g)
fmt.Println(g.area())
fmt.Println(g.perim())
}
func main() {
r := rect{width: 3, height: 4}
c := circle{radius: 5}
/*结构体类型 circle 和 rect 都实现了 geometry 接口, 所以我们可以将其实例作为 measure 的参数。*/
measure(r)
measure(c)
}
错误处理
符合 Go 语言习惯的做法是使用一个独立、明确的返回值来传递错误信息。 这与 Java、Ruby 使用的异常(exception) 以及在 C 语言中有时用到的重载 (overloaded) 的单返回/错误值有着明显的不同。 Go 语言的处理方式能清楚的知道哪个函数返回了错误,并使用跟其他(无异常处理的)语言类似的方式来处理错误。
错误通常是最后一个返回值并且是 error
类型,它是一个内建的接口。
package main
import (
"errors"
"fmt"
)
func f1(arg int) (int, error) {
if arg == 42 {
return -1, errors.New("can't work with 42") //errors.New 使用给定的错误信息构造一个基本的 error 值。
}
return arg + 3, nil //返回错误值为 nil 代表没有错误。
}
/*可以通过实现 Error() 方法来自定义 error 类型。 这里使用自定义错误类型来表示上面例子中的参数错误。
使用 &argError 语法来建立一个新的结构体, 并提供了 arg 和 prob 两个字段的值。*/
type argError struct {
arg int
prob string
}
func (e *argError) Error() string {
return fmt.Sprintf("%d - %s", e.arg, e.prob)
}
func f2(arg int) (int, error) {
if arg == 42 {
return -1, &argError{arg, "can't work with it"}
}
return arg + 3, nil
}
func main() {
/* 注意,在 if 的同一行进行错误检查,是 Go 代码中的一种常见用法。*/
for _, i := range []int{7, 42} {
if r, e := f1(i); e != nil {
fmt.Println("f1 failed:", e)
} else {
fmt.Println("f1 worked:", r)
}
}
for _, i := range []int{7, 42} {
if r, e := f2(i); e != nil {
fmt.Println("f2 failed:", e)
} else {
fmt.Println("f2 worked:", r)
}
}
_, e := f2(42)
if ae, ok := e.(*argError); ok {
fmt.Println(ae.arg)
fmt.Println(ae.prob)
}
}
你想在程序中使用自定义错误类型的数据, 你需要通过类型断言来得到这个自定义错误类型的实例。
协程
协程(goroutine) 是轻量级的执行线程。
package main
import (
"fmt"
"time"
)
/*有一个函数叫做 f(s)。 我们一般会这样 同步地 调用它*/
func f(from string) {
for i := 0; i < 3; i++ {
fmt.Println(from, ":", i) //
}
}
func main() {
f("direct")
go f("goroutine") //使用 go f(s) 在一个协程中调用这个函数。 这个新的 Go 协程将会 并发地 执行这个函数。
go func(msg string) {//匿名函数启动一个协程。
fmt.Println(msg)
}("going")
time.Sleep(time.Second)
fmt.Println("done")
}
我们运行这个程序时,首先会看到阻塞式调用的输出,然后是两个协程的交替输出。 这种交替的情况表示 Go runtime 是以并发的方式运行协程的。
通道
通道(channels) 是连接多个协程的管道。 你可以从一个协程将值发送到通道,然后在另一个协程中接收。
package main
import "fmt"
func main() {
messages := make(chan string) //使用 make(chan val-type) 创建一个新的通道。 通道类型就是他们需要传递值的类型。
/*使用 channel <- 语法 发送 一个新的值到通道中。 这里我们在一个新的协程中发送 "ping" 到上面创建的 messages 通道中。*/
go func() { messages <- "ping" }()
/*使用 <-channel 语法从通道中 接收 一个值。 这里我们会收到在上面发送的 "ping" 消息并将其打印出来。*/
msg := <-messages
/*通过通道, 成功的将消息 "ping" 从一个协程传送到了另一个协程中*/
fmt.Println(msg)
}
默认发送和接收操作是阻塞的,直到发送方和接收方都就绪。 这个特性允许我们,不使用任何其它的同步操作, 就可以在程序结尾处等待消息 "ping"
。
通道缓冲
默认情况下,通道是 无缓冲 的,这意味着只有对应的接收(<- chan
) 通道准备好接收时,才允许进行发送(chan <-
)。 有缓冲通道 允许在没有对应接收者的情况下,缓存一定数量的值。
package main
import "fmt"
func main() {
//make 了一个字符串通道,最多允许缓存 2 个值
messages := make(chan string, 2)
messages <- "buffered"
messages <- "channel"
fmt.Println(<-messages)
fmt.Println(<-messages)
}
由于此通道是有缓冲的, 因此我们可以将这些值发送到通道中,而无需并发的接收。
通道同步
可以使用通道来同步协程之间的执行状态。 这儿有一个例子,使用阻塞接收的方式,实现了等待另一个协程完成。 如果需要等待多个协程,WaitGroup 是一个更好的选择。