Go 是一门编译型,具有静态类型和类 C 语言语法的语言,并且有垃圾回收(GC)机制。
变量声明赋值
格式 : var + 变量名 + 变量类型
var age int
age = 1000
简写为
var age int = 9000
短变量声明符::=,它可以自动推断变量类型。
var age := 9000
同时声明多个变量
name,age := "张三",18
多个变量同时声明时,只要其中有一个变量是新的,就可以继续使用 :=,对于已声明类型的变量,只能赋值相同类型的值。
注意 :
- 在相同作用域下,同一个变量不能被声明两次。
- 不允许在程序中拥有未使用的变量。
import的包未使用也会导致编译失败。
变量类型
按类别区分几种类型:布尔型,数字型,字符串型,派生类型。
派生类型可分为:
-
指针类型:
pointer -
数组类型
-
结构化类型:
struct -
Map类型
-
切片类型
-
接口类型:
interface -
函数类型
-
channel类型
运行代码
命令:go run 文件名,它包含了 编译 和 执行 。它使用一个临时目录来构建程序,执行完然后清理掉目录。
加上 --work 参数可以查看临时文件的位置。
文件编译命令:go build 文件名,将会产生一个可执行文件,你可以运行它。
开发环境时,以上两个命令时都可以使用。正式环境时,使用 go build 产生的二进制文件,并执行它。
导入包
import 关键字被用于去声明文件中代码要使用的包。
内置函数 :
len:返回字符串的长度或返回字典值的长度或返回数组的元素的数量。
无网环境,获取本地文档
godoc -http=:6060
函数声明
格式:func + 函数名称(参数+参数类型) + 返回值类型
以下三个函数:一个没有返回值,一个有一个返回值,一个有两个返回值。
func log(message string){
}
func add(a int, b int) int{
}
func power(name string) (int,bool){
}
函数返回值赋值给空白字符 _
示例:
_,exists := power("zhangsan")
if exists == false {
// 错误情况处理
}
参数有相同类型,可以使用以下简洁语法
func add(a, b int) int{
}
结构体
Go 不像 Java 等语言是一门面向对象的语言,当然也没有重载和多态等和面相对象相关的概念。
Go 所具备的结构体概念,可以将一些方法和结构体关联。
定义结构体
格式: type + 结构体名称 + strcut
举例:
type Saiyan struct {
Name string
Power int
}
创建结构体的值:
goku := Saiyan {
Name:"goku",
Power:900001,
}
不写字段名,依赖字段顺序去初始化结构体
goku := Saiyan{
"goku",
900001,
}
注意: 结构末尾的 , 是必须的。
Go 中传递参数到函数是镜像赋值,运行以下程序运行结果是 90001。因为这里只是修改 原始值 goku 的赋值版本,而不是它本身。
func main(){
// go 传递参数到函数是镜像赋值
goku := Saiyan{
"Goku",
90001,
}
Super(goku)
fmt.Println(goku.Power)
}
func Super(s Saiyan){
s.Power += 10000
}
如何修改 goku 本身呢,需要传递指针到函数内,具体示例如下:
func main(){
goku := &Saiyan{"Goku",90001}
Super(goku)
fmt.Println(goku.Power)
}
func Super(s *Saiyan){
s.Power += 10000
}
注意: 这里修改了两个地方。& :取地址操作符,用于获取值的地址。修改了 Super 参数的期望类型。它之前期望是一个 Saiyan 类型,现在它期望的是一个地址类型 *Saiyan。 *X :指向类型 X 值的指针。
指针真正价值在于能够分享它所指向的值。
构造器
结构体没有构造器。但是可以创建一个返回所期望类型的实例(类似于工厂)。
func NewSaiyan(name string,power int) Saiyan{
return Saiyan{
Name:name,
Power:power,
}
}
New : 即使 Go 中没有构造函数,却有一个内置的 New 函数 ,使用它来分配类型所需要的内存。作用等同于 &X{}。
goku := new(Saiyan)
goku.Name = "Goku"
goku.Power = 1000
等同于
goku:= &Saiyan{"Goku",1000}
不过推荐使用第一种方式,看起来更具备可读性。
组合
Go 支持组合,这是将一个结构包含进另外一个结构的行为。举例:
type Person struct {
Name string
}
func (p *Person) Introduce() {
fmt.Printf("Hi, I'm %s\n", p.Name)
}
type Saiyan struct {
*Person
Power int
}
func main() {
goku := &Saiyan{
Person: &Person{"Goku"},
Power: 9001,
}
goku.Introduce()
fmt.Println(goku.Name)
// 等同于
fmt.Println(goku.Person.Name)
}
数组 Arrays
声明 : var + 数组名[数组长度] + 类型
举例:
var scores [10]int
scores[0] = 123
初始化数组指定值
scores := [4]int{1,2,3,4}
注意点:
-
数组长度是固定的。
-
声明一个数组需要指定它的长度,指定长度后不可以改变。
声明数组示例
-
len函数获取数组的长度。
遍历数组:range 在遍历迭代中使用。
for index,value := range scores{
// to do something
}
数组非常高效但是十分死板。很多时候,事先我们并不知道数组的长度,针对这个情况,切片 slices 就出来了。
切片 Slice
几种创建切片的方式
-
创建
slice,用于当元素数量未知时与append连接。var names []string -
事先知道数组中的值的时候,可以使用以下方式声明。和数组有点类似,但是不需要在方括号内声明长度。
scores := []int{1,23,6,4,5} -
使用
make函数创建切片,格式:make([]类型,长度,容量即用来指定预留的空间长度)。当你想要写入切片具体的索引时,这种方式很有用。scores := make([]int,10) -
创建一个初始容量的
slice,我们大概知道元素的容量时很有用。scores := make([]int,0,10)创建一个切片具体来说,我们必须要为一个底层数组分配一段内存,同时初始化这个切片的长度和容量。
当我们为
slice分配内存的时候,尽可能预估到slice的最大长度,通过make传递第三个参数为slice预留好内存空间,这样可以避免二次分配内存带来的开销,极大地提高效率。
扩展切片:通过关键字 append 来实现
func main(){
scores := make([]int,0,10)
scores = append(scores,5)
fmt.Println(scores)
}
追加一个长度为 0 的切片将会设置为第一个元素,当我们想设置索引为 7 的索引值,可以重新切片:
func main(){
scores := make([]int,0,10)
scores = scores[0:8]
scores[7] = 123
fmt.Println(scores)
}
调整切片大小的最大范围是此切片的容量。
Go 使用 2x 算法来增加数组的长度。
举例
func main() {
scores := make([]int, 0, 5)
c := cap(scores)
fmt.Println(c)
for i := 0; i < 25; i++ {
scores = append(scores, i)
if cap(scores) != c {
c = cap(scores)
fmt.Println(c)
}
}
}
// 结果:5 10 20 40
对于编译器而言,是追加一个值到已经有五个值的切片:
func main() {
scores := make([]int, 5)
scores = append(scores, 12342)
fmt.Println(scores)
}
// 结果:[0,0,0,0,12342]
[X:] 从 X 到尾,[:X] 从开始到 X。Go 语言不支持负数索引。
移除切片中最后一个元素
scores := []int{1,2,3,4,5}
scores = scores[:len(scores)-1]
生成 1000 以内的随机整数
import("math/rand")
rand.Int31n(1000)
数组正序排序
import("sort")
scores := []int{1,3,4,232,}
sort.Ints(scores)
内置函数:copy
func main() {
// scores := make([]int, 100)
scores := []int{0,1,2,3444,43,343}
for i := 0; i < 100; i++ {
scores[i] = int(rand.Int31n(1000))
}
sort.Ints(scores)
worst := make([]int, 5)
copy(worst,scores[5:100])
fmt.Println(worst)
}
映射 Map
Go 语言中的映射类似于其它语言的 hash 表。它们的工作方式就是:可以定义键和值,可以获取,设置和删除其中的值。
创建映射:使用 make 创建
lookup := make(map[string]int)
获取映射键的数量
len(lookup)
删除映射中某个键值。没有返回值,可以再不存在的键值上调用。
delete(lookup,"power")
映射是动态变化的。设置第二个参数到 make 方法中可以设置一个初始化大小。
lookup := make(map[string]int,100)
如果提前知道映射会有多少键值,那么设置初始化大小可以改善性能。
将映射设置为结构体的字段时,可以这样定义:
type Saiyan struct{
Name string
Friends map[string]*Saiyan
}
初始化上述结构体:
goku := &Saiyan{
Name:"goku",
Friends:make(map["goku"]*Saiyan),
}
复合方式
lookup :=map[string]int{
"goku":9000,
"meme":23442,
}
迭代映射
使用 for 和 range 关键字迭代映射
for key,value := range lookup {
}
迭代映射没有顺序。每次迭代映射将会随机返回键值对。
包管理
**包外可见性:**如果一个类型或函数名称首字母以大写开始的话,它就具备了包外可见性。如果以一个小写字母开始,它就不可以。
**结构体字段:**如果一个字段以小写字母命名,则只有包内的代码可以访问他们。
go get 库名:获取第三方库。
**依赖管理:**如果我们在一个项目内使用 go get , 它将浏览所有的文件,查找所有 improts 的第三款库并下载他们。某种程度上,我们的代码变成了 Gemfile 和 package.json'。
go get -u:它将更新所有的包。
go get -u FULL_PACKAGE_NAME:更新一个具体的包。
不足之处:go get 没有办法指定版本。
这个时候需要引入第三方的依赖管理工具:goop 和 gode 。
接口
接口是定义了合约但并没有实现类型。接口有助于将代码和特定的实现分离。
举例:
type Logger interface{
Log(message string)
}
type SqlLogger struct { ... }
type ConsoleLogger struct { ... }
错误处理
Go 首选的错误处理方式是返回值而不是异常。
strconv.Atoi 函数:它可以接受一个字符串并将它转换成一个整数。
你可以创建自己的错误类型,但是必须实现内建 error 接口的契约。
Go 标准库中有一个使用 error 变量的通用模式。比如 io 包中有一个 EOF 变量它是这样定义的:
var EOF = errors.New("EOF")
Go 有两个函数 panic 和 recover ,但是很少使用。
panic: 抛出异常。recover: 类似于catch。
关键字 Defer
尽管 Go 有自带的垃圾回收器,但是有一些显式资源还是需要我们手动去释放。比如使用完文件后需要 close 它们。
当一个函数有多个返回点,Go 给出的解决方案使用 defer 关键字。
无论什么情况,在函数返回之后, defer 将被执行。
应用场景举例:
- 函数退出时的日志记录。
- 资源管理。
Go 语言风格
当你在一个项目内,你可以运用格式化规则到这个项目及其子目录。
go fmt ./....
它不仅缩进你的代码,也对齐了声明的字段和按字母书序导入。
空接口和转化
Go 没有继承,不过有一个没有任何方法的空接口 interface{}。
将一个接口变量转化为显式的类型,可以用 .(TYPE) :
return a.(int)
类型转换
switch a.(type) {
case int:
fmt.Printf("a is now an int and equals %d\n", a)
case bool, string:
// ...
default:
// ...
}
字符串和字节数组
字符串和字节数组关系是紧密相关的,它们之间可以轻松互转。
stra := "this is str"
byts := []byte(stra)
strb = string(byts)
当你使用 []byte()X 或 string(X) 时,实际上是创建了数据的副本。因为字符串是不可变的。
函数类型
函数是一种类型。它可以用在任何地方,作为字段类型,参数或者返回值。
package main
import "fmt"
type Add func(a int, b int) int
func main() {
fmt.Println(process(func(a int, b int)int {
return a + b
}))
}
func process(adder Add) int {
return adder(1, 5)
}
并发
Go是一门并发友好的语言,原因它提供了两种强大机制的简单语法:协程和通道。
Go 协程
协程类似于一个线程,但是由 go 而不是操作系统决定。在协程中运行的代码可以与其它代码同时运行。
如何开始协程?
使用关键字 go ,然后使用想要执行的函数。
注意:
- 协程易于创建而且开销很小。(
M:N线程模型:M个应用线程(协程)运行在N个系统操作线程上。一个协程的开销比操作系统线程相比少几KB) - 隐藏了映射和调度复杂性。我们只需要声明这段代码需要并发执行让
go自己去实现它。 - 主进程在退出前,协程才有机会执行。主进程退出前不会等待全部协程执行完毕,需要协调我们的代码。
同步
编写并发代码需要特别注意在哪里读取和写入一个值。
从变量中读取变量是唯一安全的并发处理变量的方式。
写操作必须保持同步,常用的操作还是互斥量 mutex 。互斥量序列化会锁住锁下的代码访问。
package main
import (
"fmt"
"time"
"sync"
)
var (
counter = 0
lock sync.Mutex
)
func main() {
for i := 0; i < 25; i++ {
go incr()
}
time.Sleep(time.Millisecond * 10)
}
func incr() {
lock.Lock()
defer lock.Unlock()
counter++
fmt.Println(counter)
}
读写互斥锁:主要提供了两种功能 锁定写入 和 锁定读取 。它的区别是允许多个同时读取,同时确保写入是独占的。在 Go 中 , RWMetux 就是读写互斥锁。
通道
为了解决协调并发代码,Go 提供了通道。
并发编程最大的调整源于 数据共享 。
一个通道和其它变量一样,都有一个类型。这个类型是在通道中 传递的数据 的类型 。
格式 : make(chan 类型, 长度)
举例:创建一个通道用于传递一个整数。
c := make(chan int)
通道只支持两种操作 接收 和 发送 。接收和发送操作是阻塞的。
往通道发送一个数据:
CHANNEL <- DATA
从通道接收一个数据:
VAR := <-CHANNEL
举例:
package main
import (
"fmt"
"time"
"math/rand"
)
type Worker struct {
id int
}
func main() {
c := make(chan int)
for i := 0; i < 5; i++ {
worker := &Worker{id: i}
go worker.process(c)
}
for {
// 往通道发送数据
c <- rand.Int()
time.Sleep(time.Millisecond * 50)
}
}
func (w *Worker) process(c chan int) {
for {
// 从通道接收数据
data := <-c
fmt.Printf("worker %d got %d \n", w.id, data)
}
}
我们不确定哪一个 worker 会接收到什么数据。但是 Go 可以保证 发送到通道的数据只会被一个接收器接收。
缓冲通道: 如果没有 worker 可用,我们想去临时存储数据在某些队列中。通道内建这种缓冲容量,当我们使用 make 创建通道的时候,可以设置通道的长度。缓冲通道不会增加容量,他们只提供待处理工作的队列,以及处理突然飙升的任务量的好方法。
c := make(chan int,100)
select: 主要目的是管理多个通道, select 将阻塞直到第一个通道可用。当没有通道可用,如果提供了 default ,它就会执行。
for {
select {
case c <- rand.Int() :
// 执行代码
default:
// 这里可以留空静默删除数据或做其他操作
fmt.Println("dropped")
}
}
超时: 为了阻塞最长时间,我们可以使用 time.After() 函数。
selcet {
case c <-rand.Int():
case <-time.After(time.Millisecond * 100):
fmt.Println("time out")
}
time.Millisecond * 50
time.After() 返回了一个通道 所以我们在 select 中可以使用它。time.After 是一个 chan time.Time 类型的通道。
select 的工作方式是相同的:
- 第一个可用的通道被选中。
- 当有多个通道时,随机选择一个通道使用。
- 如果没有通道可用,
default将会被执行。 - 如果没有
default,select将会被阻塞。
Goroutines 有效地帮我们抽象了需要并发执行的代码。通道帮助消除数据共享时共享数据可能发生的一些严重错误。