Hello,各位小伙伴们,让我们继续在Go的知识海洋里学习
话不多说,开始吧
指针
1、指针表示一个变量在栈中存储的key,表示一个值的内存地址,如果没有赋值,默认指向nil
2、golang语言 ,指针不能进行运算,C语言是可以的
3、通过指针传递,可以修改原来地址的值
package main
import "fmt"
//指针
//Go 语言的取地址符是 & *根据地址取值或者声明一个类型
func main() {
//1、变量指针
var ip *int
var num = 666
ip = &num // &num 表示拿到一个变量的内存地址
fmt.Println(ip) //拿到地址
fmt.Println(*ip) //地址前面加个* 拿到值
//2、数组指针
arr :=[3]int{11,2,33}
var dArr [3]*int
for i := 0; i < len(arr); i++ {
dArr [i]=&arr[i]
}
//查看dArr数组
for i := range dArr {
fmt.Print(*dArr[i]," ")
}
fmt.Println()
//3、类型别名
type myInt string
var str myInt ="给数据类型起个别名"
fmt.Println(str)
//3A、对象赋值1,初始化赋值
user:= User{"admin",123,1,18,
Money{"admin",100}}
fmt.Println(user)
//5、对象赋值2,构造方法赋值,在User光标下,按住ctrl+enter键,生成构造方法
//6、对象赋值3,直接赋值,C++也是一样
var users User
users.username="Jessica"
users.password=666
fmt.Println(users)
//7、new创建出来的是一个指针
var tom=new(User)
tom.username="tom"
tom.password=123
fmt.Println(*tom)
//8、传递指针
var tom1 *User
tom1=tom
fmt.Println("tom1的数据为: ",*tom1)
}
/**
结构体属性默认值
int 0
float 0.0
bool false
string nil //IDEA控制台显示是空的,啥也没有
*/
type User struct {
username string
password int
//同类型的可以连着写
sex,age int
money Money
}
func NewUser(username string, password int, sex int, age int, money Money) *User {
return &User{username: username, password: password, sex: sex, age: age, money: money}
}
type Money struct {
userId string
count int
}
3A、方法
①方法不允许重载,也就是同名不同参数
②方法可以属于结构体,也可以属于结构体指针
③如果接收者是一个指针类型,赋值会自动解除指针
④方法的接收者,可以是其他类型,不一定要是结构体,格式为 func(recv receiver)xxx(){}
package main
import "fmt"
//方法,在Java中,方法可以写在对象里面
//在Golang中,方法不可以写在结构体里面,需要分开写,每个方法前带个(结构体变量 结构体类型)
//表示这个方法属于这个结构体
type Person struct {
name string
age int
}
func (per Person) eat() {
fmt.Println("eat...")
}
func (per Person) sleep() {
fmt.Println("sleep...",per)
}
func main() {
p1 :=Person{"tom",18}
p1.eat()
p1.sleep()
fmt.Println()
//&Person 表示把Person当做一个变量,取内存地址(指针)
p2 :=&Person{"jack",22}
fmt.Printf("%T",p2)
fmt.Println()
fmt.Println(p2)
//同样,这也是一个指针
p3 :=new(Person)
p3.name="hole"
p3.age=15
fmt.Println(*p3)
}
5、接口
①实现一个接口,需要实现接口的所有方法,有些编译器(IDEA)可以只实现其中某一个方法,但是最好全部实现,以免嵌套接口时出现未知的错误
②接口可以有返回值,也可以没有
③GoLang里面,实现一个接口,需要分开两个部分
先写结构体,再把结构体放到接口方法前面
④一个结构体(相当于Java的类)可以实现多个接口,多个结构体可以实现同一个接口
⑤接口可以嵌套,也就是接口里面定义接口
⑥接口方法可以传参,也可以传结构体
package main
import "fmt"
//接口
//Java里面 实现一个接口,通过implements 关键字+接口名 就可以了
//Golang里面,实现一个接口,需要分成两个部分,第一步,声明一个结构体
//第二步,把结构体放到接口方法的前面
type Usb interface {
read() string
write() string
}
type Player interface {
music()
video()
}
type Computer struct {
}
type Mobile struct {
}
func (computer Computer) read() string {
fmt.Println("电脑的接口,正在读")
return "computer read..."
}
func (computer Computer) write() string {
fmt.Println("电脑的接口,正在写")
return "computer write..."
}
func (mobile Mobile) read() string {
fmt.Println("手机的接口,正在读")
return "mobile read..."
}
func (mobile Mobile) write() string {
fmt.Println("手机的接口,正在写")
return "mobile write..."
}
func (computer Computer) music() {
fmt.Println("电脑的接口,正在播放音乐")
}
func (mobile Mobile) video() {
fmt.Println("手机的接口,正在播放视频")
}
func main() {
//Computer相当于实现类 new实现类
c:=Computer{}
c.read()
c.write()
c.music()
fmt.Println()
m:=Mobile{}
m.read()
m.write()
m.video()
}
package main
import "fmt"
//接口嵌套
type Ball interface {
//篮球
basketball()
//羽毛球
badminton()
}
type Swim interface {
swimming() string
}
//motion 运动
//一个接口里面有两个接口
type Motion interface {
Ball
Swim
}
type Person1 struct {
}
type Person2 struct {
}
func (p1 Person1) basketball() {
fmt.Print("p1正在打篮球...")
}
func (p1 Person1) badminton() {
fmt.Println("p1正在打羽毛球...")
}
func (p1 Person1) swimming() string {
fmt.Println("p1正在打游泳...")
return "success"
}
func main() {
//Person1相当于实现类 new实现类
var ff Motion
ff =Person1{}
ff.basketball()
fmt.Println()
ff.badminton()
ff.swimming()
}
6、继承
Golang里面没有专门的继承关键字,只能通过结构体嵌套,实现继承
package main
import "fmt"
/**
继承
*/
type Animal struct {
name string
age int
}
//构造方法
func NewAnimal(name string, age int) *Animal {
return &Animal{name: name, age: age}
}
func (animal Animal) eat() {
fmt.Println(animal.name,"正在吃...")
}
func (animal Animal) sleep() {
fmt.Println(animal.name,"正在睡...")
}
type Dog struct {
animal Animal
speak string
}
type Cat struct {
Animal
speak string
}
func main() {
//写法一
var ff =Dog{
Animal{"dog",10},
"wang wang wang",
}
fmt.Println(ff)
ff.animal.eat()
ff.animal.sleep()
fmt.Println()
//写法二,简写的话,结构体不要加 animal Animal
//应该这样写
/**
type Cat struct {
Animal
speak string
}
*/
fff := Cat{
Animal{"cat",12},
"miao miao miao",
}
fmt.Println(fff)
fff.eat()
fff.sleep()
}
7、自定义导包,注意,选第一个Empty file
7.1 test.go文件
package test3
import "fmt"
func testImport() {
fmt.Print("import success ...")
}
//这是一个普通文件,里面不能有main函数
7.2 main.go文件
package main
import (
"goday01/com.dowhere/test3"
)
func main() {
test3.TestImport()
}
7.3 如果出现无法导入的情况,查看一下go mod文件所在的包是否正确,另外下面这个方法无法使用,需要导入以后,才可以使用
7.4 测试
8、远程包导入,我们需要使用一个网站,它是一个远程包的管理依赖,https://pkg.go.dev/
搜一下gin 这个gin有点类似于java的tomcat
8.1 在IDEA 终端下载该依赖
D:\Learning\Go\project\src\goday01>go get -u github.com/gin-gonic/gin
8.2 如果依赖没有下载下来,可以使用 go mod tidy
8.3 main.go 引入代码
package main
import (
"goday01/com.dowhere/test3"
)
import "github.com/gin-gonic/gin"
import "net/http"
//测试导入包 自定义包和远程包
func main() {
//1、自定义包
test3.TestImport()
//2、远程包
r := gin.Default()
r.GET("/ping", func(c *gin.Context) {
c.JSON(http.StatusOK, gin.H{
"message": "pong",
})
})
r.Run() // listen and serve on 0.0.0.0:8080 (for windows "localhost:8080")
}
8.4 IDEA 出现net/http无法导入的时候,出现Found several packages [http, main] in 'D:/Learning/Go/windows/src/net/http;D:/Learning/Go/windows/s,这种情况,一般是IDEA Go 插件和 GO的版本不兼容,不影响使用
8.5 启动程序,访问 localhost:8080/ping,出现pong表示没有问题
并发
1、并发可以说是Go的一大亮点,它设计的初衷,就是为了应对高并发,所以这一节十分重要
2、协程,在讲协程之前,我们需要了解进程、线程、协程的关系,当一个应用程序启动的时候,会向操作系统申请内存资源,操作系统会一个应用程序分配一个进程,
一个进程包含多个线程,每个线程拥有一定数量的协程,
说到底,协程才是最后做事的。
3、注意:Java线程受到操作系统调度,没有执行完的异步线程需要等到全部执行完,main方法才结束, Go语言协程由程序员手动控制,默认情况下,main协程结束就直接结束了,不会等到其他协程全部执行完
3A、在Java中,main方法启动的是一个main线程
在Golang中,main函数启动的是一个main线程中的一个协程
5、启动一个Go的协程,非常简单,go + 函数名()
6、goroutine在go里面是叫协程的,一般情况下进程里处理并发任务时都会开启多个线程来处理,线程的生命周期是通过操作系统控制的,而协程就相当于go在线程的基础上,又往下写了一套线程的架构,然后不给操作系统管理,自己写一套调度策略来使用,有进程-线程-协程这种关系。
7、协程的话,可以算做一种用户态的轻量级线程,协程的调度完全由用户控制。协程拥有自己的寄存器上下文和栈。协程调度切换时,将寄存器上下文和栈保存到其他地方,在切回来的时候,恢复先前保存的寄存器上下文和栈,直接操作栈则基本没有内核切换的开销,可以不加锁的访问全局变量,所以上下文的切换非常快。
8、在go中每一个协程是一个独立的执行单元,相较于每个线程固定分配 2M 内存的模式,协程的栈采取了动态扩容方式, 初始时仅为2KB,随着任务执行按需增长,最大可达 1GB(64 位机器最大是 1G,32 位机器最大是 256M),且完全由 go自己的调度器来调度。此外,go的gc还会周期性地将不再使用的内存回收,收缩栈空间,因此,go程序可以同时并发成千上万个协程是得益于它强劲的调度器和高效的内存模型。
package main
import (
"fmt"
"time"
)
func test() {
fmt.Println("test 协程正在处理...")
time.Sleep(time.Microsecond*2000)
}
func main() {
go test() //表示启动一个协程
fmt.Println("main 协程结束...")
}
9、channel
多个协程之间相互通信 类似于Java的JMM模型
协程通信 分为同步和异步
package main
import (
"fmt"
"math/rand"
"time"
)
//新建一个channel
var channel=make(chan int)
func send() {
rand.Seed(time.Now().UnixNano())
//生成一个0-100的随机整数
value :=rand.Intn(100)
//发送的时候,通道在前面
channel <- value
}
func main() {
//延迟 逆序执行关闭流
defer close(channel)
go send()
fmt.Println("main协程开始接收数据...")
//接收的时候,通道在后面
data := <- channel
fmt.Println("main协程接收的数据为 ",data)
fmt.Println("end...")
}
10、waitgroup, 等待组
多个协程执行的时候,有个协程需要等待其他协程执行的某个结果,它才执行
下面是不加waitgroup,可以看到,其他协程和main协程会争抢cpu执行,一旦main协程拿到时间片,同一时刻的协程会一起执行,后面的就不会管了,这样是无法接受的
我们加上 waitgroup,通过控制台可以发现,打印结果是10次
package main
import (
"fmt"
"sync"
)
//waitGroup
var wp sync.WaitGroup
func showMsg(i int) {
defer wp.Done()
fmt.Println("当前的i为: ",i)
}
func main() {
var count =0
for i := 0; i < 10; i++ {
count++
go showMsg(i)
wp.Add(1)
}
defer fmt.Println("一共执行了",count,"次")
//直到waitGroup 等于0 ,就会停止
wp.Wait()
fmt.Println("main协程结束了...")
}
11、runtime包
runtime.Gosched() 让出当前Cpu,不一定有效
package main
import (
"fmt"
"runtime"
)
func show(msg string) {
for i := 0; i < 2; i++ {
fmt.Println(msg,i)
}
}
func main() {
go show("golang")
for i := 0; i < 2; i++ {
//让出当前Cpu 类似于java的 Thread.yield
//但是不一定会有效
runtime.Gosched()
fmt.Println("main start...")
}
fmt.Println("end ...")
}
runtime.Goexit() 退出当前协程
package main
import (
"fmt"
"runtime"
)
func show(msg string) {
for i := 0; i < 2; i++ {
if i == 1 {
runtime.Goexit()
}
fmt.Println(msg,i)
}
}
func main() {
go show("golang")
for i := 0; i < 2; i++ {
//让出当前Cpu 类似于java的 Thread.yield
//但是不一定会有效
runtime.Gosched()
fmt.Println("main start...")
}
//可以设置允许最多允许多少个Cpu执行任务
runtime.GOMAXPROCS(1)
fmt.Println("end ...")
}
12、mutex,我们先看不加锁的情况,正常情况下,num 加一次减一次,值应该是100,但是实际情况不是的,多个协程之间出现了协程不安全问题,有些小伙伴可能测试不出效果,需要多测几次
然后我们加上锁,打印结果发现,无论打印多少次,结果都是100
package main
import (
"fmt"
"sync"
)
var lock sync.Mutex
var wg sync.WaitGroup
var num =100
func add() {
//每次只有一个协程进入 num++
lock.Lock()
defer wg.Done()
num ++
fmt.Println("num++ 当前num的值为 ",num)
lock.Unlock()
}
func sub() {
//每次只有一个协程进入 num--
lock.Lock()
defer wg.Done()
num --
fmt.Println("num-- 当前num的值为 ",num)
lock.Unlock()
}
//mutex 互斥 类似于java的 synchronized 和 ReentrantLock
func main() {
for i := 0; i < 100; i++ {
wg.Add(1)
go add()
wg.Add(1)
go sub()
}
//这里之所以要引入 WaitGroup,是因为 我们需要等待上面两个协程结束
wg.Wait()
fmt.Println("main end...",num)
}
13、channel的多次读取,我们进行协程之间通信的时候,如果channel里面不再发送数据了,我们需要关闭通道,否则会报错
14、channel的遍历,当我们向channel 发送多条数据时,我们需要遍历,把数据一次性取出来
package main
import "fmt"
//channel的遍历
var chanel = make(chan int)
func main() {
//发送方
go func() {
for i := 0; i < 2; i++ {
chanel <- i
}
//数据发送完成以后,通道必须关闭 防止死锁
close(chanel)
}()
//接收方
for i := 0; i <= 2; i++ {
r := <- chanel
fmt.Println(r)
}
}
15、select ,多个channel 通信,进行选择
package main
import (
"fmt"
"time"
)
//select 类似于java 的switch
//有一点不同的是 select 一般用于channel的选择
var chanInt = make(chan int)
var str = make(chan string)
func main() {
go func() {
chanInt <- 100
str <- "hello"
defer close(chanInt)
defer close(str)
}()
for {
select {
case r := <- chanInt:
fmt.Println(r)
case s :=<- str:
fmt.Println(s)
//这个总是会执行的
default:
fmt.Println("default")
}
time.Sleep(time.Second)
}
}
16、timer 定时器 执行一次
package main
import (
"fmt"
"time"
)
//定时器,只执行一次
func main() {
//1、新建一个2秒的定时器
timer := time.NewTimer(time.Second * 2)
//2、获得当前时间
fmt.Println(time.Now())
//3、timer.C 会阻塞获得2秒之后的数据
//timer.C 的数据放在channel里面,需要拿出来给到c
c := <- timer.C
fmt.Println(c)
}
17、Ticker 会周期执行,执行多次
package main
import (
"fmt"
"time"
)
//定时任务 周期执行,可以执行多次
func main() {
ticker := time.NewTicker(time.Second * 2)
//1、演示周期执行
//var count = 0
//for range ticker.C {
// fmt.Println("ticker...")
// count++
// if count >= 5 {
// ticker.Stop()
// break
// }
//}
//2、演示周期 发送数据到channel
chanInt := make(chan int)
go func() {
for range ticker.C {
select {
case chanInt <- 1:
case chanInt <- 2:
case chanInt <- 3:
}
}
}()
count := 0
for value := range chanInt {
fmt.Println("收到的value值是: ", value)
count += value
if count >= 10 {
break
}
}
}
18、原子操作,利用CAS 修改值,这种相比较于加锁的方式,优点在于性能,直接操作主内存
package main
import (
"fmt"
"sync/atomic"
"time"
)
var n int32=100
//CAS 操作 n的值
func addNum() {
atomic.AddInt32(&n,1)
}
func subNum() {
atomic.AddInt32(&n,-1)
}
func main() {
for i := 0; i < 100; i++ {
go addNum()
go subNum()
}
//这里是为了防止 main协程 退出导致其他协程无法继续执行
time.Sleep(time.Second *2)
fmt.Println("main end ...",n)
}
19、原子读写,除了 atomic.AddInt32()之外
go语言中,atomic包 还提供了其他的 api 保证读写安全
package main
import (
"fmt"
"sync/atomic"
"time"
)
var m int32=100
func read() {
//CAS 读 保证每次读到最新的数据
atomic.LoadInt32(&m)
fmt.Println(m)
}
func write() {
//CAS 写
atomic.StoreInt32(&m,200)
fmt.Println(m)
}
func compareAndSet() {
//直接调用具体的api,它也是CAS
atomic.CompareAndSwapInt32(&m,200,300)
fmt.Println(m)
}
func main() {
read()
write()
compareAndSet()
time.Sleep(time.Second*2)
}
20、最后,各位小伙伴们,麻烦给老哥一个点赞、关注、收藏三连好吗,你的支持是老哥更新最大的动力,谢谢!