入门
1、安装环境
- 源码包下载
- 环境安装
- GOROOT: 包含Go语言的安装根目录的路径
- GoPATh: 包含若干工作区目录的路径
- GOBIN :包含用于旋转Go程序生成的可执行文件的目录的路径
- 测试开发环境
2、永远的Hello World
package main
import (
"fmt"
"math/rand"
)
func main() {
fmt.Println("My favorite number is", rand.Intn(10))
}
每个 Go 程序都是由包构成的。golang中的表达式,加";", 和不加 都可以,建议是不加
程序从 main 包开始运行。
本程序通过导入路径 "fmt" 和 "math/rand" 来使用这两个包。
按照约定,包名与导入路径的最后一个元素一致。例如,"math/rand" 包中的源码均以 package rand 语句开始。
3、数据类型、变量、函数
3.1 数据类型
bool
string
int int8 int16 int32 int64
uint uint8 uint16 uint32 uint64 uintptr
byte // uint8 的别名
rune // int32 的别名 // 表示一个 Unicode 码点
float32 float64
complex64 complex128
int, uint 和 uintptr 在 32 位系统上通常为 32 位宽,在 64 位系统上则为 64 位宽
3.2 变量
var 语句用于声明一个变量列表,跟函数的参数列表一样,类型在最后
单个变量声明
- var 变量名称 数据类型 = 变量值;如果不赋值,使用的是该数据类型的默认值。
var a int =100 - var 变量名称 = 变量值;根据变量值,自行判断数据类型。
var c = 100 - 变量名称 := 变量值;省略了 var 和数据类型,变量名称一定要是未声明过的。
e := 100全局变量不能使用
多个变量声明
-
单⾏写法
- var xx, yy int = 100, 200
- var kk, ll = 100, "Aceld"
-
多选写法
- var ( vv int = 100 jj bool = true )
3.3 常量
const a int = 10
const ( a = 10 b = 20 )
3.4 iota与const来表示枚举类型
3.5 函数
函数定义
func function_name(input1 type1, input2 type2) (type1, type2) {
// 函数体
// 返回多个值
return value1, value2
}
- 函数用
func声明。 - 函数可以有一个或多个参数,需要有参数类型,用
,分割。 - 函数可以有一个或多个返回值,需要有返回值类型,用
,分割。 - 函数的参数是可选的,返回值也是可选的。
定义了函数之后,我们可以通过函数名()的方式调用函数
返回值
4、流程控制
break 跳出循环:break语句可以结束for、switch和select的代码块。
continue 继续下次循环:continue语句可以结束当前循环,开始下一次的循环迭代过程,仅限在for循环内使用。
4.1 if else 分支结构
if 表达式1 {
分支1
} else if 表达式2 {
分支2
} else{
分支3
}
if条件判断特殊写法
可以在 if 表达式之前添加一个执行语句,再根据变量值进行判断,
func ifDemo2() {
if score := 65; score >= 90 {
fmt.Println("A")
} else if score > 75 {
fmt.Println("B")
} else {
fmt.Println("C")
}
}
4.2 for循环结构
for 初始语句;条件表达式;结束语句{
循环体语句
}
while形式
func forDemo() {
i := 0
for i < 10 {
fmt.Println(i)
i++
}
}
无限循环
for {
循环体语句
}
4.3 for range(键值循环)
Go语言中可以使用for range遍历数组、切片、字符串、map 及通道(channel)。 通过for range遍历的返回值有以下规律:
- 数组、切片、字符串返回索引和值。
- map返回键和值。
- 通道(channel)只返回通道内的值。
4.4 switch case
func switchDemo1() {
finger := 3
switch finger {
case 1:
fmt.Println("大拇指")
case 2:
fmt.Println("食指")
case 3:
fmt.Println("中指")
case 4:
fmt.Println("无名指")
case 5:
fmt.Println("小拇指")
default:
fmt.Println("无效的输入!")
}
}
5、数组
var 数组变量名 [元素数量]T
5.1 数据初始化
-
初始化数组时可以使用初始化列表来设置数组元素的值。
-
var testArray [3]int //数组会初始化为int类型的零值 var numArray = [3]int{1, 2} //使用指定的初始值完成初始化 var cityArray = [3]string{"北京", "上海", "深圳"} //使用指定的初始值完成初始化
-
-
按照上面的方法每次都要确保提供的初始值和数组长度一致,一般情况下我们可以让编译器根据初始值的个数自行推断数组的长度
-
var testArray [3]int var numArray = [...]int{1, 2} var cityArray = [...]string{"北京", "上海", "深圳"}
-
-
我们还可以使用指定索引值的方式来初始化数组:
a := [...]int{1: 1, 3: 5}[0 1 0 5] -
使用类型推导:
primes := [6]int{2, 3, 5, 7, 11, 13}
5.2 数组遍历
func main() {
a := [3][2]string{
{"北京", "上海"},
{"广州", "深圳"},
{"成都", "重庆"},
}
for _, v1 := range a {
for _, v2 := range v1 {
fmt.Printf("%s\t", v2)
}
fmt.Println()
}
}
5.3、数组是值类型
数组是值类型,赋值和传参会复制整个数组。因此改变副本的值,不会改变本身的值。
- 数组支持 “==“、”!=” 操作符,因为内存总是被初始化过的。
[n]*T表示指针数组,*[n]T表示数组指针 。
6、切片
切片(Slice)是一个拥有相同类型元素的可变长度的序列。它是基于数组类型做的一层封装。它非常灵活,支持自动扩容。
切片是一个引用类型,它的内部结构包含地址、长度和容量。切片一般用于快速地操作一块数据集合。
var name []T name:表示变量名 T:表示切片中的元素类型
var a []string //声明一个字符串切片
var b = []int{} //声明一个整型切片并初始化
var c = []bool{false, true} //声明一个布尔切片并初始化
var d = []bool{false, true} //声明一个布尔切片并初始化
切片拥有自己的长度和容量,我们可以通过使用内置的len()函数求长度,使用内置的cap()函数求切片的容量。
6.1 切片表达式
切片表达式中的low和high表示一个索引范围(左包含,右不包含),
为了方便起见,可以省略切片表达式中的任何索引。省略了low则默认为0;省略了high则默认为切片操作数的长度
a[2:] // 等同于 a[2:len(a)]
a[:3] // 等同于 a[0:3]
a[:] // 等同于 a[0:len(a)]
6.2 使用make()函数构造切片
make([]T, size, cap)
- T:切片的元素类型
- size:切片中元素的数量
- cap:切片的容量
6.3 切片声明
6.4 切片本质
切片的本质就是对底层数组的封装,它包含了三个信息:底层数组的指针、切片的长度(len)和切片的容量(cap)。
举个例子,现在有一个数组a := [8]int{0, 1, 2, 3, 4, 5, 6, 7},切片s1 := a[:5],相应示意图如下。
要检查切片是否为空,请始终使用len(s) == 0来判断,而不应该使用s == nil来判断。
6.5 切片的遍历、添加、删除
for index, value := range s {
fmt.Println(index, value)
}
Go语言的内建函数append()可以为切片动态添加元素。 可以一次添加一个元素,可以添加多个元素,也可以添加另一个切片中的元素(后面加…)
func main(){
var s []int
s = append(s, 1) // [1]
s = append(s, 2, 3, 4) // [1 2 3 4]
s2 := []int{5, 6, 7}
s = append(s, s2...) // [1 2 3 4 5 6 7]
}
注意:通过var声明的零值切片可以在append()函数直接使用,无需初始化。
Go语言中并没有删除切片元素的专用方法,我们可以使用切片本身的特性来删除元素。 代码如下:
func main() {
// 从切片中删除元素
a := []int{30, 31, 32, 33, 34, 35, 36, 37}
// 要删除索引为2的元素
a = append(a[:2], a[3:]...)
fmt.Println(a) //[30 31 33 34 35 36 37]
}
6.6 切片的扩容机制
6.7 使用copy()函数复制切片
由于切片是引用类型,所以a和b其实都指向了同一块内存地址。修改b的同时a的值也会发生变化。
Go语言内建的copy()函数可以迅速地将一个切片的数据复制到另外一个切片空间中,copy()函数的使用格式如下: copy(destSlice, srcSlice []T)
- srcSlice: 数据来源切片
- destSlice: 目标切片
7、map
map是一种无序的基于key-value的数据结构,Go语言中的map是引用类型,必须初始化才能使用。
定义:map[KeyType]ValueType
map类型的变量默认初始值为nil,需要使用make()函数来分配内存 make(map[KeyType]ValueType, [cap])
func main() {
scoreMap := make(map[string]int, 8)
scoreMap["张三"] = 90
scoreMap["小明"] = 100
fmt.Println(scoreMap)
fmt.Println(scoreMap["小明"])
fmt.Printf("type of a:%T\n", scoreMap)
}
声明时填充元素
func main() {
userInfo := map[string]string{
"username": "沙河小王子",
"password": "123456",
}
fmt.Println(userInfo) //
}
7.1 遍历、存在、删除
**存在:**如果key存在ok为true,v为对应的值;不存在ok为false,v为值类型的零值
value, ok := map[key]
map range 遍历
for k, v := range scoreMap {
fmt.Println(k, v)
}
delete函数删除
delete(map, key)
7.2 按照指定顺序遍历
从map中取出所有的key存放在数组切片中,对切片排序,按数组遍历
func main() {
rand.Seed(time.Now().UnixNano()) //初始化随机数种子
var scoreMap = make(map[string]int, 200)
for i := 0; i < 100; i++ {
key := fmt.Sprintf("stu%02d", i) //生成stu开头的字符串
value := rand.Intn(100) //生成0~99的随机整数
scoreMap[key] = value
}
//取出map中的所有key存入切片keys
var keys = make([]string, 0, 200)
for key := range scoreMap {
keys = append(keys, key)
}
//对切片进行排序
sort.Strings(keys)
//按照排序后的key遍历map
for _, key := range keys {
fmt.Println(key, scoreMap[key])
}
}
7.3 元素为map类型的切片
func main() {
var mapSlice = make([]map[string]string, 3)
for index, value := range mapSlice {
fmt.Printf("index:%d value:%v\n", index, value)
}
fmt.Println("after init")
// 对切片中的map元素进行初始化
mapSlice[0] = make(map[string]string, 10)
mapSlice[0]["name"] = "小王子"
mapSlice[0]["password"] = "123456"
mapSlice[0]["address"] = "沙河"
for index, value := range mapSlice {
fmt.Printf("index:%d value:%v\n", index, value)
}
}
7.4 值为切片类型的map
func main() {
var sliceMap = make(map[string][]string, 3)
fmt.Println(sliceMap)
fmt.Println("after init")
key := "中国"
value, ok := sliceMap[key]
if !ok {
value = make([]string, 0, 2)
}
value = append(value, "北京", "上海")
sliceMap[key] = value
fmt.Println(sliceMap)
}
7.5 单词统计
package main
import (
"strings"
"golang.org/x/tour/wc"
)
func WordCount(s string) map[string]int {
wordcount := make(map[string]int)
str := strings.Split(s, " ")
for i := 0; i < len(str); i++ {
if v, exists := wordcount[str[i]]; exists {
wordcount[str[i]] = v + 1
} else {
wordcount[str[i]] = 1
}
}
return wordcount
}
func main() {
wc.Test(WordCount)
}
8、指针和结构体
8.1 指针
任何程序数据载入内存后,在内存都有他们的地址,这就是指针。而为了保存一个数据在内存中的地址,我们就需要指针变量
new 和 make的区别
- 二者都是用来做内存分配的。
- make只用于slice、map以及channel的初始化,返回的还是这三个引用类型本身;
- 而new用于类型的内存分配,并且内存对应的值为类型零值,返回的是指向类型的指针。
8.2 结构体
- 封装多个基本数据类型,通过struct来定义
- Go语言中通过
struct来实现面向对象。 - 结构体占用一块连续的内存。
- Go语言的结构体没有构造函数
- **权限:**结构体中字段大写开头表示可公开访问,小写表示私有(仅在定义当前结构体的包中可访问)。
type 类型名 struct {
字段名 字段类型
字段名 字段类型
…
}
实例化
-
基本实例化
-
type person struct { name string city string age int8 } func main() { var p1 person p1.name = "沙河娜扎" p1.city = "北京" p1.age = 18 fmt.Printf("p1=%v\n", p1) //p1={沙河娜扎 北京 18} fmt.Printf("p1=%#v\n", p1) //p1=main.person{name:"沙河娜扎", city:"北京", age:18} }
-
-
键值对初始化
-
p5 := person{ name: "小王子", city: "北京", age: 18, }
-
-
取结构体的地址实例化
- 使用
&对结构体进行取地址操作相当于对该结构体类型进行了一次new实例化操作。
- 使用
8.3 方法
Go语言中的方法(Method)是一种作用于特定类型变量的函数。这种特定类型变量叫做接收者(Receiver)。接收者的概念就类似于其他语言中的this或者 self。
func (接收者变量 接收者类型) 方法名(参数列表) (返回参数) {
函数体
}
方法与函数的区别是,函数不属于任何类型,方法属于特定的类型。
8.4 值接收者和指针接收者
值接收者和指针接收者实现接口的区别主要体现在对接收者的拷贝行为和对接收者的修改能力上。
从上面的代码中我们可以发现,使用值接收者实现接口之后,不管是dog结构体还是结构体指针*dog类型的变量都可以赋值给该接口变量。因为Go语言中有对指针类型变量求值的语法糖,dog指针fugui内部会自动求值*fugui
此时实现Mover接口的是*dog类型,所以不能给x传入dog类型的wangcai,此时x只能存储*dog类型的值。
什么时候使用指针接收者
- 需要修改接收者中的值
- 接收者是拷贝代价比较大的大对象
- 保证一致性,如果有某个方法使用了指针接收者,那么其他的方法也应该使用指针接收者。
8.5 结构体嵌套
//Address 地址结构体
type Address struct {
Province string
City string
}
//User 用户结构体
type User struct {
Name string
Gender string
Address Address
}
func main() {
user1 := User{
Name: "小王子",
Gender: "男",
Address: Address{
Province: "山东",
City: "威海",
},
}
fmt.Printf("user1=%#v\n", user1)//user1=main.User{Name:"小王子", Gender:"男", Address:main.Address{Province:"山东", City:"威海"}}
}
8.6 结构体的继承
//Animal 动物
type Animal struct {
name string
}
func (a *Animal) move() {
fmt.Printf("%s会动!\n", a.name)
}
//Dog 狗
type Dog struct {
Feet int8
*Animal //通过嵌套匿名结构体实现继承
}
func (d *Dog) wang() {
fmt.Printf("%s会汪汪汪~\n", d.name)
}
func main() {
d1 := &Dog{
Feet: 4,
Animal: &Animal{ //注意嵌套的是结构体指针
name: "乐乐",
},
}
d1.wang() //乐乐会汪汪汪~
d1.move() //乐乐会动!
}
8.7 结构体与JSON序列化
//Student 学生
type Student struct {
ID int
Gender string
Name string
}
//Class 班级
type Class struct {
Title string
Students []*Student
}
func main() {
c := &Class{
Title: "101",
Students: make([]*Student, 0, 200),
}
for i := 0; i < 10; i++ {
stu := &Student{
Name: fmt.Sprintf("stu%02d", i),
Gender: "男",
ID: i,
}
c.Students = append(c.Students, stu)
}
//JSON序列化:结构体-->JSON格式的字符串
data, err := json.Marshal(c)
if err != nil {
fmt.Println("json marshal failed")
return
}
fmt.Printf("json:%s\n", data)
//JSON反序列化:JSON格式的字符串-->结构体
str := `{"Title":"101","Students":[{"ID":0,"Gender":"男","Name":"stu00"},{"ID":1,"Gender":"男","Name":"stu01"},{"ID":2,"Gender":"男","Name":"stu02"},{"ID":3,"Gender":"男","Name":"stu03"},{"ID":4,"Gender":"男","Name":"stu04"},{"ID":5,"Gender":"男","Name":"stu05"},{"ID":6,"Gender":"男","Name":"stu06"},{"ID":7,"Gender":"男","Name":"stu07"},{"ID":8,"Gender":"男","Name":"stu08"},{"ID":9,"Gender":"男","Name":"stu09"}]}`
c1 := &Class{}
err = json.Unmarshal([]byte(str), c1)
if err != nil {
fmt.Println("json unmarshal failed!")
return
}
fmt.Printf("%#v\n", c1)
}
8.8 结构体标签
Tag是结构体的元信息,可以在运行的时候通过反射的机制读取出来。 Tag在结构体字段的后方定义,由一对反引号包裹起来,具体的格式如下:
`key1:"value1" key2:"value2"`
//Student 学生
type Student struct {
ID int `json:"id"` //通过指定tag实现json序列化该字段时的key
Gender string //json序列化是默认使用字段名作为key
name string //私有不能被json包访问
}
func main() {
s1 := Student{
ID: 1,
Gender: "男",
name: "沙河娜扎",
}
data, err := json.Marshal(s1)
if err != nil {
fmt.Println("json marshal failed!")
return
}
fmt.Printf("json str:%s\n", data) //json str:{"id":1,"Gender":"男"}
}
8.9 结构中slice和map注意
因为slice和map这两种数据类型都包含了指向底层数据的指针,因此我们在需要复制它们时要特别注意
type Person struct {
name string
age int8
dreams []string
}
func (p *Person) SetDreams(dreams []string) {
p.dreams = dreams
}
func main() {
p1 := Person{name: "小王子", age: 18}
data := []string{"吃饭", "睡觉", "打豆豆"}
p1.SetDreams(data)
// 你真的想要修改 p1.dreams 吗?
data[1] = "不睡觉"
fmt.Println(p1.dreams) // ?
}
正确的做法
func (p *Person) SetDreams(dreams []string) {
p.dreams = make([]string, len(dreams))
copy(p.dreams, dreams)
}
9、go基础之并发
并发:同一时间段内执行多个任务
并行:同一时刻执行多个任务
9.1 进程、线程和协程
进程(process):程序在操作系统中的一次执行过程,系统进行资源分配和调度的一个独立单位
线程(thread):操作系统基于进程开启的轻量级进程,是操作系统调度的最小单位,**内核态,**线程跑多个协程,栈MB级别
协程(coroutine):非操作系统提供而是由用户自行创建和控制的用户态线程,用户态,轻量级线程,栈KB级别
9.2 go 程(goroutine)和channel
-
Go 程(goroutine)是由 Go 运行时管理的轻量级线程。
go f(x, y, z)。会启动一个新的 Go 程并执行。go 程序默认会给main 函数创建一个goroutine-
其实在 Go 程序启动时,Go 程序就会为 main 函数创建一个默认的 goroutine 。在上面的代码中我们在 main 函数中使用 go 关键字创建了另外一个 goroutine 去执行 hello 函数,而此时 main goroutine 还在继续往下执行,我们的程序中此时存在两个并发执行的 goroutine。当 main 函数结束时整个程序也就结束了,同时 main goroutine 也结束了,所有由 main goroutine 创建的 goroutine 也会一同退出。也就是说我们的 main 函数退出太快,另外一个 goroutine 中的函数还未执行完程序就退出了,导致未打印出“hello”。
package main import ( "fmt" ) func hello() { fmt.Println("hello") } func main() { go hello() // 启动另外一个goroutine去执行hello函数 fmt.Println("你好") } 输出 你好 -
Go 程在相同的地址空间中运行,因此在访问共享的内存时必须进行同步
-
sync 包中的
WaitGroup是实现等待一组并发操作完成的好方法-
多次执行下面的代码会发现每次终端上打印数字的顺序都不一致。这是因为10个 goroutine 是并发执行的,而 goroutine 的调度是随机的
package main import ( "fmt" "sync" ) var wg sync.WaitGroup func hello(i int) { defer wg.Done() // goroutine结束就登记-1 fmt.Println("hello", i) } func main() { for i := 0; i < 10; i++ { wg.Add(1) // 启动一个goroutine就登记+1 go hello(i) } wg.Wait() // 等待所有登记的goroutine都结束 }
-
-
-
信道是带有类型的管道,你可以通过它用信道操作符
<-来发送或者接收值。用于go程之间通信-
和映射与切片一样,信道在使用前必须创建。
ch := make(chan int) -
默认情况下,发送和接收操作在另一端准备好之前都会阻塞。这使得 Go 程可以在没有显式的锁或竞态变量的情况下进行同步。
-
package main import "fmt" func sum(s []int, c chan int) { sum := 0 for _, v := range s { sum += v } c <- sum // 将和送入 c } func main() { s := []int{7, 2, 8, -9, 4, 0} c := make(chan int) go sum(s[:len(s)/2], c) go sum(s[len(s)/2:], c) x, y := <-c, <-c // 从 c 中接收 fmt.Println(x, y, x+y) }
-
-
信道可以是 带缓冲的 。将缓冲长度作为第二个参数提供给
make来初始化一个带缓冲的信道:ch := make(chan int, 100)- func main() { ch := make(chan int) ch <- 10 fmt.Println("发送成功") }
- 能通过编译但会出现死锁。因为我们使用
ch := make(chan int)创建的是无缓冲的通道,无缓冲的通道只有在有接收方能够接收值的时候才能发送成功,否则会一直处于等待发送的阶段。同理,如果对一个无缓冲通道执行接收操作时,没有任何向通道中发送值的操作那么也会导致接收操作阻塞。
-
单向通道:
<- chan int // 只接收通道,只能接收不能发送chan <- int // 只发送通道,只能发送不能接收-
其中,箭头
<-和关键字chan的相对位置表明了当前通道允许的操作,这种限制将在编译阶段进行检测。另外对一个只接收通道执行close也是不允许的,因为默认通道的关闭操作应该由发送方来完成 -
// Producer2 返回一个接收通道 func Producer2() <-chan int { ch := make(chan int, 2) // 创建一个新的goroutine执行发送数据的任务 go func() { for i := 0; i < 10; i++ { if i%2 == 1 { ch <- i } } close(ch) // 任务完成后关闭通道 }() return ch } // Consumer2 参数为接收通道 func Consumer2(ch <-chan int) int { sum := 0 for v := range ch { sum += v } return sum } func main() { ch2 := Producer2() res2 := Consumer2(ch2) fmt.Println(res2) // 25 }
-
-
-
9.3 goroutine调度
区别于操作系统内核调度操作系统线程,goroutine 的调度是Go语言运行时(runtime)层面的实现,是完全由 Go 语言本身实现的一套调度系统——**go scheduler。**它的作用是按照一定的规则将所有的 goroutine 调度到操作系统线程上执行。
在经历数个版本的迭代之后,目前 Go 语言的调度器采用的是 GPM 调度模型。
- G:表示 goroutine,每执行一次
go f()就创建一个 G,包含要执行的函数和上下文信息。 - 全局队列(Global Queue):存放等待运行的 G。
- P:表示 goroutine 执行所需的资源,最多有 GOMAXPROCS 个。
- P 的本地队列:同全局队列类似,存放的也是等待运行的G,存的数量有限,不超过256个。新建 G 时,G 优先加入到 P 的本地队列,如果本地队列满了会批量移动部分 G 到全局队列。
- M:线程想运行任务就得获取 P,从 P 的本地队列获取 G,当 P 的本地队列为空时,M 也会尝试从全局队列或其他 P 的本地队列获取 G。M 运行 G,G 执行之后,M 会从 P 获取下一个 G,不断重复下去。
- Goroutine 调度器和操作系统调度器是通过 M 结合起来的,每个 M 都代表了1个内核线程,操作系统调度器负责把内核线程分配到 CPU 的核上执行。
GOMAXPROCS
Go运行时的调度器使用GOMAXPROCS参数来确定需要使用多少个 OS 线程来同时执行 Go 代码。默认值是机器上的 CPU 核心数。例如在一个 8 核心的机器上,GOMAXPROCS 默认为 8。Go语言中可以通过runtime.GOMAXPROCS函数设置当前程序并发时占用的 CPU逻辑核心数。(Go1.5版本之前,默认使用的是单核心执行。Go1.5 版本之后,默认使用全部的CPU 逻辑核心数。)
9.4 range 和 close
发送者可通过 close 关闭一个信道来表示没有需要发送的值了。接收者可以通过为接收表达式分配第二个参数来测试信道是否被关闭:若没有值可以接收且信道已被关闭,那么在执行完 v, ok := <-ch之后 ok 会被设置为 false。
循环 for i := range c 会不断从信道接收值,直到它被关闭。
注意: 只有发送者才能关闭信道,而接收者不能。向一个已经关闭的信道发送数据会引发程序恐慌(panic)。
还要注意: 信道与文件不同,通常情况下无需关闭它们。只有在必须告诉接收者不再有需要发送的值时才有必要关闭,例如终止一个
range循环。
9.5 select
单流程下⼀个go只能监控⼀个channel的状态,select可以完成监控多个channel的状态
10、socket
TCP/IP(Transmission Control Protocol/Internet Protocol) 即传输控制协议/网间协议,是一种面向连接(连接导向)的、可靠的、基于字节流的传输层(Transport layer)通信协议,因为是面向连接的协议,数据像水流一样传输,会存在黏包问题。
10.1 基于TCP通信编码
**TCP服务端:**一个TCP服务端可以同时连接很多个客户端,例如世界各地的用户使用自己电脑上的浏览器访问淘宝网。因为Go语言中创建多个goroutine实现并发非常方便和高效,所以我们可以每建立一次链接就创建一个goroutine去处理。
TCP服务端处理流程:
-
监听端口
-
接受客户端请求建立链接
-
创建goroutine处理链接
-
使用
net包实现代码如下-
package main import ( "bufio" "fmt" "net" ) func process(conn net.Conn) { defer conn.Close() //关闭连接 for { reader := bufio.NewReader(conn) var buf [128]byte n, err := reader.Read(buf[:]) if err != nil { fmt.Printf("read from client failed,err:%v\n", err) return } recvStr := string(buf[:n]) fmt.Printf("收到client的数据:%v\n", recvStr) conn.Write([]byte(recvStr)) //发送数据 } } func main() { listen, err := net.Listen("tcp", "127.0.0.1:20000") if err != nil { fmt.Println("listen failed,err:", err) return } for { conn, err := listen.Accept() //等待客户端建立连接 if err != nil { fmt.Println("accept failed,err:", err) continue } go process(conn) } }
-
一个TCP客户端进行TCP通信如下
-
建立与服务端链接
-
进行数据转发
-
关闭链接
-
基于
net包代码如下-
package main import ( "bufio" "fmt" "net" "os" "strings" ) func main() { conn, err := net.Dial("tcp", "127.0.0.1:20000") if err != nil { fmt.Println("err:", err) return } defer conn.Close() inputReader := bufio.NewReader(os.Stdin) for { input, _ := inputReader.ReadString('\n') // 读取用户输入 inputInfo := strings.Trim(input, "\r\n") if strings.ToUpper(inputInfo) == "Q" { // 如果输入q就退出 return } _, err := conn.Write([]byte(inputInfo)) //发送服务端数据 if err != nil { return } buf := [512]byte{} n, err := conn.Read(buf[:]) if err != nil { fmt.Println("recv failed,err", err) return } fmt.Println("收到服务端的数据:", string(buf[:n])) } }
-
10.2 基于UDP通信
服务端:
-
监听端口,接收数据
相对于TCP少了与客户端建立链接,来者不惧
func main() { listen, err := net.ListenUDP("udp", &net.UDPAddr{ IP: net.IPv4(0, 0, 0, 0), Port: 30000, }) if err != nil { fmt.Println("listen failed, err:", err) return } defer listen.Close() for { var data [1024]byte n, addr, err := listen.ReadFromUDP(data[:]) // 接收数据 if err != nil { fmt.Println("read udp failed, err:", err) continue } fmt.Printf("data:%v addr:%v count:%v\n", string(data[:n]), addr, n) _, err = listen.WriteToUDP(data[:n], addr) // 发送数据 if err != nil { fmt.Println("write to udp failed, err:", err) continue } } }
客户端:
-
建立与服务端链接,进行数据转发,关闭链接
// UDP 客户端 func main() { socket, err := net.DialUDP("udp", nil, &net.UDPAddr{ IP: net.IPv4(0, 0, 0, 0), Port: 30000, }) if err != nil { fmt.Println("连接服务端失败,err:", err) return } defer socket.Close() sendData := []byte("Hello server") _, err = socket.Write(sendData) // 发送数据 if err != nil { fmt.Println("发送数据失败,err:", err) return } data := make([]byte, 4096) n, remoteAddr, err := socket.ReadFromUDP(data) // 接收数据 if err != nil { fmt.Println("接收数据失败,err:", err) return } fmt.Printf("recv:%v addr:%v count:%v\n", string(data[:n]), remoteAddr, n) }
特性
1、高阶函数
高阶函数分为函数作为参数和函数作为返回值两部分。
1.1、函数作为参数
func add(x, y int) int {
return x + y
}
func calc(x, y int, op func(int, int) int) int {
return op(x, y)
}
func main() {
ret2 := calc(10, 20, add)
fmt.Println(ret2) //30
}
1.2、函数作为返回值
func do(s string) (func(int, int) int, error) {
switch s {
case "+":
return add, nil
case "-":
return sub, nil
default:
err := errors.New("无法识别的操作符")
return nil, err
}
}
2、匿名函数和闭包
2.1、匿名函数
匿名函数就是没有函数名的函数,匿名函数的定义格式如下:
func(参数)(返回值){
函数体
}
匿名函数因为没有函数名,所以没办法像普通函数那样调用,所以匿名函数需要保存到某个变量或者作为立即执行函数:
func main() {
// 将匿名函数保存到变量
add := func(x, y int) {
fmt.Println(x + y)
}
add(10, 20) // 通过变量调用匿名函数
//自执行函数:匿名函数定义完加()直接执行
func(x, y int) {
fmt.Println(x + y)
}(10, 20)
}
匿名函数多用于实现回调函数和闭包
2.2、闭包
闭包指的是一个函数和与其相关的引用环境组合而成的实体。简单来说,闭包=函数+引用环境。
-
函数作为返回值
-
闭包=函数+外层变量的引用
-
闭包传参,函数没有引用,就去外层函数寻找变量引用
3、defer
Go语言中的defer语句会将其后面跟随的语句进行延迟处理。在defer归属的函数即将返回时,将延迟处理的语句按defer定义的逆序进行执行,也就是说,先被defer的语句最后被执行,最后被defer的语句,最先被执行。
func main() {
fmt.Println("start")
defer fmt.Println(1)
defer fmt.Println(2)
defer fmt.Println(3)
fmt.Println("end")
}
输出: start end 3 2 1
由于defer语句延迟调用的特性,所以defer语句能非常方便的处理资源释放问题。比如:资源清理、文件关闭、解锁及记录时间等。
3.1 defer执行时机
在Go语言的函数中return语句在底层并不是原子操作,它分为给返回值赋值和RET指令两步。而defer语句执行的时机就在返回值赋值操作后,RET指令执行前。具体如下图所示:
func f1() int {
x := 5
defer func() {
x++
}()
return x
}
func f2() (x int) {
defer func() {
x++
}()
return 5
}
func f3() (y int) {
x := 5
defer func() {
x++
}()
return x
}
func f4() (x int) {
defer func(x int) {
x++
}(x)
return 5
}
func main() {
fmt.Println(f1()) //5
fmt.Println(f2()) //6
fmt.Println(f3()) //5
fmt.Println(f4()) //5
}
4、内置函数
4.1、panic/recover
recover()必须搭配defer使用。defer一定要在可能引发panic的语句之前定义。
func funcA() {
fmt.Println("func A")
}
func funcB() {
defer func() {
err := recover()
//如果程序出出现了panic错误,可以通过recover恢复过来
if err != nil {
fmt.Println("recover in B")
}
}()
panic("panic in B")
}
func funcC() {
fmt.Println("func C")
}
func main() {
funcA()
funcB()
funcC()
}
程序运行期间funcB中引发了panic导致程序崩溃,异常退出了。这个时候我们就可以通过recover将程序恢复回来,继续往后执行。
5、Error接口和错误处理
Go 语言中的错误处理与其他语言不太一样,它把错误当成一种值来处理,更强调判断错误、处理错误,而不是一股脑的 catch 捕获异常。
5.1 Error接口
Go 语言中把错误当成一种特殊的值来处理,不支持其他语言中使用try/catch捕获异常的方式
type error interface {
Error() string
}
6、反射
前置: Go语言中的变量是分为两部分的:
- 类型信息:预先定义好的元信息。
- 值信息:程序运行过程中可动态变化的。
定义: 反射是指在程序运行期间对程序本身进行访问和修改的能力