go语言基础
GO语言
安装go
a. 打开网址https://golang.org/dl/
b. 根据操作系统选择对应的安装包
c. 点击安装包进行安装(linux直接解压)
d. 设置环境变量(linux)
1. export GOROOT=$PATH:/path/to/go/
2. export PATH=$PATH:$GOROOT/bin/
3. export GOPATH=/home/user/project/go
e. 设置环境变量(window不用设置)
开发环境搭建
2. IDE搭建(vscode)
a. 打开网址:https://code.visualstudio.com/
b. 根据操作系统选择对应的安装包
c. 点击安装包进行安装(linux直接解压)
d. 选择查看-》扩展-》搜索go,安装第二个
2. 新建项目
a. 新建目录/home/user/project/go/src/listen1
b. 用vscode打开目录/home/user/project/go/src/listen1
c. 右键新建文件hello.go,保存
d. vscode会提示你安装一些go的工具,我们点击install all
3. 调试工具delve安装
打开网址: https://github.com/derekparker/delve/tree/master/Documentation/installation
b. mac: brew install go-delve/delve/delve
c. linux&windows: go get github.com/derekparker/delve/cmd/dlv
go语言特性
1. 垃圾回收
a.内存自动回收,再也不需要开发人员管理内存
b. 开发人员专注业务实现,降低了心智负担
c. 只需要new分配内存,不需要释放
2. 天然并发
a.从语言层面支持并发,非常简单
b. goroute,轻量级线程,创建成千上万个goroute成为可能
c. 基于CSP(Communicating Sequential Process)模型实现
func main() {
go fmt.Println(“hello")
}
3. channel
a.管道,类似unix/linux中的pipe
b. 多个goroute之间通过channel进行通信
c. 支持任何类型
func main() {
pipe := make(chan int,3)
pipe <- 1
pipe <- 2
}
4. 多返回值
a.一个函数返回多个值
func calc(a int, b int)(int,int) {
sum := a + b
avg := (a+b)/2
return sum, avg
}
1. hello world,在listen1目录下新建hello.go
package main
import(
“fmt”
)
func main() {
fmt.Println(“hello world”)
}
包的概念
1. 和python一样,把相同功能的代码放到一个目录,称之为包
2. 包可以被其他包引用
3. main包是用来生成可执行文件,每个程序只有一个main包
4. 包的主要用途是提高代码的可复用性
1. 在listen1目录下新建calc目录
2. 在calc目录下新建calc.go
package calc
func Add(a int, b int) int {
return a + b
}
1. 修改hello.go代码,如下
package main
import(
“fmt”
“calc”
)
func main() {
sum := calc.Add(3,5)
fmt.Println(“hello world,%d”,sum)
}
基本数据类型和操作符
文件名关键字标识符
1. 所有go源码以.go结尾
2. 标识符以字母或下划线开头,大小写敏感,比如:
a.boy b.Boy a+b
_boy =_boy _
_是特殊标识符,表示忽略结果
4. 保留关键字
基本结构
package main
import (
"fmt"
)
func main() {
fmt.Println("hello")
}
任何代码文件属于一个包
import 关键字,引用其他包,
import (
"fmt"
)
3. golang可执行程序,package main,
并且有且只有一个main入口函数
4. 包中函数调用:
a. 同一个包中函数,直接调用
b. 不同包中函数,通过包名+点+
函数名进行调用
5. 包访问控制规则:
大写意味着这个函数/变量是可导出的
小写意味着这个函数/变量是私有的,
包外部不能访问
函数声明注释
函数声明: func 函数名字 (参数列表) (返回值列表){}
func add(){}
func add(a int ,b int){}
func add(a int , b int ) (int int){}
常量
1.常量使用const 修饰,代表永远是只读的,不能修改。
2. const 只能修饰boolean,number(int相关类型、浮点类型、complex)和string。
3. 语法:const identifier [type] = value,其中type可以省略。
const a string ="hello"
const b = 9/2
const [
a = 1
b = 2
c = 3
]
变量
语法:var identifier type
var a int
var b string
var {
a int
b string
c bool
d int =8
s string = "hello world"
}
值类型和引用类型
值类型:变量直接存储值,内存通常在栈中分配。
引用类型:变量存储的是一个地址,这个地址存储最终的值。内存通常在
堆上分配。通过GC回收。
值类型和引用类型
值类型:基本数据类型int、float、bool、string以及数组和struct。
引用类型:指针、slice、map、chan等都是引用类型。
变量的作用域
在函数内部声明的变量叫做局部变量,生命周期仅限于函数内部。
在函数外部声明的变量叫做全局变量,生命周期作用于整个包,如果是大写的,
则作用于整个程序。
数据类型和操作符
bool类型,只能存true和false
var a bool
var a bool = true
var a = true
相关操作符, !、&&、||
var a bool = true
var b
数据类型和操作符
数字类型,主要有int、int8、int16、int32、int64、uint8、uint16、uint32、uint64、float32、float64
类型转换,type(variable),比如:var a int=8; var b int32=int32(a)
package main
func main() {
var a int
var b int32
a = 15
b = int32(a+a)
b = b +5
}
数据类型和操作符
字符类型:var a byte
var a byte = ‘c’
字符串类型: var str string
package main
func main() {
var str = "hello"
fmt.Println("str=",str)
}
字符串表示两种方式: 1)双引号 2)`` (反引号)
strings和strconv使用
strings.HasPrefix(s string, prefix string) bool:判断字符串s是否以prefix开头 。
练习1:判断一个url是否以http://开头,如果不是,则加上http://。
2. strings.HasSuffix(s string, suffix string) bool:判断字符串s是否以suffix结尾。
3. strings.Index(s string, str string) int:判断str在s中首次出现的位置,如果没有
出现,则返回-1
4. strings.LastIndex(s string, str string) int:判断str在s中最后出现的位置,如果没有
出现,则返回-1
package main
import (
"fmt"
"path"
"strconv"
"strings"
)
func urlProcess(url string) string {
result := strings.HasPrefix(url,"http://")
if !result {
url = fmt.Sprintf("http://%s",url)
}
return url
}
func main() {
strings.TrimSpace(" sksk ")
strings.Trim("abababab","ab")
strings.TrimLeft("a","b")
strings.TrimRight("b","c")
strings.Fields("abc cbd dfc ")
strings.Split("abc,cbd edn",",")
//var a = "aaa"
a := "aaaa"
b := "bbbbb"
c := strings.Join([]string{a,b},",")
fmt.Println(c)
path.Join("a","c")
a1 := "hello hhhhhh"
b1 := "world"
strings.Replace(a1,"hhhhhh",b1,1)
strings.Repeat(a1,3)
strings.ToUpper(a1) // 大小写转换
strings.ToLower(a1)
a2 := "aaa,ccc,c"
strings.Split(a2,",")
str2 := strconv.Itoa(1000)
fmt.Println("itoa",str2)
}
练习3:写一个函数返回一个字符串在另一个字符串的首次出现和最后出现位置
func StrIndex(str string, substr string)(int, int){}
5. strings.Replace(str string, old string, new string, n int):字符串替换
6. strings.Count(str string, substr string)int:字符串计数
7. strings.Repeat(str string, count int)string:重复count次str
8. strings.ToLower(str string)string:转为小写
9. strings.ToUpper(str string)string:转为大写
练习4:写一个函数分别演示Replace、Count、Repeat、ToLower、ToUpper的用法
5. strings.TrimSpace(str string):去掉字符串首尾空白字符
strings.Trim(str string, cut string):去掉字符串首尾cut字符
strings.TrimLeft(str string, cut string):去掉字符串首cut字符
strings.TrimRight(str string, cut string):去掉字符串首cut字符
6. strings.Field(str string):返回str空格分隔的所有子串的slice
strings.Split(str string, split string):返回str split分隔的所有子串的slice
7. strings.Join(s1 []string, sep string):用sep把s1中的所有元素链接起来
练习5:写一个函数分别演示TrimSpace、Trim、TrimLeft、TrimRight、Field、Split、以及Join的用法
8. strings.Itoa(i int):把一个整数i转成字符串
9. strings.Atoi(str string)(int, error):把一个字符串转成整数
练习6:写一个函数分别演示Itoa、Atoi的用法
时间和日期类型
1. time包
2. time.Time类型,用来表示时间
3. 获取当前时间, now := time.Now()
4. time.Now().Day(),time.Now().Minute(),time.Now().Month(),time.Now().Year()
5. 格式化,fmt.Printf(“%02d/%02d%02d %02d:%02d:%02d”, now.Year()…)
6. time.Duration用来表示纳秒
7.一些常量
const (
Nanosecond Duration = 1
Microsecond = 1000 * Nanosecond
Millisecond = 1000 * Microsecond
Second = 1000 * Millisecond
Minute = 60 * Second
Hour = 60 * Minute
)
8. 格式化:
now := time.Now()
fmt.Println(now.Format(“02/1/2006 15:04”))
fmt.Println(now.Format(“2006/1/02 15:04”))
fmt.Println(now.Format(“2006/1/02”))
练习6:写一个程序,获取当前时间,并格式化成 2017/06/15 08:05:00形式
package main
import (
"fmt"
"time"
)
func main() {
now := time.Now()
fmt.Println(now.Format("2006/01/02 15:04:05"))
start := time.Now().UnixNano()
time.Sleep(time.Millisecond*100)
end := time.Now().UnixNano()
fmt.Printf("cost:%d us\n",(end-start)/1000)
}
指针类型
1. 普通类型,变量存的就是值,也叫值类型
2. 获取变量的地址,用&,比如: var a int, 获取a的地址:&a
3. 指针类型,变量存的是一个地址,这个地址存的才是值
4. 获取指针类型所指向的值,使用:*,比如:var *p int, 使用*p获取p指向的值
练习8:写一个程序,获取一个变量的地址,并打印到终端。
package main
import "fmt"
func main() {
var a int =10
fmt.Println(&a)
var p *int
p = &a
fmt.Println(*p)
*p = 100
fmt.Println(a)
var b int = 900
p = &b
*p = 5
fmt.Println(a)
fmt.Println(b)
}
流程控制
1. If / else分支判断
package main
import “fmt”
func main() {
bool1 := true
if bool1 {
fmt.Printf(“The value is true\n”)
} else {
fmt.Printf(“The value is false\n”)
}
}
2. switch case语句
switch var {
case var1:
case var2:
case var3:
default:
}
var i = 0
switch i {
case 0:
case 1:
fmt.Println(“1”)
case 2:
fmt.Println(“2”)
default:
fmt.Println(“def”)
}
var i = 0
switch i {
case 0:
fallthrough
case 1:
fmt.Println(“1”)
case 2:
fmt.Println(“2”)
default:
fmt.Println(“def”)
}
var i = 0
switch i {
case 0, 1:
fmt.Println(“1”)
case 2:
fmt.Println(“2”)
default:
fmt.Println(“def”)
}
var i = 0
switch {
condition1:
fmt.Println(“i > 0 and i < 10”)
condition2:
fmt.Println(“i > 10 and i < 20”)
default:
fmt.Println(“def”)
}
switch i := 0 {
case i > 0 && i < 10:
fmt.Println(“i > 0 and i < 10”)
case i > 10 && i < 20:
fmt.Println(“i > 10 and i < 20”)
default:
fmt.Println(“def”)
}
练习11:猜数字,写一个程序,随机生成一个0到100的整数n,然后用户在终端,输入数字,如果和n相等,则提示用户猜对了。如果不相等,则提示用户,大于或小于n。
package main
import (
"fmt"
"math/rand"
"time"
)
func main() {
var n int
rand.Seed(time.Now().UnixNano())
n = rand.Intn(100)
for {
var input int
fmt.Scanf("%d\n", &input)
flag := true
switch {
case input == n:
fmt.Println("right")
flag=false
case input>n:
fmt.Println("bigger")
case input<n:
fmt.Println("smaller")
}
if flag == false{
break
}
}
}
练习12:写一个程序,在终端打印如下图形
A
AA
AAA
AAAA
AAAAA
package main
import "fmt"
func print_n() {
var n int
fmt.Scanf("%d\n",&n)
for i := 1; i <n; i++ {
for j :=0; j<i; j++{
fmt.Printf("*")
}
fmt.Println()
}
}
func main() {
print_n()
}
for语句
for 条件 {
}
for i > 0 {
fmt.Println(“i > 0”)
}
for true {
fmt.Println(“i > 0”)
}
for {
fmt.Println(“i > 0”)
}
3. for range 语句
str := “hello world,中国”
for i, v := range str {
fmt.Printf(“index[%d] val[%c] len[%d]\n”, i, v, len([]byte(v)))
}
用来遍历数组、slice、map、chan
3. break continue语句
str := “hello world,中国”
for i, v := range str {
if i > 2 {
continue
}
if (i > 3) {
break }
fmt.Printf(“index[%d] val[%c] len[%d]\n”, i, v, len([]byte(v)))
}
4. goto 和 label 语句
package main
import "fmt"
func main() {
LABEL1:
for i := 0; i <= 5; i++ {
for j := 0; j <= 5; j++ {
if j == 4 {
continue LABEL1
}
fmt.Printf("i is: %d, and j is: %d\n", i, j)
}
}
}
package main
func main() {
i := 0
HERE:
print(i)
i++
if i == 5 {
return
}
goto HERE
}
2. golang函数特点:
a. 不支持重载,一个包不能有两个名字一样的函数
b. 函数是一等公民,函数也是一种类型,一个函数可以赋值给变量
c. 匿名函数
d. 多返回值
package main
import "fmt"
func add(a, b int) int {
return a + b
}
func main() {
c := add
fmt.Println(c)
sum := c(10, 20)
fmt.Println(sum)
if ( c == add ) {
fmt.Println(“c equal add”)
}
}
package main
import "fmt"
type add_func func(int, int) int
func add(a, b int) int {
return a + b
}
func operator(op add_func, a int, b int) int {
return op(a, b)
}
func main() {
c := add
fmt.Println(c)
sum := operator(c, 100, 200)
fmt.Println(sum)
}
package main
import "fmt"
type add_func func(int, int) int
func add(a, b, c int) int { // 错误,参数两个,返回三个
return a + b
}
func operator(op add_func, a int, b int) int {
return op(a, b)
}
func main() {
c := add
fmt.Println(c)
sum := operator(c, 100, 200)
fmt.Println(sum)
}
-
函数参数传递方式
-
1). 值传递 2). 引用传递 注意1:无论是值传递,还是引用传递,传递给函数的都是变量的副本,不过,值传递是值的拷贝。引用传递是地址的拷贝,一般来说,地址拷贝更为高效。而值拷贝取决于拷贝的对象大小,对象越大,则性能越低。 注意2:map、slice、chan、指针、interface默认以引用的方式传递 package main import "fmt" func modify(a int) { a = 100 } func main() { a := 8 fmt.Println(a) modify(a) fmt.Println(a) } 3. 函数参数传递方式: 练习13:修改上一页的程序,使其功能正确。 4. 命名返回值的名字: func add(a, b int) (c int) { c = a + b return} func calc(a, b int) (sum int, avg int) { sum = a + b avg = (a +b)/2 return}
-
_标识符,用来忽略返回值:
func calc(a, b int) (sum int, avg int) {
sum = a + b
avg = (a +b)/2
return}
func main() {
sum, _ := calc(100, 200)
}
- 可变参数:
func add(arg…int) int { 0个或多个参数
}
func add(a int, arg…int) int { 1个或多个参数
}
func add(a int, b int, arg…int) int { 2个或多个参数
}
注意:其中arg是一个slice,我们可以通过arg[index]依次访问所有参数
通过len(arg)来判断传递参数的个数
- defer用途:
1. 当函数返回时,执行defer语句。因此,可以用来做资源清理
2. 多个defer语句,按先进后出的方式执行
3. defer语句中的变量,在defer声明时就决定了。
func a() {
i := 0
defer fmt.Println(i)
i++
return
}
func f() {
for i := 0; i < 5; i++ {
defer fmt.Printf(“%d “, i)
}
}
1. 关闭文件句柄
func read() {
file := open(filename)
defer file.Close()
//文件操作
}
2. 锁资源释放
func read() {
mc.Lock()
defer mc.Unlock()
//其他操作
}
3. 数据库连接释放
func read() {
conn := openDatabase()
defer conn.Close()
//其他操作
}
一个或者多个操作在 CPU 执行的过程中不被中断的特性,称为原子性(atomicity) 。这些操作对外表现成一个不可分割的整体,他们要么都执行,要么都不执行,外界不会看到他们只执行到一半的状态
互斥锁是用来保护一段逻辑,原子操作用于对一个变量的更新保护。
需要注意的是,所有原子操作方法的被操作数形参必须是指针类型,通过指针变量可以获取被操作数在内存中的地址,从而施加特殊的CPU指令,确保同一时间只有一个goroutine能够进行操作。
atomic.Value保证任意值的读写安全
atomic
包里提供了一套Store
开头的方法,用来保证各种类型变量的并发写安全,避免其他操作读到了修改变量过程中的脏数据。
struct的定义
- struct 声明:
type 标识符 struct {
field1 type
field2 type
}
type Student struct {
Name string
Age int
Score int
}
2. struct 中字段访问:和其他语言一样,使用点
var stu Student
stu.Name = “tony”
stu.Age = 18
stu.Score=20
fmt.Printf(“name=%s age=%d score=%d”,
stu.Name, stu.Age, stu.Score
- struct定义的三种形式:
var stu Student
var stu *Student = new (Student)
var stu *Student = &Student{}
1)其中b和c返回的都是指向结构体的指针,访问形式如下:
a. stu.Name、stu.Age和stu.Score或者 (*stu).Name、(*stu).Age等
struct的初始化
4. struct的内存布局:struct中的所有字段在内存是连续的,布局如下:
5. 链表定义
type Student struct {
Name string
Next* Student
}
每个节点包含下一个节点的地址,这样把所有的节点串起来了,通常把
链表中的第一个节点叫做链表头
6. 双链表定义
type Student struct {
Name string
Next* Student
Prev* Student
}
如果有两个指针分别指向前一个节点和后一个节点,我们叫做双链表
7. 二叉树定义
type Student struct {
Name string
left* Student
right* Student
}
如果每个节点有两个指针分别用来指向左子树和右子树,我们把这样的
结构叫做二叉树
8. 结构体是用户单独定义的类型,不能和其他类型进行强制转换
type Student struct {
Number int
}
type Stu Student //alias
var a Student
a = Student(30)
var b Stu
a = b
9.golang中的struct没有构造函数,一般可以使用工厂模式来解决这个问题
Package model
type student struct {
Name stirng
Age int
}
func NewStudent(name string, age int) *student {
return &student{
Name:name,
Age:age,}
}
Package main
S := new (student)
S := model.NewStudent(“tony”, 20)
10. 再次强调:
make 用来创建map、slice、channel
new用来创建值类型
11. 我们可以为struct中的每个字段,写上一个tag。这个tag可以通过反射的
机制获取到,最常用的场景就是json序列化和反序列化
type Student struct {
Name string `json:"student_name"`
Age int `json:"age"`
Score int `json:"score"`
}
12. 结构体中字段可以没有名字,即匿名字段
type Car struct {
Name stirng
Age int
}
type Train struct {
Car
Start time.Time
int
}
13. 匿名字段冲突处理
type Car struct {
Name string
Age int
}
type Train struct {
Car
Start time.Time
Age int
}
type A struct {
a int
}
type B struct {
a int
b int
}
type C struct {
A
B
}
13. Golang中的方法是作用在特定类型的变量上,因此自定义类型,都可以
有方法,而不仅仅是struct
定义:func (recevier type) methodName(参数列表)(返回值列表){}
14. 方法的调用
type A struct {
a int
}
func (this A) test() { fmt.Println(this.a)
}
var t A
t.test()
15. 方法和函数的区别
函数调用: function(variable, 参数列表)
方法:variable.function(参数列表)
16. 指针receiver vs 值receiver
本质上和函数的值传递和地址传递是一样的
17. 方法的访问控制,通过大小写控制
大写能访问,小写不能
18. 继承
如果一个struct嵌套了另一个匿名结构体,那么这个结构可以直接访问
匿名结构体的方法,从而实现了继承。
19. 组合和匿名字段
如果一个struct嵌套了另一个匿名结构体,那么这个结构可以直接访问
匿名结构体的方法,从而实现了继承。
如果一个struct嵌套了另一个有名结构体,那么这个模式就叫组合。
20. 多重继承
如果一个struct嵌套了多个匿名结构体,那么这个结构可以直接访问
多个匿名结构体的方法,从而实现了多重继承。
21. 实现String()
如果一个变量实现了String()这个方法,那么fmt.Println默认会调用这个
变量的String()进行输出。
22.接口
Interface类型可以定义一组方法,但是这些不需要实现。并且interface不能
包含任何变量。
type example interface{
Method1(参数列表) 返回值列表
Method2(参数列表) 返回值列表
…
}
3. interface类型默认是一个指针
type example interface{
Method1(参数列表) 返回值列表
Method2(参数列表) 返回值列表
…
}
var a example
a.Method1()
4. 接口实现
a. Golang中的接口,不需要显示的实现。只要一个变量,含有接口类型中
的所有方法,那么这个变量就实现这个接口。因此,golang中没有implement
类似的关键字
b. 如果一个变量含有了多个interface类型的方法,那么这个变量就实现了多个
接口。
5. 接口实现
a. Golang中的接口,不需要显示的实现。只要一个变量,含有接口类型中
的所有方法,那么这个变量就实现这个接口。因此,golang中没有implement
类似的关键字
b. 如果一个变量含有了多个interface类型的方法,那么这个变量就实现了多个
接口。
c. 如果一个变量只含有了1个interface的方部分方法,那么这个变量没有实现
这个接口。
6. 多态
一种事物的多种形态,都可以按照统一的接口进行操作
7. 接口嵌套
一个接口可以嵌套在另外的接口,如下所示:
type ReadWrite interface {
Read(b Buffer) bool
Write(b Buffer) bool
}
type Lock interface {
Lock()
Unlock()
}
type File interface {
ReadWrite
Lock
Close()
}
8. 类型断言,由于接口是一般类型,不知道具体类型,如果要转成具体类型
可以采用以下方法进行转换:
var t int
var x interface{}
x = t
y = x.(int) //转成int
var t int
var x interface{}
x = t
y, ok = x.(int) //转成int,带检查
类型断言
9. 练习,写一个函数判断传入参数的类型
func classifier(items ...interface{}) {
for i, x := range items {
switch x.(type) {
case bool: fmt.Printf(“param #%d is a bool\n”, i)
case float64: fmt.Printf(“param #%d is a float64\n”, i)
case int, int64: fmt.Printf(“param #%d is an int\n”, i)
case nil: fmt.Printf(“param #%d is nil\n”, i)
case string: fmt.Printf(“param #%d is a string\n”, i)
default: fmt.Printf(“param #%d’s type is unknown\n”, i)
}
}
10. 类型断言,采用type switch方式
switch t := areaIntf.(type) {
case *Square:
fmt.Printf(“Type Square %T with value %v\n”, t, t)
case *Circle:
fmt.Printf(“Type Circle %T with value %v\n”, t, t)
case float32:
fmt.Printf(“Type float32 with value %v\n”, t)
case nil:
fmt.Println(“nil value: nothing to check?”)
default:
fmt.Printf(“Unexpected type %T”, t)
}
空接口
空接口没有任何方法,所以所有类型都实现了空接口。
var a int
var b interface{}
b = a
判断一个变量是否实现指定接口
type Stringer interface {String() string }
var v MyStruct
if sv, ok := v.(Stringer); ok {
fmt.Printf(“v implements String(): %s\n”, sv.String());
}
13. 指针类型和值类型的区别
定义:
A方法传一个参数p 至B方法后,当B方法改变了p的值
A方法中p值也改变了就了传指针
A方法中p值没改变就是传值
值类型的变量:
值类型的变量是直接指向内存中的值,值都是存储在栈中,当用等号进行赋值时,实际上是进行了一次拷贝, 如 i = j 实际上是进行了一次拷贝。
GO中值类型有int ,float,bool,string等,除了数组、切片、结构体、channel外都是值类型
& 操作符会生成一个指向其操作数的指针。
* 操作符表示指针指向的底层值。
package main
import (
"fmt"
)
func main() {
fmt.Println("#### 基本类型实验")
i := "4333qw"
fmt.Println("i = ",i)
fmt.Println("&i = ",&i)
fmt.Println("*&i = ",*&i)
}
运行结果:
#### 基本类型实验
i = 9999yt
&i = 0xc00000e1e0
*&i = 9999yt
1
2
3
4
基本类型赋值
实验内容:两个基本类型之间赋值后,其中一个修改不影响另一个,他们的指针地址是不同的,是存在两个地址空间
package main
import (
"fmt"
)
func main() {
fmt.Println("#### 基本类型赋值后的实验")
i := "4333qw"
j := "9999yt"
i = j
fmt.Println("i = ",i)
fmt.Println("j = ",j)
fmt.Println("&i = ",&i)
fmt.Println("&j = ",&j)
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
引用类型赋值
实验内容:两个引用类型之间赋值后,改变其中一个值另一个值也会跟着改变,这是指针赋值,两个指针指向的是同一块内存地址
从实验可以看出 user1 = user2 的结果是user1和user2指向相同的成员变量空间,如下面代码运行结果显示,user1,user2指向的name,age的存储空间地址是相同的,所以user1.name改变后user2.name也会根着变
package main
import (
"fmt"
)
func main() {
fmt.Println()
fmt.Println("#### 引用类型实验")
user1 := new(User)
user1.name = "wq"
user1.age = 33
user2 := new(User)
user2.name = "lq"
user2.age = 22
user1 = user2
user1.name = "zs"
fmt.Println("user1.name ",user1)
fmt.Println("user2.name ",user2)
fmt.Println("&user1 ",&user1)
fmt.Println("&user2 ",&user2)
fmt.Println("&user1.name ",&user1.name)
fmt.Println("&user2.name ",&user2.name)
fmt.Println("&user1.age ",&user1.age)
fmt.Println("&user2.age ",&user2.age)
}
type User struct {
name string
age int
}
运行结果:
#### 引用类型实验
user1.name &{zs 22}
user2.name &{zs 22}
&user1 0xc00000c030
&user2 0xc00000c038
&user1.name 0xc00000a0a0
&user2.name 0xc00000a0a0
&user1.age 0xc00000a0b0
&user2.age 0xc00000a0b0
1
2
3
4
5
6
7
8
9
复合引用类型的赋值
实验内容:引用类型嵌套引用类型,相互赋值后引用类型成员变量也同样是指向相同的地址空间
从实验可以看出 user1 = user2 的结果是user1和user2指向相同的成员变量空间,如下面代码运行结果显示,user1.cy,user2.cy指向的存储空间地址是相同的,所以user1.cy.name改变后user3.cy.name也会根着变
package main
import (
"fmt"
)
func main() {
fmt.Println()
fmt.Println("#### 复合引用类型实验")
user1 := new(User)
user1.name = "wq"
user1.age = 33
user1.cy.name = "shanghai"
user1.cy.area = 7000000
user2 := new(User)
user2.name = "lq"
user2.age = 22
user2.cy.name = "shenzheng"
user2.cy.area = 7000000
user1 = user2
user1.cy.name = "bj"
fmt.Println("user1.cy.name ",user1.cy.name)
fmt.Println("user2.cy.name ",user2.cy.name)
fmt.Println("&user1.cy ",&user1.cy)
fmt.Println("&user2.cy ",&user2.cy)
fmt.Println("&user1.cy.name ",&user1.cy.name)
fmt.Println("&user2.cy.name ",&user2.cy.name)
fmt.Println("&user1.cy.area ",&user1.cy.area)
fmt.Println("&user2.cy.area ",&user2.cy.area)
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
运行结果:
#### 复合引用类型实验
user1.cy.name bj
user2.cy.name bj
&user1.cy &{bj 7000000}
&user2.cy &{bj 7000000}
&user1.cy.name 0xc0000621c8
&user2.cy.name 0xc0000621c8
&user1.cy.area 0xc0000621d8
&user2.cy.area 0xc0000621d8
1
2
3
4
5
6
7
8
9
奇怪现现解析
碰到个奇怪的问题,一下掉到思维陷进了,其实并不是很复杂,下去抽根烟上来就想通了。
现像描述:
有个切片引用类型变量slice1, 分别传入两个方法,方法1改变了slice1外面没变,方法2改变了slice1外面变了,这纠竟是为什么呢?先上代码
package main
import (
"fmt"
)
func main() {
slice1 := []string{"zhang", "san"}
fmt.Printf("&slice1 %p\n", &slice1)
modify1(slice1)
fmt.Println(slice1)
fmt.Println("----------------")
modify2(slice1)
fmt.Println(slice1)
}
func modify1(data []string) {
fmt.Printf("modify1 &data %p\n",&data)
data = nil
}
func modify2(data []string) {
fmt.Printf("modify2 &data %p\n",&data)
data[1] = "lisi"
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
运行结果:
&slice1 0xc000088020
modify1 &data 0xc000088060
[zhang san]
------------------------------
modify2 &data 0xc0000880a0
[zhang lisi]
1
2
3
4
5
6
问题:上述代码中modify1执行后slice1为什么没有变成nil呢?难道传的不是引用吗?modify2改变后外面也改变了,对于引用类型来说,这个是正常的。
解析:其实上面的代码打印过程已经给出了答案,引用类型传过去的也是值,只不过这个值是指向存储空间的指针,这个指针会在内存中开辟一块新的空间来存储。所以上面运行结果中主方法中的slice1、modify1中的data、modify2中的data的地址是不一样的,但是他们所指向的存储空间是想同的。
接下来就是问题的重点了,为啥modify1中的data=nil没有影响到外面的slice1呢,其实很简单,下面我画个图来说明这个问题
16. interface{},接口中一个方法也没有,所以任何类型都实现了空接口,
也就是任何变量都可以赋值给空接口。
var a int
var b interface{}
b = a
17. 变量slice和接口slice之间赋值操作,for range
var a []int
var b []interface{}
b = a
反射:可以在运行时动态获取变量的相关信息
Import (“reflect”)
两个函数:
a. reflect.TypeOf,获取变量的类型,返回reflect.Type类型
b. reflect.ValueOf,获取变量的值,返回reflect.Value类型
c. reflect.Value.Kind,获取变量的类别,返回一个常量
d. reflect.Value.Interface(),转换成interface{}类型
2. reflect.Value.Kind()方法返回的常量
const (
Invalid Kind = iota
Bool
Int
Int8
Int16
Int32
Int64
Uint
Uint8
Uint16
Uint32
Uint64
Uintptr
Float32
Float64
Complex64
Complex128
Array
Chan
Func
Interface
Map
Ptr
Slice
String
Struct
UnsafePointer
)
反射练习
package main
import (
"fmt"
"reflect"
)
func main() {
var x float64 = 3.4
fmt.Println("type:", reflect.TypeOf(x))
v := reflect.ValueOf(x)
fmt.Println("value:", v)
fmt.Println("type:", v.Type())
fmt.Println("kind:", v.Kind())
fmt.Println("value:", v.Float())
fmt.Println(v.Interface())
fmt.Printf("value is %5.2e\n", v.Interface())
y := v.Interface().(float64)
fmt.Println(y)
}
获取变量的值
reflect.ValueOf(x).Float()
reflect.ValueOf(x).Int()
reflect.ValueOf(x).String()
reflect.ValueOf(x).Bool()
5. 通过反射的来改变变量的值
reflect.Value.SetXX相关方法,比如:
reflect.Value.SetFloat(),设置浮点数
reflect.Value.SetInt(),设置整数
reflect.Value.SetString(),设置字符串
package main
import (
"fmt"
"reflect"
)
func main() {
var a float64
fv := reflect.ValueOf(a)
fv.SetFloat(3.3)
fmt.Printf("%v\n", a)
}
7. 崩溃的原因
还是值类型和引用类型的原因
v := reflect.ValueOf(x)
v是x的一个拷贝,修改v,x不会修改!
传地址
package main
import (
"fmt"
"reflect"
)
func main() {
var a float64
fv := reflect.ValueOf(&a)
fv.Elem().SetFloat(3.3)
fmt.Printf("%v\n", a)
}
其中fv.Elem()用来获取指针指向的变量,相当于:
var a *int;
*a = 100
9. 用反射操作结构体
a. reflect.Value.NumField()获取结构体中字段的个数
b. reflect.Value.Method(n).Call来调用结构体中的方法
package main
import (
"fmt"
“reflect"
)
type NotknownType struct {
s1 string
s2 string
s3 string
}
func (n NotknownType) String() string {
return n.s1 + "-" + n.s2 + "-" + n.s3
}
var secret interface{} = NotknownType{"Ada", "Go", "Oberon"}
func main() {
value := reflect.ValueOf(secret) // <main.NotknownType Value>
typ := reflect.TypeOf(secret) // main.NotknownType
fmt.Println(typ)
knd := value.Kind() // struct
fmt.Println(knd)
for i := 0; i < value.NumField(); i++ {
fmt.Printf("Field %d: %v\n", i, value.Field(i))
//value.Field(i).SetString("C#")
}
results := value.Method(0).Call(nil)
fmt.Println(results) // [Ada - Go - Oberon]
}
10. 练习,通过反射操作结构体
package main
import (
"fmt"
“reflect"
)
type NotknownType struct {
s1 string
s2 string
s3 string
}
func (n NotknownType) String() string {
return n.s1 + "-" + n.s2 + "-" + n.s3
}
var secret interface{} = NotknownType{"Ada", "Go", "Oberon"}
func main() {
value := reflect.ValueOf(secret) // <main.NotknownType Value>
typ := reflect.TypeOf(secret) // main.NotknownType
fmt.Println(typ)
knd := value.Kind() // struct
fmt.Println(knd)
for i := 0; i < value.NumField(); i++ {
fmt.Printf("Field %d: %v\n", i, value.Field(i))
//value.Field(i).SetString("C#")
}
results := value.Method(0).Call(nil)
fmt.Println(results) // [Ada - Go - Oberon]
}
10. 练习2,通过反射修改结构体
package main
import (
"fmt"
"reflect"
)
type T struct {
A int
B string
}
func main() {
t := T{23, "skidoo"}
s := reflect.ValueOf(&t).Elem()
typeOfT := s.Type()
for i := 0; i < s.NumField(); i++ {
f := s.Field(i)
fmt.Printf("%d: %s %s = %v\n", i,
typeOfT.Field(i).Name, f.Type(), f.Interface())
}
s.Field(0).SetInt(77)
s.Field(1).SetString("Sunset Strip")
fmt.Println("t is now", t)
}
终端读写
1. 终端读写
操作终端相关文件句柄常量
os.Stdin:标准输入
os.Stdout:标准输出
os.Stderr:标准错误输出
终端读写
package main
import (
"fmt"
)
var (
firstName, lastName, s string
i int
f float32
input = "56.12 / 5212 / Go"
format = "%f / %d / %s"
)
func main() {
fmt.Println("Please enter your full name: ")
fmt.Scanln(&firstName, &lastName)
// fmt.Scanf("%s %s", &firstName, &lastName)
fmt.Printf("Hi %s %s!\n", firstName, lastName) // Hi Chris Naegels
fmt.Sscanf(input, format, &f, &i, &s)
fmt.Println("From the string we read: ", f, i, s)
}
带缓冲区
package main
import (
"bufio"
"fmt"
"osa"
)
var inputReader *bufio.Reader
var input string
var err error
func main() {
inputReader = bufio.NewReader(os.Stdin)
fmt.Println("Please enter some input: ")
input, err = inputReader.ReadString('\n')
if err == nil {
fmt.Printf("The input was: %s\n", input)
}
}
文件读写
package main
import (
"bufio"
"fmt"
"os"
)
func main() {
file,err := os.Open("E://test1.log")
if err != nil {
fmt.Println("read file err",err)
return
}
defer file.Close()
reader := bufio.NewReader(file)
str,err := reader.ReadString('\n')
if err != nil{
fmt.Println("read string failed ,err ",err)
return
}
fmt.Printf("read str success ,ret :%S\n",str)
}
打开一个文件进行读操作: os.Open(name string) (*File, error)
关闭一个文件:File.Close()
package main
import (
"bufio"
"fmt"
"io"
"os"
)
type CharCount struct {
ChCount int
NumCount int
SpaceCount int
OtherCount int
}
func main() {
file,err := os.Open("E://test1.log")
if err != nil{
fmt.Println("read file err ",err)
return
}
defer file.Close()
var count CharCount
reader := bufio.NewReader(file)
for {
str,err := reader.ReadString('\n')
if err == io.EOF{
break
}
if err != nil{
fmt.Printf("read file failed ,err%v",err)
break
}
runeArr := []rune(str)
for _,v := range runeArr{
switch {
case v> 'a' && v<= 'z':
fallthrough
case v> 'A' && v<= 'Z':
count.ChCount++
case v == ' ' || v == '\t':
count.NumCount++
default:
count.OtherCount++
}
}
}
fmt.Printf("char count :%d\n",count.ChCount)
fmt.Printf("num count :%d\n",count.NumCount)
fmt.Printf("space count :%d\n",count.SpaceCount)
fmt.Printf("other count :%d\n",count.OtherCount)
}
读取压缩文件示例
文件读写
文件写入
os.OpenFile(“output.dat”, os.O_WRONLY|os.O_CREATE, 0666)
第二个参数:文件打开模式:
1. os.O_WRONLY:只写
2. os.O_CREATE:创建文件
3. os.O_RDONLY:只读
4. os.O_RDWR:读写
5. os.O_TRUNC :清空
第三个参数:权限控制:
r ——> 004
w——> 002
x——> 001
package main
import (
"bufio"
"fmt"
"os"
)
func main() {
outputFile,outputError := os.OpenFile("output.dat",os.O_WRONLY|os.O_CREATE,0666)
if outputError != nil {
fmt.Printf("an error occurred with file creation\n")
return
}
defer outputFile.Close()
outputWriter := bufio.NewWriter(outputFile)
outputString := "hello world\n"
for i := 0; i < 10; i++ {
outputWriter.WriteString(outputString)
}
outputWriter.Flush()
}
拷贝文件
package main
import (
"io"
"os"
)
func CopyFile(dstName,srcName string)(written int64,err error) {
src,err := os.Open(srcName)
if err != nil{
return
}
defer src.Close()
dst,err := os.OpenFile(dstName,os.O_WRONLY|os.O_CREATE,0644)
if err != nil{
return
}
defer dst.Close()
return io.Copy(dst,src)
}
os.Args是一个string的切片,用来存储所有的命令行参数
flag包的使用,用来解析命令行参数:
flag.BoolVar(&test, "b", false, "print on newline")
flag.StringVar(&str, "s", "", "print on newline")
flag.IntVar(&count, "c", 1001, "print on newline")
同步
多个控制共享一个资源
线程同步机制
互斥锁:建议锁,拿到锁才能访问数据,没有拿到锁的线程,阻塞等待,等到拿到锁的线程释放锁
读写锁:一把锁,写独占,读共享,写锁优先级高
协程
轻量级线程
Goexit
runtime.GOMAXPROCS()
channel俩个端
一端:写端(传入端)chan<-
另一端:读端 <- chan
进阶一点
指针就是地址,指针变量就是存储地址的变量
*p 解引用,间接引用
栈帧:用来给函数运行时提供内存空间,取内存与stack上
当函数调用时,产生栈帧,函数调用结束,释放栈帧
栈帧存储,1.局部变量,2.形参,(形参与局部变量存储地位等同),3.字段描述值
变量存储
等号左边变量,代表变量所指向的内存空间(写)
等号右边变量,代表变量存储空间存储的数据值
指针的函数传参
传地址:将形参地址值作为函数参数的传递
传值:将实参的值拷贝一份给形参
传引用,在A栈帧内部,修改B栈帧的变量值
为什么用切片
数组容量固定,不能自动拓展。
值传递,数组作为函数参数,,将整个数组拷贝一份给形参
go语言当,我们几乎可以在所有场景中,使用切片替换数组使用
切片本质:不是一个数组指针,是一个数据结构体,用来操作数组内部元素
runtime/slice.go type slice struct {
*p
len
cap // 数组容量
}
截取数组,初始化切片时,切片容量跟随原数组
切片的使用: cap
}
数组和切片定义区别:
创建数组时 [ ] 指定数组长度。
创建切片时, [] 为空,或者 ...
切片名称 [ low : high : max ]
low: 起始下标位置
high:结束下标位置 len = high - low
容量:cap = max - low
截取数组,初始化切片,没有指定切片容量,切片容量随原数组(切片)
s[:high:max] 从0开始,到high结束
s[low:] : 从low 开始,到 末尾
s[: high]: 从 0 开始,到 high结束。容量跟随原先容量。【常用】
切片创建:
1. 自动推导类型创建 切片。slice := []int {1, 2, 4, 6}
2. slice := make([]int, 长度,容量)
3. slice := make([]int, 长度) 创建切片时,没有指定容量, 容量== 长度。【常用】
切片做函数参数-传引用
append
map 字典,映射
map 不能使用cap
创建方式
var m1 map[int]string 不能存储数据
m2 := map[int]string{} 能存储数据
m2 := make(map[int]string) 默认是长度0
m3 := make(map[int]string,5)
初始化
1. var m map[int]string = map[int]string{ 1: "aaa", 2:"bbb"} 保证key彼此不重复。
2. m := map[int]string{ 1: "aaa", 2:"bbb"}
赋值
赋值过程中,如果map元素key与原来的map元素key相同,替换
map使用
遍历map
for key[i] value := range map{
}
for key值 := range.map{
}
判断mapKey 是否存在
map[下标]: 运算,返回两个值,第一个表value 的值,如果value不存在,nil
第二个表,key是否存在bool类型,存在是true,不存在就是false
删除map中的一个元素
delete
delete() 函数
参数1 待删除元素的map
参数2 key 值
结构体
是一种数据类型
type Person struct{
name string
sex byte
age int
}
普通变量定义和初始化
顺序初始化:依次将结构体内部成员初始化
var man Person = Person{“aa”,”man”,20}
指定成员初始化
man := Person{name:”aa”,age:18}
索引成员变量 用 “:”
结构体变量的比较和赋值
比较 只能用== !=
相同类型结构体赋值
结构体传参
结构体变量的值拷贝一份 几乎不用
指针变量定义初始化
顺序初始化:依次将结构体内部所有成员初始化
var man Person = Person{name: "aa", age: 10,sex:'m'}
结构体指针做函数返回值
不能返回局部变量地址值–局部变量保存在栈帧上,函数调用之后,栈帧释放,局部变量地址,不再受系统保护,可能随时分配给其他程序
打开创建文件
创建文件,文件不存在,文件存在,将文件内容清空
打开文件 以只读方式打开
打开文件openFile 可以以只读,只写,读写方式
421
读写执行
文件
按字符串写
Seek()修改文件读写指针位置
参数 偏移量;向文件尾偏,负,向文件头偏
io.SeekEnd
io.SeekStart
io.SeekCurrent
返回值:表示从文件起始位置,文件偏移量
writeAt() 在文件指定偏移位置,写入[]byte 通常搭配seek()
参数1:待写入的数据
偏移量
返回,实际写出字节数
按字节写
文件
按行读
创建一个带有缓冲区的Reader
bufio.NewReader (打开文件指针)
2.从Reader缓冲区中,读取指定长度的数据,数据的长度取决于参数dlime
buf,err :=reader.ReadBytes('\n')
判断到达文件结尾 if err != nil &&err == io.EOF 达到文件结尾
目录操作
打开目录:OpenFile
name 打开文件路径,绝对路径,
打开文件权限 O_RDONLY O_WRONLY O_RDWR
os.ModeDir
Readdir(n int) 读几个目录项
type FileInfo interface {
Name() string // base name of the file
Size() int64 // length in bytes for regular files; system-dependent for others
Mode() FileMode // file mode bits
ModTime() time.Time // modification time
IsDir() bool // abbreviation for Mode().IsDir()
Sys() interface{} // underlying data source (can return nil)
}
并行
宏观:那就是用户体验上
微观:那就是cpu多核运行多条命令
进程并发
程序:编译成功的到二进制文件
进程:运行起来的程序,占用系统资源
进程太
进程基本的状态有5种。分别为初始态,就绪态,运行态,挂起态与终止态
Goroutine特性
主go结束,子go程随之退出
runtime.Goexit():
return: 返回当前函数调用到调用者那里去。 return之前的 defer 注册生效。
Goexit(): 结束调用该函数的当前go程。Goexit():之前注册的 defer都生效。
runtime..Gosched()
channel
是一种数据类型,对应一个管道
channel定义:
make(chan 在channel 传递数据类型,荣亮亮)
make( chan int)
ch := make(chan string)
写端 ch<-“hello” 如果没有在读端读,写端阻塞
读端:str := <-ch 读端读数据,写段不在写,读端阻塞
len(ch) channel 中剩余未读取个数, cap(ch) :通道容量
channel 同步,数据传递
无缓冲channel: 同步通信
通道容量是0 len = 0 不能存储数据
channel 应用于两个go程,一个读,一个写
具备同步能力,读,写同步
有缓冲channel –异步通信
通道是非0 len(ch) channel 中剩余未读取数据个数 cap(ch) :通道容量
channel 应用于两个go程中,一个读,一个写
缓冲区可以进行到数据存储,存储容量上限,阻塞,具备异步能力,不需要同时操作channel缓冲区
关闭channel
使用close 关闭channel
对端可以判断channel是否关闭
如果对端已经关闭,ok–false num无数据
如果对端没有关闭,ok–true num保存读到的数据
数据不发送完,不应该关闭
已经关闭的channel 不能向其写数据,报错
写端已经关闭channel,可以从中读取数据,读到0,说明–写端关闭
读无缓冲channel读到0,说明,写端关闭
读有缓冲channel:如果缓冲区有数据,先读数据,读数据后,可以继续读,读到0
单向channel
默认channel是双向的
单向写channel var sendCh chan <<- int
单向读channel var revCh <-chan int
转换
双向channel可以把隐式转换为任意一种单向channel
单向channel不能转换为双向channel
生产者消费者
缓冲区:降低生产着消费者之间耦合度,解耦
package main
import "fmt"
type OrderInfo struct { // 创建结构体类型OrderInfo,只有一个id 成员
id int
}
func producer2(out chan <- OrderInfo) { // 生成订单——生产者
for i:=0; i<10; i++ { // 循环生成10份订单
order := OrderInfo{id: i+1}
out <- order // 写入channel
}
close(out) // 写完,关闭channel
}
func consumer2(in <- chan OrderInfo) { // 处理订单——消费者
for order := range in { // 从channel 取出订单
fmt.Println("订单id为:", order.id) // 模拟处理订单
}
}
func main() {
ch := make(chan OrderInfo) // 定义一个双向 channel, 指定数据类型为OrderInfo
go producer2(ch) // 建新协程,传只写channel
consumer2(ch) // 主协程,传只读channel
}
定时器
Timer :创建定时器,指定时间时长,定时到达后,系统自动向定时器成员C系统当前时间
1)创建定时器myTimer := time.NewTimer(2 * time.Second)
2) 停止: myTimer.Stop —— 将定时器归零。 <-myTimer.C 会阻塞
3) 重置:myTimer.Reset(time.Second)
周期定时:
type Ticker struct {
C <-chan Time
r runtimeTimer
}
1) 创建周期定时器 myTicker := time.NewTicker(time.Second)
定时时长到达后,系统会自动向 Ticker 的 C 中写入 系统当前时间。
并且,每隔一个定时时长后,循环写入 系统当前时间。
2) 在 子 go 程中循环读取 C。获取系统写入的时间。
select 超时处理
select
select 监听 time.After() 中 channel 的读事件。 如果定时时间到,系统会向该channel中写入系统当前时间。
select {
case <-time.After(time.Second * 5)
定时到达后,要处理的内容
}
死锁: 不是锁的一种!!!是一种错误使用锁导致的现象。
1. 单go程自己死锁
channel 应该在 至少 2 个以上的 go程中进行通信。否则死锁!!!
2. go程间channel访问顺序导致死锁
使用channel一端读(写), 要保证另一端写(读)操作,同时有机会执行。否则死锁。
3. 多go程,多channel 交叉死锁
Ago程,掌握M的同时,尝试拿N; Bgo程,掌握N的同时尝试拿M。
4. 在go语言中,尽量不要将 互斥锁、读写锁 与 channel 混用。 —— 隐性死锁。
互斥锁:(互斥量)
A 、B go程 共同访问共享数据。 由于cpu调度随机,需要对 共享数据访问顺序加以限定(同步)。
创建 mutex(互斥锁),访问共享数据之前,加锁,访问结束,解锁。 在Ago程加锁期间,B go程加锁会失败——阻塞。
直至 A go程 解说mutex,B 从阻塞处。恢复执行。
读写锁:
读时共享,写时独占。写锁优先级比读锁高。
条件变量
条件变量
本身不是锁,但经常与锁结合使用
使用流程
1.创建条件变量
2.指定条件变量使用锁
4.condLLock 给公共区加锁
4.判断是否到达阻塞条件–for循环判断
for len(ch) == cap(ch) cond.,Wait( 1.阻塞,2.解锁,2.加锁)
5.访问公共区–读写数据打印
6.解锁条件变量用的锁 cond.L.Unlock
7.唤醒阻塞在条件变量的消费者
网络分层架构
osi七层模型:物数网传会表应
tcp/ip四层模型:数据层,网络层,传输层,应用层
链路层: ARP
源mac —— 目标mac
ARP 协议作用: 借助 IP 获取 mac 地址。
网络层: IP
源IP —— 目标IP
IP协议的作用: 在 网络环境中唯一标识一台主机。
IP地址本质:2进制数。—— 点分十进制 IP地址 (string)
传输层: TCP / UDP
port —— 在 一台主机上唯一标识一个进程。
应用层: ftp、http、自定义
对数据进行封装。 解封装。
数据通信过程
封装:应用层-传输层–网络层–链路层,没有经过封装的数据,不能在网络环境中传递
解封装:链路层–网络层–传输层–应用层
总结通信过程:
1. mac地址(不需要用户指定) (ARP 协议)Ip ——> mac
2. IP 地址 (需要用户指定) —— 确定主机
3. port 端口号 (需要用户指定) —— 确定程序
1. 不能使用系统占用的默认端口。 5000+ 端口我们使用 (8080)
2. 65535为端口上限。
socket: 套接字。
网络通信过程中,socket 一定是成对出现的。
网络应用设计模式:
C/S:
优点:数据传输效率高、协议选择灵活
缺点:工作量大、安全性构成威胁
B/S:
优点:开发工作较小、不受平台限制、安全威胁小
缺点:缓存数据差、协议选择不灵活、
TCP-CS服务器:
1. 创建监听socket listener := net.Listen("TCP", "IP+port") IP+port —— 服务器自己的IP 和 port
2. 启动监听 conn := listener.Accept() conn 用于 通信的 socket
3. conn.Read()
4. 处理使用 数据
5. conn.Write()
6. 关闭 listener、conn
nc工具环境变量配置。
1. 解压 netcat-win32-1.12.zip 文件 到指定目录
2. 拷贝 解压后目录,保存到环境变量:
方法:我的电脑-->属性-->高级系统设置-->环境变量-->系统变量中找“path”-->双击它-->新建-->粘贴
3. 启动 cmd 执行 nc 命令 充当客户端测试
4. nc 127.0.01 8000 (注意 IP 和 端口之间是“空格”)
5. 输入 hello socket。 服务器应该能读到并向服务器的屏幕打印 “hello socket”
TCP-CS客户端:
1. conn, err := net.Dial("TCP", 服务器的IP+port)
2. 写数据给 服务器 conn.Write()
3. 读取服务器回发的 数据 conn.Read()
4. conn.Close()
TCP-CS并发服务器:
1. 创建 监听套接字 listener := net.Listen("tcp", 服务器的IP+port) // tcp 不能大写
2. defer listener.Close()
3. for 循环 阻塞监听 客户端连接事件 conn := listener.Accept()
4. 创建 go程 对应每一个 客户端进行数据通信 go HandlerConnet()
5. 实现 HandlerConnet(conn net.Conn)
1) defer conn.Close()
2) 获取成功连接的客户端 Addr conn.RemoteAddr()
3) for 循环 读取 客户端发送数据 conn.Read(buf)
4) 处理数据 小 —— 大 strings.ToUpper()
5)回写转化后的数据 conn.Write(buf[:n])
服务器判断关闭:
Read读客户端/服务器,返回 0 —— 对端关闭!
nc 命令发送数据时,默认在结尾自带‘\n’
TCP-CS并发客户端:
1. 匿名 go 程 , 获取 键盘输入, 写给服务器
2. for 循环读取服务器回发数据
发送数据时,默认在结尾自带‘ \r\n ’
TCP通信过程:
三次握手:
1. 主动发起请求端, 发送 SYN
2. 被动建立连接请求端 , 应答ACK 同时 发送 SYN
3. 主动发起请求端,发送应答 ACK
标志 TCP 三次握手建立完成。 —— server:Accept() 返回 。 —— client:Dial() 返回。
四次挥手:
1. 主动关闭连接请求端, 发送 FIN
2. 被动关闭连接请求端 ,应答 ACK
标志。半关闭完成。 —— close()
3. 被动关闭连接请求端 ,发送 FIN
4. 主动关闭连接请求端,应答 ACK
标志。四次挥手建立完成。 —— close().
TCP状态转换图:
主动发起连接请求端: CLOSED —— 完成三次握手 —— ESTABLISEHED(数据通信状态)—— Dial()函数返回
被动发起连接请求端: CLOSED —— 调用Accept()函数 —— LISTEN —— 完成三次握手 —— ESTABLISEHED (数据通信状态)—— Accept()函数返回
数据传递期间 —— ESTABLISEHED (数据通信状态)
主动关闭连接请求端:
ESTABLISEHED —— FIN_WAIT_2 (半关闭)—— TIME_WAIT —— 2MSL —— 确认最后一个ACK被对端成功接收。—— CLOSE
半关闭、TIME_WAIT、2MSL ——只会出现在 “主动关闭连接请求端”
被动关闭连接请求端:ESTABLISEHED —— CLOSE
查看状态命令:
windows:netstat -an | findstr 8001(端口号)
Linux: netstat -apn | grep 8001
tcp 面向连接的可靠的传输
udp通信
无连接的,不可靠的报文传递
udp服务器
1.创建server端地址结构(IP+port)ret.ResolveUDPAddr()
defer udpCoon.Close()
读取客户端发送数据 ReadFromUDP(buf) 返回: n, cltAddr(客户端的IP+port) , err
写数据给客户端 WriteToUDP("待写数据",cltAddr)
网络文件传输
命令行参数:在main函数启动时,向整个程序传参
go run xxxx.go argv1,argv2….
文件传输——发送端(客户端):
1. 提示用户使用命令行参数输入文件名。接收文件名 filepath(含访问路径)
2. 使用 os.Stat()获取文件属性,得到纯文件名 fileName(去除访问路径)
3. 主动发起连接服务器请求,结束时关闭连接。
4. 发送文件名到接收端 conn.Write()
5. 读取接收端回发的确认数据 conn.Read()
6. 判断是否为“ok”。如果是,封装函数 SendFile() 发送文件内容。传参 filePath 和 conn
7. 只读 Open 文件, 结束时Close文件
8. 循环读本地文件,读到 EOF,读取完毕。
9. 将读到的内容原封不动 conn.Write 给接收端(服务器)
文件传输——接收端(服务器):
1. 创建监听 listener,程序结束时关闭。
2. 阻塞等待客户端连接 conn,程序结束时关闭conn。
3. 读取客户端发送文件名。保存 fileName。
4. 回发“ok”。
5. 封装函数 RecvFile 接收客户端发送的文件内容。传参 fileName 和 conn
6. 按文件名 Create 文件,结束时 Close
7. 循环 Read 发送端网络文件内容,当读到 0 说明文件读取完毕。
8. 将读到的内容原封不动Write到创建的文件中
聊天室
主go程中,创建监听套接字,deferfor循环监听客户端连接请求,Accept()
有一个客户端连接,创建新的go程处理客户端数据
定义全局类型结构 Name,Addres
创建全局map
实现HandelCoonect 获取客户端ip+port,remotAddr,创建新用户结构体信息,name=addr
创建Manager 实现管理go程--accept()之前
实现manager,初始化在线用户map,循环读取全局channel,
将新用户添加到在线用户map汇总,key==ip +port
创建writeMessageToClient,专门给当前用户写数据,来源用户自带的
实现writeMessageToClient(client,conn) 遍历自带的C,读数据,conn.Write客户端
HandelCoonect 结束位置,组织用户上线信息,发送用户信息写到全局channel
广播用户消息,封装函数MakeMsg()来处理光波,用户消息
HandlerConnect,创建匿名go程,读取用户socket上发送聊天内容,写到全局channel
for循环conn.Read
写给全局message,后续广播用户上线
查询在线用户
将读取到的用户信息msg结尾 的\n去掉
判断是否是who的命令
如果是,;遍历用户在线列表组织信息,写到socket
如果不是,写给全局message
修改用户名
将读到的用户消息msg判断是否包含 rename
提取|后面的字符串,存到client的Name成员中
更新在线用户列表,onlineMap key,ip+port
提示用户更新完成,conn.write
用户退出
1.用户成功登录,创建监听用户退出的channel--ISQuit
当conn.Read == 0 isQuit
当HandlerConnect结尾for中,添加select监听<- isQuit
条件满足。 将用户从在线列表移除。 组织用户下线消息,写入 message (广播)
超时强踢
在select中监听定时器,计时到达,将用户从在线列表移除,组织用户下线消息,写入message
1.创建监听用户超时channel-hasdata
只用户执行,聊天,改名,who任意一个操作,hasdata<-true
在select中添加监听<-hasDate,条件满足,不做任何事情
http应答包
使用net包创建web服务器
注册回调函数
http.handlerFunc(“itcast”,handler)
参数1:用户访问位置
回调函数名-函数必须是()
绑定服务器监听地址
htttp.ListenAndSerbve(“127.0.0.1:8000”,nil)
回调函数
爬虫
访问web数据
流程
明确目标url,f
发送请求,获取应答包
保存过滤数据,提取有用信息
使用,分析得到的数据信息
百度贴吧爬虫,起始页,终止页
使用start,end循环爬取每一页数据
获取每一页url下一页等于前一页
封装实现httpget,爬取一个网页数据内容
创建html 使用循环因子命名
将result 写入文件 writeString f.close
封装爬取一个网页内容代码到spider(index)函数中
在working 函数循环启动调用spiderpage n个待爬取的页面对应n个 go 程
为防止主go程提前结束,引入channel 实现同步,Spider‘Page(index,channel)
在spiderPage结尾处,channel 中写内容channel<- index
在working函数天极爱新的for循环从channel不断读取各个子go程中写入数据,n个子go程,写n次channel
正则表达式
? 匹配前面单元出现0-1次
- 匹配前面单元出现 1-N次
- 匹配前面单元出现0-N次
-
解析编译正则表达式:
MustCompile(str string) *Regexp
参数:正则表达式: 建议使用“反引号”——
返回值: 编译后的正则表达式 (结构体类型)
-
提取需要的数据:
func (re *Regexp) FindAllStringSubmatch(s string, n int) [][]string
参数1:待匹配的字符串。
参数2:匹配次数。 -1 表匹配全部
返回值: 存储匹配结果的 [ ][ ]string
数组的每一个成员都有 string1 和 string2 两个元素。
string1:表示, 带有匹配参考项的字符串。 【0】 string2:表示,不包含匹配参考项的字符串内容。【1】
-
----- 提取网页标签数据:
举例: 提取 <div></div> 之中的数据
1) <div>(.*)</div> --> 可以提取div标签的内容
2) 对于 div 标签中 含有多行内容清空:
正则表达式:(?s:(.*?))
双向爬取
横向:以页为单位
纵向:以一个页面的条目为单位爬取