前言
接触了新的语言go,记录一下学习的笔记方便日后温故知新。
一、channel(管道)1.channel的本质就是数据结构:队列
2.数据是先进先出
3.线程安全,channel本身是线程安全的
4.channel是有类型的,string的channel只能放string,结构体类型的channel只能放结构体
5.快速入门
使用:
初始化一个可以存放三个int类型的channel
func main() {
//初始化一个可以存放三个int类型的channel
var intChan chan int
//管道的容量和map不一样,管道的容量不能自动扩容,所以要随用随取,及时取出管道里面的数据,存取数据遵从先进先出原则
intChan = make(chan int, 3)
fmt.Printf("intChan的值是 %v,intChan的地址是 %p \n", intChan, &intChan)
//向管道中追加数据
intChan <- 10
num := 3
intChan <- num
fmt.Printf("intChan len is %v, intChan cap is %v \n", len(intChan), cap(intChan))
//在没有使用协程的情况下,如果管道的数据取完了,再取就会报错deadlock
num2 := <-intChan
fmt.Println("num2 = ", num2)
}
6.管道的关闭
使用内置的函数 close 可以关闭管道,当管道关闭后,这个管道就只能读不能写了
func main() {
intChan := make(chan int, 3)
intChan <- 10
intChan <- 20
close(intChan) //panic: send on closed channel
intChan <- 30
}
7.管道的遍历
func main() {
intChan := make(chan int, 3)
intChan <- 10
intChan <- 20
//在遍历channel时.如果没关闭channel则会出现死锁,只有关闭channel之后才能正常遍历,并且遍历管道不能使用经典for,需使用 foreach
close(intChan) //panic: send on closed channel
fmt.Println("before for intChan len is ", len(intChan))
for v := range intChan {
fmt.Println(v)
}
fmt.Println("after for intChan len is ", len(intChan))
//before foreach intChan len is 2
//10
//20
//after foreach intChan len is 0
}
8.使用管道解决协程的资源竞争问题
1.开启一个协程,向管道中写入50个整数,并开启另一个协程来读取上一个协程写入的整数,两个协程 *** 作同一个管道,主线程等两个协程结束后,退出
(数据量更改为1000并开启10个逻辑cpu才能看到并行执行的效果)
思路:
两个管道,其中一个管道由协程不停的读写,另一个管道做标志位来阻塞主线程
func writeData(intChan chan int) {
for i := 0; i < 1000; i++ {
//放入数据
intChan <- i
fmt.Println("写入数据 = ", i)
}
//写完之后关闭管道,一定要关闭否则读的时候会死锁
close(intChan)
}
func readData(intChan, exitChan chan int) {
for {
//如果还存在数据 ok 就是true
v, ok := <-intChan
if ok {
fmt.Println("读取到数据 : ", v)
} else {
break
}
}
//任务完成
exitChan <- 1
close(exitChan)
}
func main() {
runtime.GOMAXPROCS(10)
intChan := make(chan int, 1000)
exitChan := make(chan int, 1)
go readData(intChan, exitChan)
go writeData(intChan)
for {
v := <-exitChan
if v == 1 {
break
}
}
}
小练习 启动一个写协程写入 1~2000个数字,然后启动8个读线程,计算取出的数字的累加,计算后把结果以map的形式放入到管道当中最后遍历这个管道
func write(writeChan chan int) {
for i := 1; i <= 200; i++ {
writeChan <- i
}
close(writeChan)
}
func read(readChan chan int, exitChan chan int, numChan chan map[int]int) {
for {
num, ok := <-readChan
res := 0
if ok {
for i := 1; i <= num; i++ {
res += i
}
m := make(map[int]int)
m[num] = res
numChan <- m
} else {
v := <-exitChan
if v == 1 {
close(numChan)
}
break
}
}
}
func main() {
//小练习 启动一个写协程写入 1~2000个数字,然后启动8个读线程,计算取出的数字的累加,计算后把结果以map的形式放入到管道当中最后遍历这个管道
intChan := make(chan int, 200)
exitChan := make(chan int, 1)
numChan := make(chan map[int]int, 200)
go write(intChan)
for i := 0; i < 8; i++ {
go read(intChan, exitChan, numChan)
}
for {
if len(numChan) == 200 {
exitChan <- 1
break
}
}
fmt.Println("numChan is finish the len is ", len(numChan))
for m := range numChan {
fmt.Println(m)
}
fmt.Println("numChan has foreach the len is ", len(numChan))
}
9.管道的阻塞
当管道的容量小于数据的量的时候,会产生阻塞,但如果是边写边读的话,go分析有协程大量的写,即便只有协程读的很慢,也不会报错
如果编译器发现一个管道只有写没有读则会发生阻塞,如果管道的读写频率不一致是可以正常跑的
10.计算1~20000哪些是素数
传统方法,使用一个循环,循环判断是不是素数
使用并发/并行来计算将计算素数的任务分配给多个协程去完成
思路:
建立一个管道放入数据
写一个函数,入参为管道,取出一个数,看是否是素数,是的话就放入另一个管道,直到取不出数据的时候,放flag管道一个标志
设置多核然后for cpuNum - 1 来执行求素数函数
写一个函数,入参是管道,for 判断管道的len,当管道的len == cpuNum - 1 时候,break 所有协程,打印素数,退出main
// 获取素数
// num := 100
//flag:
// for i := 2; i <= num; i++ {
// for j := 2; j < i; j++ {
// if i%j == 0 {
// continue flag
// }
// }
// }
计算1~20000哪些是素数:
//这个协程放入需要处理的数据
//func putNum(intChan chan int) {
// for i := 0; i < 100000; i++ {
// intChan <- i
// }
// close(intChan)
//}
//获取素数的协程
func primeNumberFunc(intChan, primeNumber chan int, flagChan chan bool) {
for {
num, ok := <-intChan
if !ok {
break
}
isPrimeNumber := false
for i := 2; i < num; i++ {
if num%i == 0 {
//不是素数
isPrimeNumber = true
break
}
}
if !isPrimeNumber {
//是素数
primeNumber <- num
}
}
//跳出循环,已经取不到数据了
flagChan <- true
}
func main() {
//计算1~20000哪些是素数
//数据管道
intChan := make(chan int, 1000)
//结果管道
primeNumber := make(chan int, 2000)
//标志位管道
flagChan := make(chan bool, 4)
start := time.Now().Unix()
//go putNum(intChan)
for i := 2; i < 1000; i++ {
intChan <- i
}
close(intChan)
for i := 0; i < 4; i++ {
go primeNumberFunc(intChan, primeNumber, flagChan)
}
//主线程阻塞等待<-flagChan管道可以取出四个值的时候完成循环否则就一直阻塞
go func() {
for i := 0; i < 4; i++ {
<-flagChan
}
end := time.Now().Unix()
fmt.Println("消耗时间为:", end-start)
close(primeNumber)
}()
for {
res, ok := <-primeNumber
if !ok {
break
}
fmt.Println("素数有:", res)
}
fmt.Println("主线程退出")
}
结论:
使用go协程后,效率至少提高 cpu数量的倍数
管道的注意事项:
管道可以声明为只读或只写
func main() {
//默认情况下管道是双向的,可读可写
//使用场景,此时管道可以作为方法的入参只允许方法对这个管道读或写,效率上更好并且可以防止误 *** 作,是管道的一种属性,我们可以把双向的通道传递到只读或只写的入参
//声明只写管道
var writeChan chan<- int
writeChan = make(chan int, 1)
writeChan <- 1
//声明只读管道
var readChan <-chan int
readChan = make(chan int, 1)
<-readChan
}
//使用select可以解决从管道中取数据而不用关闭管道的问题
func main() {
intChan := make(chan int, 10)
for i := 0; i < 10; i++ {
intChan <- i
}
stringChan := make(chan string, 5)
for i := 0; i < 5; i++ {
stringChan <- "hello" + fmt.Sprintf("%d", i)
}
//传统方法遍历管道如果不关闭会阻塞而导致死锁deadlock,当我们不确定什么时候关闭管道的时候,我们可以用select
//flag:
for {
select {
//如果我们取不到数据了,会向下继续执行case
case v := <-intChan:
fmt.Println("我们在管道未关闭的情况下取到了数据:", v)
case v := <-stringChan:
fmt.Println("我们在管道未关闭的情况下取到了数据:", v)
default:
fmt.Println("什么都取不到了,执行自己的业务逻辑,比如 return 或 break 退出当前函数,目前是在main函数,那么就是退出程序了")
return
//break flag
}
}
//如果使用return,则fmt.Println("hahaha ") 永远不可达
fmt.Println("hahaha ")
}
//在协程中捕获 panic 保证程序可以继续执行
func sayHello() {
for i := 0; i < 10; i++ {
time.Sleep(time.Second)
fmt.Println("hello world")
}
}
func test() {
//panic: assignment to entry in nil map
//触发panic,未对map进行make就使用,最终会影响main函数运行,这里加入 defer 和 recover 来解决
//var myMap map[int]string
//myMap[1] = "hello"
defer func() {
//捕获test()抛出的panic
if err := recover(); err != nil {
fmt.Println("func test() panic is", err)
}
}()
var myMap map[int]string
myMap[1] = "hello"
}
func main() {
go sayHello()
go test()
for i := 0; i < 10; i++ {
time.Sleep(time.Second)
fmt.Println("main() is ok ...")
}
}
//func test() panic is assignment to entry in nil map
//main() is ok ...
//hello world
//hello world
二、反射
1.对基本数据类型的反射 *** 作
//适配器函数
//package reflect
//import "reflect"
//
//reflect包实现了运行时反射,允许程序 *** 作任意类型的对象。典型用法是用静态类型interface{}保存一个值,通过调用TypeOf获取其动态类型信息,该函数返回一个Type类型值。调用ValueOf函数返回一个Value类型值,该值代表运行时的数据。Zero接受一个Type类型参数并返回一个代表该类型零值的Value类型值。
//
//参见"The Laws of Reflection"获取go反射的介绍:http://golang.org/doc/articles/laws_of_reflection.html
//专门演示反射
func reflectTest01(b interface{}) {
//通过入参获取 type kind 值
//先获取到 reflect.Type()
rType := reflect.TypeOf(b) //rType本质是一个接口
fmt.Println("rType = ", rType) //rType = int
//获取reflect.ValueOf()
rValue := reflect.ValueOf(b)
fmt.Println("rValue = ", rValue) //rValue = 100 但是此时 100 的类型并不是int,所以不能当成int来用(不能来做运算) ,可以使用文档type Value的部分函数来获取真正的值
fmt.Printf("the rValue type is %T \n", rValue) //the rValue type is reflect.Value
fmt.Println("rValue's is = ", rValue.Int()+1) //以反射获取的值获取真正的值来运算 rValue's is = 101
//将rValue转成interface{}类型
iv := rValue.Interface()
//将interface{}通过断言转成需要的类型
num2 := iv.(int)
fmt.Println("num2 = ", num2)
}
func main() {
num := 100
reflectTest01(num)
}
2.对结构体的反射演示
func reflectTest02(b interface{}) {
//通过入参获取 type kind 值
//先获取到 reflect.Type()
rType := reflect.TypeOf(b) //rType本质是一个接口
fmt.Println("rType = ", rType) //rType = main.student
//获取reflect.ValueOf()
rValue := reflect.ValueOf(b)
fmt.Println("rValue = ", rValue) //rValue = {张三 12}
fmt.Printf("the rValue type is %T \n", rValue) //the rValue type is reflect.Value
//下面将rValue转成interface{}
iv := rValue.Interface()
fmt.Printf("the iv type is %T \n", iv) //the iv type is main.student,但是此时无法取出stu的值,还需要对接口进行断言才行.因为是在运行时获取的,编译时无法确定iv是stu
stu, ok := iv.(student)
if ok {
fmt.Println("stu.Name = ", stu.Name) //stu.Name = 张三
}
//.因为是在运行时获取的,编译时无法确定iv是stu,所以可能存在多个相同属性的结构体,其实可以使用switch的形式来完成多种形式的断言
}
//专门演示反射
type student struct {
Name string `json:"name"`
Age int `json:"age"`
}
func main() {
stu := student{
Name: "张三",
Age: 12,
}
reflectTest02(stu)
}
3.注意细节
reflect.Value.Kind 获取变量的类别,返回的是一个常量
type student struct {
Name string `json:"name"`
Age int `json:"age"`
}
func main() {
stu := student{
Name: "张三",
Age: 12,
}
kind1 := reflect.TypeOf(stu).Kind()
kind2 := reflect.ValueOf(stu).Kind()
fmt.Println(kind1) //struct
fmt.Println(kind2) //struct
}
4.定义常量
只能定于基本数据类型
func main() {
const name = "张三"
const (
name1 = "李四"
age = 12
)
const (
a = iota //iota = 0 如果下面的没有显式的赋值或者赋值 iota 那么就是 iota ++
b
c
d
)
const (
q = iota // 当前是0
w = iota //换行后是1
e, r = iota, iota //换行后是2 两个值一行赋值就是上面的值累加下的
)
fmt.Println(name) //张三
fmt.Println(name1) //李四
fmt.Println(age) //12
fmt.Println(a) //0
fmt.Println(b) //1
fmt.Println(c) //2
fmt.Println(q) //0
fmt.Println(w) //1
fmt.Println(e) //2
fmt.Println(r) //2
}
func main() {
const (
age = "阿松大"
hi
how
)
fmt.Println(age) //阿松大
fmt.Println(hi) //阿松大
fmt.Println(how) //阿松大
}
5.type是类型,kind是类别两者可能相同也可能不同
比如不同包下的相同结构体,type是 pkg1.Person 和 pkg2.Person 但kind都是struct
可以总结为,如果是基本数据类型的话.kind和type是一样的,因为不存在不同包的问题,结构体的话因为type是 包名.结构体名称 所以type可能是不同的,当然了kind都是结构体
通过反射可以让变量在interface{}和Reflect.Value直接互相转换
变量 <–> interface{} <–> Reflect.Value
通过反射的方式获取变量的值(并返回对应的类型),要求数据类型匹配
比如 x 是 int,那么就应该用 Reflect.Value(x).int(),而不能用其他的,否则会报panic
Elem() 获取 反射指针指向的值
func changeValue(b interface{}) {
//此时想修改b的值
value := reflect.ValueOf(b)
fmt.Println("value's kind is ", value.Kind()) //如果 changeValue(number) 那么返回 value's kind is int , 是无法修改这个变量的值的,所以必须传入地址
value.Elem().SetInt(20) //所以要通过方法.Elem()获取到指针所指向的值然后调用set方法来实现通过反射修改值的 *** 作,可以实现传递地址然后通过*来修改的效果
}
func main() {
//此时想修改number的值,则需要对方法传入指针
number := 10
fmt.Println("before change number is", number)
changeValue(&number)
fmt.Println("after change number is", number)
//before change number is 10
//value's kind is ptr
//after change number is 20
}
实践
通过反射来遍历结构体字段,调用结构体方法,获取结构体标签的值
func main() {
mon := Monster{
Name: "牛魔王",
Age: 20,
Score: 100.00,
gender: "男",
}
TestStruct(mon)
}
type Monster struct {
Name string `json:"name"`
Age int `json:"monster_age"`
Score float64
gender string
}
func (m Monster) Print() {
fmt.Println("---start---")
fmt.Println(m)
fmt.Println("---end---")
}
func (m Monster) GetSum(n1, n2 int) int {
return n1 + n2
}
func (m Monster) Set(name string, age int, score float64, gender string) {
m.Name = name
m.Age = age
m.gender = gender
m.Score = score
}
func TestStruct(b interface{}) {
//获取 reflect.Type 类型
typ := reflect.TypeOf(b)
// 获取 reflect.Value 类型
val := reflect.ValueOf(b)
//通过 reflect.Value 获取 b 对应的类别
kd := val.Kind()
//如果不是结构体就退出函数,不玩了!
if kd != reflect.Struct {
fmt.Println("expect struct")
return
}
//获取该结构体下有几个字段
fields := val.NumField()
fmt.Printf("this Struct has %d fields\n", fields) //this Struct has 4 fields
//遍历结构体字段
for i := 0; i < fields; i++ {
fmt.Printf("field %d is %v\n", i, val.Field(i))
//field 0 is 牛魔王
//field 1 is 20
//field 2 is 100
//field 3 is 男
}
//遍历结构体tag
for i := 0; i < fields; i++ {
//获取Struct的tag,此时需要注意,获取标签需要使用reflect.Type来获取
tagVal := typ.Field(i).Tag.Get("json")
if tagVal != "" {
fmt.Printf("field val is %v and tag is %v\n", val.Field(i), tagVal)
//field val is 牛魔王 and tag is name
//field val is 20 and tag is monster_age
}
}
//获取结构体有多少个方法
methods := val.NumMethod()
fmt.Printf("struct has %d methods\n", methods) //struct has 3 methods 但是 val.NumMethod() 只统计公有方法,私有方法(开头字母小写的方法)不纳入统计
//通过反射调用方法,获取第二个方法并调用并传入参数nil,获取方法就可以通过反射对方法命名进行规范化,比如test框架
val.Method(1).Call(nil)
//---start---
//{牛魔王 20 100 男}
//---end---
//调用的顺序是采用函数名的ASCII码顺序来确定调用顺序的,而不是声明顺序
//---
//调用结构体第一个方法 Method(0)
//构造一个结构体value切片
var params []reflect.Value //声明了[]reflect.Value
//将10转为reflect.Value类型并append到切片params
params = append(params, reflect.ValueOf(10)) //func ValueOf(i interface{}) Value { ...} 接收空接口返回value类型的数据
params = append(params, reflect.ValueOf(40))
callValue := val.Method(0).Call(params)
for _, value := range callValue {
fmt.Println(value) //50
}
fmt.Println("callValue is", callValue[0].Int()) //50
}
三、网络编程
1.TCP socket编程,是网络编程的主流之所以叫Tcp socket是因为底层是基于 TCP/IP 协议的,比如 qq聊天
2.b/s 架构,基于http,而http也是基于 TCP/IP的
3.网络编程基础知识:
osi模型(理论):
应用层
表示层
会话层
传输层
网络层
数据链路层
物理层
Tcp/Ip模型(实际):
应用层(application,smtp,ftp,telnet,http)
传输层:解释数据
网络层:(ip)定位,ip地址和确定链接路径
链路层:(link):与硬件驱动对话
cmd追踪tracert www.baidu.com
每个internet上的主机和路由器都有一个ip地址,他包括网络号和主机号.ip地址有ipv4(32位)和ipv6(128位),可以通过ipconfig查看
只要是做程序的服务必须留一个监听端口,交互(通讯)的通道,服务断的通讯端口是唯一的,但客户端可以是随意的端口与其进行交互
端口
这里指的端口不是物理端口,指的是tcp/ip协议中的端口,指的是逻辑意义上的端口,如果把ip比作一间房子,端口就是进入这间房子的门
但是一股ip地址的端口可以有65536个(即 256 * 256)个之多,端口是通过端口号来标记的,端口号只有整数范围是 0~65536(256 * 256-1)
0 是保留端口
1-1024是保留端口,程序员一般不使用,比如22:ssh,23:telnet,21:ftp,25:sftp,80:iis,7:echo服务
1025-65535是动态端口,是程序员可以编程时候使用
在计算机尤其是服务器,要尽可能的少开端口
一个程序只能监听一个端口
如果使用netstat-an可以查看本机有哪些端口在监听
可以使用netstat-ant查看监听端口的pid,在结合任务管理器关闭不必要的端口
1,队列(数组模拟环形队列)
func main() {
//测试基于数组实现的环形队列,插入 0~4
const size = 5
queue := NewCircleQueue(size)
for i := 0; i < size; i++ {
err := queue.PushCircleQueue(fmt.Sprint("hello", i))
if err != nil {
fmt.Println(err)
return
}
queue.PopCircleQueue()
}
queue.PushCircleQueue("hi")
arr := queue.ListCircleQueue()
for _, v := range arr {
fmt.Println(v)
}
}
// NewCircleQueue 创建环形队列的方法
func NewCircleQueue(size int) (c *CircleQueue) {
queue := CircleQueue{
maxSize: size,
array: make([]string, size),
head: 0,
tail: 0,
}
c = &queue
return
}
// ListCircleQueue 遍历队列内容
func (c *CircleQueue) ListCircleQueue() []string {
return c.array[c.head%c.maxSize : c.tail%c.maxSize]
}
// PushCircleQueue 添加入队列的方法
func (c *CircleQueue) PushCircleQueue(val string) (err error) {
//入参钱判断队伍是不是已经满了
if c.IsFull() {
err = errors.New("PushCircleQueue err , Queue is full")
}
//没有满,将入参加入数组的指向队尾的位置tail并将tail ++
c.array[c.tail%c.maxSize] = val
c.tail++
return
}
// PopCircleQueue d出队列的方法
func (c *CircleQueue) PopCircleQueue() (val string, err error) {
//先判断队列是否已经空了
if c.IsEmpty() {
err = errors.New(" PopCircleQueue err , Queue is empty")
}
//没有空,可以取出元素,将元素的第一个取出赋值给val,并返回
val = c.array[c.head%c.maxSize]
//将头部索引向后移动
c.head++
return
}
// IsFull 当头尾两个元素补相等时候,进行取模 *** 作,当内置切片填充到第下标为4的时候,此时push方法中c.tail++执行后,c.tail等于构造函数入参size,对maxSize取模为0,切片已达size
func (c *CircleQueue) IsFull() bool {
return !c.IsEmpty() && (c.tail-c.head)%c.maxSize == 0
}
//IsEmpty 判断环形队列空了,头就是尾,尾就是头,重叠了,没有元素了,就空了
func (c *CircleQueue) IsEmpty() bool {
return c.head == c.tail
}
//判断环形队列中有多少元素
func (c *CircleQueue) size() int {
//如果是空队列直接返回0即可
if c.IsEmpty() {
return 0
}
return (c.tail - c.head) % (c.maxSize + 1)
}
// CircleQueue 使用一个结构体管理环形队列
type CircleQueue struct {
maxSize int //最大容量len(array)
array []string //切片
head int //指向队列头部
tail int //指向队列尾部
}
2.链表
链表是有序的列表,在内存中的地址可能不是连续的
单链表:
为了比较好的对单链表进行crud *** 作,一般会给链表设置一个头节点,这个节点不存放数据
实践:
func main() {
//创建头节点
head := HeroNode{
no: 0,
name: "头节点",
}
//创建一个新节点
hero1 := HeroNode{
no: 1,
name: "宋江",
nickName: "及时雨",
}
//创建一个新节点
hero2 := HeroNode{
no: 2,
name: "卢俊义",
nickName: "玉麒麟",
}
insert(&head, &hero1)
insert(&head, &hero2)
ListLink(&head)
}
//编写第一种插入方式,直接在链表最后加入
func insert(herd, newNode *HeroNode) {
//先找到链表的最后节点
//创建一个临时节点
temp := herd
for {
if temp.next == nil { //表示找到最后一个
break
}
temp = temp.next //移动指向
}
//遍历结束 temp 为当前链表的最后一个
temp.next = newNode
}
// ListLink 展示列表
func ListLink(head *HeroNode) {
temp := head
//先判断列表是不是空的
if temp.next == nil {
fmt.Println("链表为空")
return
}
//遍历链表,因为链表不是空链表,所以采用do while 方式循环往复遍历
for {
//头节点不是最后一个节点,因为我们没有给头节点赋值,所以直接从第二个节点开始遍历
fmt.Printf("[ %d , %s ,%s] == >", temp.next.no, temp.next.name, temp.next.nickName)
temp = temp.next
if temp.next == nil {
fmt.Println("遍历结束!")
break
}
}
}
// HeroNode 定义一个node
type HeroNode struct {
no int
name string
nickName string
next *HeroNode //指向下一个节点
}
总结
对go基础的学习暂时先到这里,下一步是学习go-micro,并对go和java的微服务进行整合,项目和学习的心得会分享出来,还是挂一张好看的图
欢迎分享,转载请注明来源:内存溢出
评论列表(0条)