安装
- 下载
- 配置工作空间
- export GOPATH=~/Documents/code/workspace_go
- export PATH=$PATH:$GOPATH/bin
- 工作空间workspace_go
- mkdir -p $GOPATH/src/github.com/mamian
初识Go语言
语言特性
自动垃圾回收
更丰富的内置类型
- map
- slice:可动态增长的数组
函数多返回值
错误处理
- defer:不管程序是否出现异常,均 在函数退出时自动执行相关代码
- panic
- recover
匿名函数和闭包
f := func(x, y int) int {
return x + y
}
类型struct和接口interface
- 不支持继承和重载,而只是支持了最基本的类型组合功能。
并发编程
- 使用goroutine而不是用操作系统的并发机制。
- 使用消息传递来共享内存而不是使用共享内存来通信。
- Go语言通过系统的线程来多派这些函数的执行,使得每个用go关键字执行的函数可以行成为一个单位协程。当一个协程阻塞的时候,调度器就会自动把其他协程排到另外的线程中去执行,从而实现了程序无等并行化行。
- 跨goroutine通信:channel
- 由于一个进程内创建的所有goroutine行在同一个内存地间中,因此如果不同的goroutine不得不去访问共享的内存变量,访问前应该先获取相应的读写。Go语言标准库中的sync包提供了完的读写功能。
反射
语言交互性
第一个Go程序
- 必须package main,但文件名不一定为main.go,必包含func main(){}方法。命令行传入的参数在os.Args变量中保存。如果需要支持命令行开关,可使用flag包。
go函数
func 函数名(参数列表)(返回值列表) { //函数体 }
- 函数返回时没有被明确赋值的返回值都会被设置为认值。
- 编译程序
- 直接运行:go run hello.go
- 编译后运行
- go build hello.go
- ./hello
- 日志
- fmt.Printf() fmt.Println()
- log
- GDB调试
- build出可执行文件后,直接gdb helloworld
顺序编程
变量
变量声明
var
var v1 int var v2 string var v3 [10]int //数组 var v4 []int //数组切片 var v5 struct { f int } var v6 *int //指针 var v7 map[string]int //map key为string value为int var v8 func(a int) int
公用var
var ( v1 int v2 string )
变量初始化
var v1 int = 10 //正确方式1
var v2 = 10 //正确方式2,编译器可自动推导v2类型
v3 := 10 //正确方式3,编译器可自动推导v3类型
- 出现在:=左侧的变量不应该是已经被声明过的,否则会导致编译错误
变量赋值
- 声明后赋值
- var v10 int
- v10 = 123
- 多重赋值
- 交换变量值:i,j = j,i
匿名变量
- 不用的返回值可用_代替。
常量
- 常量可以是数值类型(整型、浮点型、复数),布尔类型、字符串类型。
字面常量
- 无类型
- 只要这个常量在相应类型的值域范围内,就可以作为该类型的常量。
- 如-12可以赋值给int、uint、int32、int64、float32、float64、complex64、complex128等类型的变量。
常量定义
- 通过
const
关键字定义常量。- const Pi float64 = 3.14159265358979323846
- const (
size int64 = 1024
eof = -1
)
预定义常量
iota
const (//iota被重设为0 c0 = iota //c0 == 0 c1 = iota //c0 == 1 c2 = iota //c0 == 2 )
true、false
枚举
go中
没有enum
,可使用const实现枚举。const( Sunday = iota Monday Tuesday )
类型
- 对于常规的开发来说,用int和uint就可以了,没必要用int8之类明确指定长度的类型,以免导致移植困难。
- 基础类型
- bool
- int8、byte、int16、int、uint、uintptr
- float32、float64
- complex64、complex128
- string
- rune:字符
- error
- 复合类型
- pointer
- array
- slice:切片
- map:字典
- chan:通道
- struct:结构体
- interface
- bool变量只能用true、false,不支持0、1,不支持类型转换。
- int与int32在Go中为2种不同类型,不做自动类型转换,但可强制转换。
- 左移*、右移/、2变量异或^、1变量取反^、与&、或|
浮点数比较
:浮点数不是一种精确的表达方式,不可像整型那样直接用==来判断是否相等。import "math" //p为用户自定义的比较精度,如0.00001 func IsEqual(f1, f2, p float64) bool { return math.Fdim(f1, f2) < p }
- 字符串的内容可以用类似于数组下标的方式获取,但与数组不同,字符串的内容不能在初始化后被修改。
len(s)
获取字符串长度,x+y
拼接字符串,s[i]
取字符。字符串遍历
按字节遍历(有中文时不方遍)
n := len(str) for i := 0; i<n; i++ { }
按Unicode字符遍历
for i, ch := range str{ fmt.Println(i, ch) //ch类型为rune }
- 字符类型
- byte:代表UTF8字符串的单个字节的值
- rune:代表单个Unicode字符
- 数组
- 数组长度在定义后不可更改
- 数组长度:len(arr)
- 定义方法
- [32]byte //长度为32的数组,每个元素为一个字节
- [2*N] struct { x, y int32 } //复杂类型数组
- [1000]*float64 //指针数组
- [3][5]int //二维数组
- [2][2][2]float64 //等同于2)
- 数组为值类型:
所有值类型在赋值和作为参数传递时都产生一次复制动作
。函数内操作的只是所传入数组的一个副本。
- 数组切片
- 创建切片
- 基于数组创建切片
- var myArray [10]int = [10]int{1, 2, 3, 4, 5, 6, 7, 8, 9, 10}
- var mySlice []int = myArray[:5]
- 直接创建
make
- mySlice1 := make([]int, 5) //容量5,初始值0
- mySlice3 := []int{1, 2, 3, 4, 5} //创建并初始化
- 基于数组创建切片
- cap()为数组切片分配的空间大小,len()为数组切片中当前所存储的元素个数
- 向切片尾端添加元素
- mySlice = append(mySlice, 1, 2, 3)
- 将一个切片添加到另一切片尾端,注意不能少了
...
- mySlice2 := []int{8, 9, 10}
- mySlice = append(mySlice, mySlice2…)
- 基于切片创建新的切片
- oldSlice := []int{1, 2, 3, 4, 5}
- newSlice := oldSlice[:3]
- 切片间复制
- slice1 := []int{1, 2, 3, 4, 5}
- slice2 := []int{5, 4, 3}
- copy(slice2, slice1) //只会复制slice1的前3个元素到slice2中
- copy(slice1, slice2) //只会复制slice2的前3个元素到slice1的前3个位置
- 创建切片
- map
- 一堆键值对的未排序集合。
- 变量声明
- var myMap map[string] PersonInfo //PersonInfo为value的类型
- 创建
- myMap = make(map[string] PersonInfo)
- myMap = map[string] PersonInfo{
"1234": PersonInfo{"1", "Jack", "Room 101,..."}, }
- 元素赋值
- myMap[“1234”] = PersonInfo{“1”, “Jack”, “Room 101,…”}
- 元素删除
- delete(myMap, “1234”),key在map中可以不存在,但key本身不可为nil。
- 元素查找
- value, ok := myMap[“1234”]
- if ok { //找到了
- //处理找到的value
- }
流程控制
条件语句 if-else
选择语句 switch-case
- 条件表达式不限制为常量或者整数
- Go语言不需要用break来明确退出一个case
- 只有在case中明确 加fallthrough关键字,才会继续执行紧跟的下一个case;
循环语句 for
- Go中
没有while
- break可选择中止哪个标签处的循环
- continue
跳转语句 goto
函数
函数定义
package mymath
import "errors"
func Add(a int, b int) (ret int, err error) {
if a < 0 || b < 0 {
err= errors.New("Should be non-negative numbers!")
return
}
return a + b, nil //支持多重返回值
}
函数调用
- 小写字母开头的函数只在本包内可见,大写字母开头的函数才能被其他包使用。
使用函数
import "mymath" c := mymath.Add(1, 2)
不定参数
固定类型的不定参数
func myfunc(args ...int) { for _, arg := range args { fmt.Println(arg) } }
任意类型的不定参数
func Printf(format string, args ...interface{}) { }
多返回值
匿名函数与闭包
匿名函数后跟参数列表表示函数调用
func(ch chan int) { ch <- ACK } (reply_chan)
Go的匿名函数是一个闭包。
错误处理
error
defer
defer例子
srcFile, err := os.Open(src) if err != nil { return } defer srcFile.Close()
可使用匿名函数来defer
defer func() { //复杂的清理工作 } ()
一个函数中可以存在多个defer语句,因此需要注意的是,defer语句的调用是按先进后出的原则, 最后一个defer语句将最先被执行。只不过,当你需要为defer语句到底哪个先执行这种细节而烦恼的时候,说明你的代码结构可能需要调整一下了。
panic()与recover()
func panic(interface{})
- 函数执行过程中调用panic,函数立即停止执行,但defer语句还会正常执行,之后函数将返回到调用函数,并导致逐层向上执行panic流程,直至所属的goroutine中所有正在执行的函数被终止。
- 例子
- panic(404)
- panic(“network broken”)
- panic(Error(“file not exists”))
func recover() interface{}
- recover用于终止错误处理流程。一般recover在defer函数中执行以有效
截取
错误处理流程。goroutine发生异常后没有调用recover的话会导致goroutine所属进程打印异常信息后直接退出。
- recover用于终止错误处理流程。一般recover在defer函数中执行以有效
面向对象编程
类型系统
- 在Go语言中,你可以给任意类型(包括内置类型,但不包括指针类型)添加相应的方法。
- Go语言和C语言一样,类型都是基于值传递的。要想修改变量的值,只能传递指针。
值语义和引用语义
- Go语言中的大多数类型都基于值语义:byte、int、bool、float32、string、array、struct、pointer
- 数组也是值引用
- var a = [3]int{1,2,3}
- var b = a
- 修改b数组对a没有任何影响
- 数组赋值时,
将内容完整复制
。
- 数组也可使用引用赋值
- var b = &a
- 修改b后a也同步修改
- 赋值后,b的类型是 *[3]int,而不是[3]int
- 数组切片内部是指向数组的指针,可改变所指向的数组元素。
- map本质上是一个字典指针;channel与map类似,本质上是一个指针。
结构体
type Rect struct {
x, y float64
width, height float64
}
func (r *Rect) Area() float64 {
return r.width * r.height
}
初始化
- 在Go语言中,未进行显式初始化的变量都会被初始化为该类型的零值,例如bool类型的零值为false,int类型的零值为0,string类型的零值为空字符串。
- rect1 := new(Rect)
- rect2 := &Rect{}
- rect3 := &Rect{0, 0, 100, 200}
- rect4 := &Rect{width: 100, height: 200}
Go语言中没有构造函数的概念,对象的创建通常交由一个全局的创建函数来完成,以NewXXX来命名,表示“构造函数”。
func NewRect(x, y, width, height float64) *Rect { return &Rect{x, y, width, height} }
匿名组合
Base类实现Foo()、Bar()方法
type Base struct { Name string } func (base *Base) Foo() { ... } func (base *Base) Bar() { ... }
Foo类从Base类“继承”并改写Bar()方法
type Foo struct { Base ... }
func (foo *Foo) Bar() {
foo.Base.Bar()
...
}
可见性
- 类型的成员变量、方法
依靠大小写来实现可见性
。 - 可访问性是包一级而不是类型一级。即类型Rect的area()方法,同一个package的其他类型也可访问到。
接口
- goroutine和channel是支撑起Go语言的并发模型的基石,接口是Go语言整个类型系统的基石。
- Go语言中,一个类只需要实现了接口要求的所有函数,我们就说这个类实现了该接口。类中的方法可以多于接口中的方法。
接口赋值
- 将对象实例赋值给接口
- 对象实例需实现接口中的所有方法
- var b interfaceName = &a
- 将一个接口赋值给另一个接口
- 只要2接口拥有相同的方法列表(次序可不同),即为等同的,可相互赋值。
接口查询
查询对象实例是否实现了某个接口
var file1 Writer = ... if file5, ok := file1.(two.IStream); ok { ... }
类型查询
查询接口指向的对象实例的类型。
var v1 interface{} = ... switch v := v1.(type) { case int: //现在v的类型是 int case string: //现在v的类型是 string ... }
- 类型查询不常使用,常配合接口查询使用。
- 利用反射也可进行类型查询:reflect.TypeOf()
接口组合
- Go支持类型组合,也支持接口组合。
Any类型
- 当函数可以接受任意的对象实例时,我们会将其声明为interface{},任何对象实例都满足空接口interface{}。
并发编程
并发基础
- 多进程
- 多线程
- 基于回调的非阻塞/异步IO
- 通过事件驱动的方式使用异步IO,使服务器持续运转,且尽可能地少用线程,降低开销,它目前在Node.js中得到了很好的实践。但是使用这种模式,编程比多线程要复杂,因为它把流程做了分割,对于问题本身的反应不够自然。
- 协程
- 本质上是一种用户态线程,不需要操作系统来进行抢占式调度, 且在真正的实现中寄存于线程中。
- 线程间通信方式:
共享内存
。为保证共享内存有效性,采取了加锁来避免死锁或资源竞争。 消息传递
- 对线程间共享状态的各种操作都被封装在线程之间传递的消息中,这通常要求:发送消息时对状态进行复制,并且在消息传递的边界上交出这个状态的所有所有权。
- 由于需执行复制操作,大多消息传递的实现在性能上并不优越。但线程中的状态管理会更简单。
协程
- 线程和进程通常最多也不能超过1万个,而协程可轻松创建上百万个而不会导致系统资源衰间竭。
goroutine
- goroutine为Go中的协程,Go语言标准库提供的所有系统调用操作(当然也包括所有同步IO操作),都会让出CPU给其他goroutine。
- goroutine由Go运行时管理。
- 函数前加
go
关键字,则会在一个新的goroutine中并发执行。若有返回值,返回值会被丢弃
。 - main函数并不会等待其内的goroutine执行完毕,可能goroutine未来得及执行,main就退出了。
并发通信
- 不要通过共享内存来通信,而应该通过通信来共享内存。
- 消息机制:每个并发单元是自包含的、独立的个体,拥有自己的变量,不同并发单元间的变量不共享。
- 并发单元间的输入、输出只有一种,就是消息。
- Go消息通信机制为channel。
channel
- 使用channel在2个或多个goroutine间传递消息。channel为进程内部的通信方式,通过channel传对象与函数执行时的参数传递一样,也可传指针。跨进程通信则可用Socket或HTTP。
- channel为类型相关,即
一个channel只能传一种类型的值
。类型需在声明channel时指定。
声明、定义、读写
- var ch chan int
- value为channel类型的map
- var m map[string] chan bool
- 初始化channel
- ch := make(chan int)
- 数据写入channel
- ch <- value
- 向channel写入数据通常会导致程序阻塞,直到有其他goroutine从这个channel中读取数据。
- 从channel中读取数据
- value := <-ch
- 如果channel之前没有写入数据,那么从channel中读取数据也会导致程序阻塞,直到channel中被写入数据为止。
select
select {
case <-chan1:
//如果chan1成功读取到数据,则进行case处理语句
case chan2 <- 1:
//如果成功向chan2写入数据,则进行该case处理语句
default:
//如以上都没成功,则进入default处理流程
}
- select用于处理`异步IO`问题。
- 每个case必为一个IO操作:面向channel的操作。
缓冲机制
- 给channel带上缓冲,可达到消息队列的效果。
- c := make(chan int, 1024)
- 即使没有读取方,写入方也可以一直往channel里写入,在缓冲区被填完完之前都不会阻塞。
可使用range读取数据
for i := range c { fmt.Println("Received:", i) }
超时机制
- select只要其中一个case已经完成,程序就会继续往下执行,而不会考虑其他case的情况。
Go语言没有提供直接的超时处理机制,但可利用select机制实现超时处理。
//首先,我们实现并执行一个匿名的超时等待函数 timeout := make(chan bool, 1) go func() { time.Sleep(1e9) //待待1秒钟 timeout <- true }() //然后我们把timeout这个channel利用起来 select { case <-ch: //从ch中读取到数据 case <-timeout: //一直没有从ch中读取到数据,但从timeout中读取到了数据 }
channel的传递
单向channel
关闭channel
- close(ch)
- 判断channel是否关闭
- x, ok := <-ch
- ok为false表示ch已经被关闭
多核并行化
出让时间片
- 在goroutine中主动出让时间片给其他goroutine。
- runtine的Gosched()
同步
- 使成功地用channel来作为通信手段,还是避免不了多个goroutine之间共享数据的问题。所以提供了资源锁方案。
同步锁
- Go的sync包提供2种锁:sync.Mutex和sync.RWMutex。
- Mutex
- 一个goroutine获得Mutex后,其他goroutine只能等待这个goroutine释放该Mutex。
- RWMutex
- 读锁(RLock()):不可写,但多个goroutine可同时读。
- 写锁(Lock()):独占,阻止任何其他goroutine(无论读和写)
- Mutex
- 任何一个Lock()或RLock()均需要保证对应有Unlock()或RUnlock()调用与之对应,否则可能导致等待该锁的所有goroutine处于饥饿状态,甚至可能导致死锁。
全局唯一性操作
- 全局角度只运行一次的代码,可使用Once类型保证全局的唯一性操作。
网络编程
- IP层:Raw Socket
- TCP/UDP层
- HTTP、FTP、SMTP层
Socket编程
- 传统Socket编程步骤
- 建立Socket:socket()
- 绑定Socket:bind()
- 监听:listen()或连接connect()
- 接受连接:accept()
- 接收:receive()或发送send()
- Go只需要调用net.Dial()即可。
Dial()函数
- func Dial(net, addr string) (Conn, error)
- TCP链接
- conn, err := net.Dial(“tcp”, “192.168.0.10:2100”)
- UDP链接
- conn, err := net.Dial(“udp”, “192.168.0.12:975”)
- ICMP链接
- conn, err := net.Dial(“ip4:icmp”, “www.baidu.com”)
- conn, err := net.Dial(“ip4:1”, “10.0.0.3”)
- TCP链接
- 支持的协议
- tcp、tcp4、tcp6
- udp、udp4、udp6
- ip、ip4、ip6
- 建立连接后,conn.Write()发送数据,conn.Read()接收数据。
HTTP编程
HTTP客户端
http.Get()
resp, err := http.Get("http://example.com/") if err != nil { //处理错误... return } defer resp.Body.close() io.Copy(os.Stdout, resp.Body)
- func (c Client) Get(url string) (r Response, err error)
- 等价于http.DefaultClient.Get()
http.Post()
resp, err := http.Post("http://example.com/upload", "image/jpeg", &imageDataBuf) if err != nil { //处理错误 return } if resp.StatusCode != http.StatusOK { //处理错误 return } // ...
- func (c Client) Post(url string, bodyType string, body io.Reader) (r Response, err
error)
- func (c Client) Post(url string, bodyType string, body io.Reader) (r Response, err
http.PostForm()
resp, err := http.PostForm("http://example.com/posts",url.Values{"title":{"article title"}, "content": {"article body"}}) if err != nil { //处理错误 return } // ...
- 实现了标准编码格式为application/x-www-form-urlencoded的表单提交
- func (c Client) PostForm(url string, data url.Values) (r Response, err error)
- http.Head()
- resp, err := http.Head(“http://example.com/“)
- func (c Client) Head(url string) (r Response, err error)
(*http.Client).Do()
req, err := http.NewRequest("GET", "http://example.com", nil) // ... req.Header.Add("User-Agent", "Gobook Custom User-Agent") // ... client := &http.Client{ //... } resp, err := client.Do(req) // ...
- func (c Client) Do(req Request) (resp *Response, err error)
- http.Get()、http.Post()、http.PostForm()和http.Head()方法其实都是在http.DefaultClient的基础上进行调用的,比如http.Get()等价于http.DefaultClient.Get()。
自定义http.Client
type Client struct { //Transport用于确定HTTP请求的创建机制 //如果为空,将会使用DefaultTransport Transport RoundTripper //CheckRedirect定义重定向策略 //如果CheckRedirect不为空,客户端将在跟踪HTTP重定向前调用该函数 //两个参数req和via分别为即将发起的请求和已经发起的所有请求,最早的已发起请求在最前面。 //如果CheckRedirect返回错误,客户端将直接返回错误,不会再发起该请求 //如果CheckRedirect为空,Client将采用一种确认策略,将在10个连续请求后终止 CheckRedirect func(req *Request, via []*Request) error //如果Jar为空,Cookie将不会在请求中发送,并会在响应中被忽略 Jar CookieJar }
HTTP服务端
- 处理HTTP请求
- net/http 包提供的http.ListenAndServe()方法
- func ListenAndServe(addr string, handler Handler) error
- 处理HTTPS请求
- net/http 包还提供 http.ListenAndServeTLS()方法
- func ListenAndServeTLS(addr string, certFile string, keyFile string, handler Handler) error
- certFile对应SSL证书文件存放路径
- keyFile对应证书私钥文件路径。
- 如果证书是由证书颁发机构签署的,certFile参数指定的路径必须是存放在服务器上的经由CA认证过的SSL证书。
RPC编程
- RPC协议构建与TCP或UDP,或者是HTTP之上。
Go中的RPC支持与处理
- RPC服务端,可将一个对象注册为可访问的服务,之后该对象的公开方法就能够以远程的方式提供访问。一个 RPC服务端可以注册多个不同类型的对象,但不允许注册同一类型的多个对象。
- func (t T) MethodName(argType T1, replyType T2) error
JSON处理
编码为JSON格式
- func Marshal(v interface{}) ([]byte, error)
- b, err := json.Marshal(gobook)
解码JSON数据
- func Unmarshal(data []byte, v interface{}) error
- var book Book
- err := json.Unmarshal(b, &book)
解码未知结构的JSON数据
- var r interface{}
- err := json.Unmarshal(b, &r)
- r将会是一个键值对的map[string] interface{}结构
- gobook, ok := r.(map[string]interface{})
JSON的流式读写
- encoding/json 包还提供Decoder和Encoder两个类型,用于支持JSON数据的流式读写,并提供NewDecoder()和NewEncoder()两个函数来便于具体实现
- func NewDecoder(r io.Reader) *Decoder
- func NewEncoder(w io.Writer) *Encoder
网站开发
最简单的网站程序
package main
import (
"io"
"log"
"net/http"
)
func helloHandler(w http.ResponseWriter, r *http.Request){
io.WriteString(w, "Hello, world!")
}
func main() {
http.HandleFunc("/hello", helloHandler)
err := http.ListenAndServe(":8080", nil)
if err != nil {
log.Fatal("ListenAndServe: ", err.Error())
}
}
安全编程
工程管理
代码风格
命名
- 变量、常量、全 函数、结构、接口、方法:任何需要对外暴露的名字必须以大写字母开头,不需要对外暴露的则应该以小写字母开头。
- 拥护驼峰命名法而排斥下划线法。
排列
- 约定了代码块中花括号的明确摆放位置。
代码格式代
- go fmt hello.go
- 若执行 go fmt,则格式化当前目录下所有代码。
远程import
import (
"fmt"
"github.com/myteam/exp/crc32"
)
开发工具
进阶话题
反射
- 反射的代码可读性差,如非必要,不推荐使用。
Type与Value
var reader io.Reader reader = &MyReader{"a.txt"}
- Type:被反射的变量本身的类型信息
- Value:被反射的变量实例本身的信息
- Type为io.Reader,Value为MyReader{“a.txt”}
获取类型信息
package main import ( "fmt" "reflect" ) func main() { var x float64 = 3.4 fmt.Println("type:", reflect.TypeOf(x)) }
- reflect.TypeOf()
- v := reflect.ValueOf(x)、v.Type()、v.Float()
goroutine机理
协程
- 能够在单一的系统线程中模拟多个任务的并发执行。
- 在一个特定的时间,只有一个任务在执行,即并非真正地并行。
- 被动的任务调度方式,即任务没有主动抢占时间片的说法。当一个任务正在执行时,外部没有办法中止它。要进行任务切换,只能通过由该任务自身调用yield()来主动出让CPU使用权。
- 每个协程都有自己的堆栈和局部变量。
- 协程都包含3种运行状态:挂起、运行和停止。