go语言学习进阶

目录

[第一章 go语言中包的使用](#第一章 go语言中包的使用)

一.main包

二.package

三.import

四.goPath环境变量

五.init包初始化

六.管理外部包

[第二章 time包](#第二章 time包)

[第三章 File文件操作](#第三章 File文件操作)

一.FileInfo接口

二.权限

三.打开模式

四.File操作

五.读文件

[参考1:Golang 中的 bufio 包详解(四):bufio.Scanner_golang scanner-CSDN博客](#参考1:Golang 中的 bufio 包详解(四):bufio.Scanner_golang scanner-CSDN博客)

[参考2:Go 语言读文件的九种方法 - 王一白 - 博客园](#参考2:Go 语言读文件的九种方法 - 王一白 - 博客园)

[参考3:Go语言读取文件_go 读取文件-CSDN博客](#参考3:Go语言读取文件_go 读取文件-CSDN博客)

[第四章 I/O操作](#第四章 I/O操作)

一、IO包

二、文件复制

三、断点续传

[第五章 bufio包](#第五章 bufio包)

一、bufio包原理

二、bufio.Read

三、bufio.write

[第六章 并发编程](#第六章 并发编程)

一、golang中的并发编程

1、多任务

2、并发

3、进程、线程、协程

二、Goroutine

1.什么是Goroutine

2.主goroutine

3.如何使用Goroutines

4.启动多个Goroutines

三、Go语言的并发模型

1、线程模型

2、Go并发调度:G-P-M模型

四、临界资源安全问题

五、channel通道

六、关闭通道和通道上范围循环

七、缓冲通道

[第七章 time包中的通道相关函数](#第七章 time包中的通道相关函数)

[第八章 反射的使用](#第八章 反射的使用)

一、reflect的基本功能TypeOf和ValueOf

二、反射的使用


第一章 go语言中包的使用

Go 语言的源码复用建立在包(package)基础上。包通过package,import,GOPATH操作完成。

一.main包

Go 语言的入口main()函数所在的包(package)叫main, main包想要引用别人的代码,需要import引入!

二.package

src目录是以代码包的形式组织并保存Go源码文件的。每个代码包都和src目录下的文件夹一一对应。每个子目录都是一个代码包。

代码包包名和文件目录名,不要求一致。比如文件目录叫hello,但是代码包包名可以声明为"main",但是同一个目录下的源码文件第一行声明的所属包,必须一致!

同一个目录下的所有.go文件的第一行添加包定义,以标记该文件归属的包,演示语法:

复制代码
package 包名

包需要满足:

  • 一个目录 下的同级文件归属一个包,也就是说,在同一个包下面的所有文件的package名,都是一样的。

  • 在同一个包下面的package名都建议设为该目录名,但也可以不是。也就是说,包名可以与其目录不两只名。

  • 包名为main的包为应用程序的入口包,其他包不能使用。

在同一个包下面的文件属于同一个工程文件,不用import包,可以直接使用.

包可以嵌套定义,对应的就是嵌套目录,但包名应该与所在的目录一致.

包中,通过标识首字母是否大写,来确定是否可以被导出.首字母大写才可以被导出,视为public公共的资源.

三.import

A:要引用其他包,可以使用import关键字,可以单个导入或者批量导入,

Go 复制代码
//单个导入
import "package"
​
//批量导入
import (
    "package1"
    "package2"
)

B: 点操作

Go 复制代码
import (
    . "fmt"
)

这个点操作的含义就是这个包导入之后在你调用这个包的函数时,你可以省略前缀的包名.

C.起别名

别名操作顾名思义我们可以把包命名成另一个我们用起来容易记忆的名字.导入时,可以为包定义别名,语法演示

复制代码
import (
    p1 "package1"
    p2 "package2"
)
​
//使用时,别名操作,调用包函数时间缀变成了我们的前缀
p1.Method()

D._ 操作

如果仅仅需要导入包时执行初始化操作,并不需要使用包内的其他函数,常量等资源.则可以在导入包时,匿名导入

导入包的路径名,可以是相对路径也可以是绝对路径,推荐使用绝对路径(起始于工程根目录)

复制代码
package main
​
import (
    "l_package/pk1"
    "l_package/utils"           //绝对路径
    "l_package/utils/timeutils" //绝对路径
)
​
func main() {
    /*
    关于包的使用
    1.一个目录下的统计文件归属一个包.package的声明要一致
    2.package声明的包和对应的目录名可以不一致,但习惯上还是写成一致的
    3.包可以嵌套
    4.同包下的函数不需要导入包,可以直接使用
    5.main包,main()函数所在的包,其他的包不能使用
    6.导入包的时候,路径要从src下开始写
    */
    utils.Count()
    timeutils.PrintTime()
    pk1.MyTest1()
    utils.MyTest2()
    p1 := p.Person{"Jack", 34, "北二胡同"}
    fmt.Println(p1.Name, p1.Add, p1.Add)
}
复制代码
package person
​
type Person struct {
    Name string
    Age  int
    Add  string
}
四.goPath环境变量

import导入时,会从GO的安装目录(也就是GOROOT环境变量设置的目录)和GOPATH环境变量设置的目录中,检索src/package来导入包。如果不存在,则导入失败。

GOROOT: 就是GO内置的包所在的位置。

GOPATH:就是自己定义的包的位置。

通常在开发Go项目时,调试或者编译构建时,需要设置GOPATH指向项目的目录,目录中的src目录中的包就可以被导入了。

五.init包初始化

函数init(), main()是go语言中的留函数。我们可以在源码中,定义init()函数,此函数会在包被导入时执行,例如如果是在main中导入包,包中存在init(), 那么init()中的代码会在main函数执行前执行,用于初始化包所需要的特定资料。

init(),main()两个函数,在go语言中的区别:

相同点:

两个函数在定义时不能有任何的参数和返回值。

该函数只能由go程序自动调用,不可以被引用。

不同点:

init可以应用于任意包中,且可以重复定义多个。

main函数只能用于main包中,且只能定义一个.

两个函数的执行顺序:

在main包中的go文件默认总是会被执行。

对同一个go文件的init()调用顺序是从上到下的。

对同一个package中的不同文件,将文件名按字符串进行"从小到大"排序,之后顺序调用各文件中的init()函数。

对于不同的package,如果不相互依赖的话,按照main包中import的顺序调用其包中的init()函数。

如果package存在依赖,调用顺序为最后被依赖的最先被初始化,例如:导入顺序 main->A->B->C, 则初始化顺序为C->B->A->main, 一次执行对应的init方法。main包总是被最后一个初始化,因为它总是依赖别的包。

六.管理外部包

go允许import不同代码库的代码。对于import要导入的外部的包,可以使用go get命令取下来放到GOPATH对应的目录中去。

例通过go语言连接mysql数据库,需要先下载mysql的数据包,那么在终端下输入以下命令:

复制代码
go get github.com/go-sql-driver/mysql

下载后在计算机中的位置:

也就是说,对于go语言来讲,其实并不关心你的代码是内部外是外部的,总之都在GOPATH里,任何import包的路径都是从GOPATH开始的;唯一的区别,就是内部依赖的包是开发者自己写的,外部依赖的包是go get下来的。

复制代码
package main
​
import (
    "database/sql"
    "fmt"
    _ "github.com/go-sql-driver/mysql"
)
​
func main() {
    //database/sql
    db, err := sql.Open("mysql", "root:root@tcp(192.168.68.128:3306)/mydata?charset=utf8")
    if err != nil {
        //panic(err)
        return
    }
    fmt.Println("连接成功", db)
}

扩展

使用go install来编译包文件

一个非main包在编译后会生成一个.a文件(在临时目录下生成,除非使用go install安装到GOROOT或GOPATH下,否则你看不到.a), 用于后续可执行程序链接使用。

在Go标准库的中的包对应的源码部分路径在: GOROOT/src, 而标准库中包编译后的.a文件路径在GOROOT/pkg/darwin_amd64下。

第二章 time包
复制代码
package main
​
import (
    "fmt"
    "math/rand"
    "time"
)
​
func main() {
    /*
        time包:
             1年=365天, day
             1天=24小时, hour
             1小时=60分钟, minute
             1分钟=60秒, second
             1秒=1000毫秒, millisecond
             1毫秒=1000微秒, microsecond -->μs
             1微秒=1000纳秒, nanosecond --> ns
             1纳秒=1000皮秒, picosecond --> ps
​
    */
    //1.获取当前的时间
    t1 := time.Now()
    fmt.Printf("%T\n", t1)
    fmt.Println(t1) //2024-12-12 14:12:10.989895 +0800 CST m=+0.007258501
​
    //2.获取指定的时间
    t2 := time.Date(2008, 5, 13, 15, 30, 38, 9, time.Local)
    fmt.Println(t2) //2008-05-13 15:30:38.000000009 +0800 CST
​
    //3.time-->string之间的转换
    /*
        t1.Format("格式模板")--->string
           模板的日期必须是固定: 2006-01-02 15:04:45
    */
    s1 := t1.Format("2006年1月2日 15:04:05")
    fmt.Println(s1)
​
    s2 := t1.Format("2006/1/2")
    fmt.Println(s2)
​
    //string --> time
    /*
        time.Parse("模板",str)--->time, err
    */
    s3 := "1999年10月10日"
    t3, err := time.Parse("2006年1月2日", s3)
    if err != nil {
        fmt.Println("err:", err)
    }
    fmt.Println(t3)
    fmt.Printf("%T\n", t3)
​
    //4.根据当前时间,获取指定的内容
    year, month, day := t1.Date() //年,月,日
    fmt.Println(year, month, day)
​
    hour, min, sec := t1.Clock()
    fmt.Println(hour, min, sec)
​
    year2 := t1.Year()
    fmt.Println("年", year2)
    fmt.Println(t1.YearDay())
​
    month2 := t1.Month()
    fmt.Println("月: ", month2)
    fmt.Println("日: ", t1.Day())
    fmt.Println("时: ", t1.Hour())
    fmt.Println("分钟: ", t1.Minute())
    fmt.Println("秒: ", t1.Second())
    fmt.Println("纳秒: ", t1.Nanosecond())
​
    fmt.Println(t1.Weekday())
​
    //5.时间戳:指定的日期,距离1970年1月1日0点0时0分0秒的时间差值: 秒, 纳秒
    t4 := time.Date(1970, 1, 1, 1, 0, 0, 0, time.UTC)
    timeStamp := t4.Unix() //秒的差值
    fmt.Println(timeStamp)
    timeStamp2 := t1.Unix()
    fmt.Println(timeStamp2)
​
    timeStamp3 := t4.UnixNano()
    fmt.Println(timeStamp3)
    timeStamp4 := t1.UnixNano()
    fmt.Println(timeStamp4)
​
    //6.时间间隔
    t5 := t1.Add(time.Minute)
    fmt.Println(t1)
    fmt.Println(t5)
    fmt.Println(t1.Add(24 * time.Hour))
​
    t6 := t1.AddDate(1, 0, 0)
    fmt.Println(t6)
​
    d1 := t5.Sub(t1)
    fmt.Println(d1)
​
    //7.睡眠
    time.Sleep(3 * time.Second) //让当前的程序进入睡眠状态
    fmt.Println("main .... over....")
​
    //睡眠[1 - 10]的随机数
    rand.Seed(New(NewSource(seed)))  
    randNum := rand.Intn(10) + 1
    fmt.Println(randNum)
    time.Sleep(time.Duration(randNum) * time.Second)
    fmt.Println("睡醒了......")
​
}
第三章 File文件操作

首先,file类是在os包中的,封装了底层的文件描述符和相关信息,同时封装了Read和Write的实现。

一.FileInfo接口

FileInfo接口中定义了File信息相关的方法。

复制代码
type FileInfo interface {
    Name() string     //base name of the file 文件名.扩展名 aa.txt
    Size() int64      //文件大小,字节数12540
    Mode() FileMode   //文件权限 -rw-rw-rw-
    ModTile() time.Time   //修改时间  2018-04-13 16:30:53 +0800 CST
    IsDir() bool        //是否文件夹
    Sys() interface{}   //基础数据源接口 (can return nil)
}
复制代码
package main
​
import (
    "fmt"
    "os"
)
​
func main() {
    /*
        FileInfo: 文件信息
            interface
                Name(), 文件名
                Size(), 文件大小,字节为单位
    */
    fileInfo, err := os.Stat("C:\\Users\\Administrator\\Desktop\\key.txt")
    if err != nil {
        fmt.Println("err", err)
        return
    }
    fmt.Printf("%T\n", fileInfo)
​
    //获取文件外
    fmt.Println(fileInfo.Name())
    //文件大小
    fmt.Println(fileInfo.Size())
    //是否是目录
    fmt.Println(fileInfo.IsDir())
    //修改时间
    fmt.Println(fileInfo.ModTime())
    //权限
    fmt.Println(fileInfo.Mode())
}
二.权限

至于操作权限perm, 除非创建文件时才需要指定,不需要创建新文件时可以将其设定为0。虽然go语言给perm权限设定了很多的学量,但是习惯上也可以直接使用数字,如0666(具体含义和Unix系统的一致).

权限控制:

(1) 符号表示方式: - type ---owner ---group ---others

文件的权限是这样分配的 读 写 可执行 分别对应的是 r w x如果没有那一个权限,用 - 代替( - 文件 d目录 | 连接符号)

(2) 八进制表示方式 r --> 004 w---> 002 x-->001 - ---> 000

三.打开模式
复制代码
const (
    // Exactly one of O_RDONLY, O_WRONLY, or O_RDWR must be specified.
    O_RDONLY int = syscall.O_RDONLY // open the file read-only.
    O_WRONLY int = syscall.O_WRONLY // open the file write-only.
    O_RDWR   int = syscall.O_RDWR   // open the file read-write.
    // The remaining values may be or'ed in to control behavior.
    O_APPEND int = syscall.O_APPEND // append data to the file when writing.
    O_CREATE int = syscall.O_CREAT  // create a new file if none exists.
    O_EXCL   int = syscall.O_EXCL   // used with O_CREATE, file must not exist.
    O_SYNC   int = syscall.O_SYNC   // open for synchronous I/O.
    O_TRUNC  int = syscall.O_TRUNC  // truncate regular writable file when opened.
)
四.File操作
复制代码
package main
​
import (
    "fmt"
    "os"
    "path"
    "path/filepath"
)
​
func main() {
    /*
        文件操作
        1.路径
            相对路径:relative
                ab.txt
                相对于当前工程
            绝对路径:   absolute
                C:\Users\Administrator\Desktop\key.txt
            .当前目录
            ..上一层
        2.创建文件夹,如果文件夹存在,创建失败
            os.MkDir()  创建一层
            os.MkDirAll()  可以创建多层
        3.创建文件,Create采用模式0666(任何人都可读写,不可执行) 创建一个名为name的文件,如果文件已存在会截断它(为空文件)
            os.Create(),   创建文件
        4.打开文件: 让当前的程序,和指定的文件之间建立一个连接
            os.Open(filename)
            os.OpenFile(filename, mode, perm)
        5.关闭文件:程序和文件之间的连接断开 
            file.Close()
        6.删除文件和目录:
            os.Remove()    删除文件和空目录
            os.RemoveAll()    删除所有
    */
    //1.路径
    fileName1 := "C:\\Users\\Administrator\\Desktop\\key.txt"
    fileName2 := "key.txt"
    fmt.Println(filepath.IsAbs(fileName1)) //true
    fmt.Println(filepath.IsAbs(fileName2)) //false
    fmt.Println(filepath.Abs(fileName1))   //C:\Users\Administrator\Desktop\key.txt <nil>
    fmt.Println(filepath.Abs(fileName2))   //D:\study_code\go\key.txt <nil>
    fmt.Println("获取父目录:", path.Join(fileName1, ".."))
    fmt.Println("获取父目录:", filepath.Dir(fileName1))
    fmt.Println(filepath.Base(fileName1))
​
    //2.创建目录
    //err := os.Mkdir("C:\\Users\\Administrator\\Desktop\\key", 0777)
    //if err != nil {
    //  fmt.Println("err:", err)
    //  //return
    //}
    //fmt.Println("文件夹创建成功....")
    //err1 := os.MkdirAll("C:\\Users\\Administrator\\Desktop\\key\\XX\\BB\\DD", os.ModePerm)
    //if err1 != nil {
    //  fmt.Println("err:", err1)
    //  //return
    //}
    //fmt.Println("多层文件夹创建成功....")
​
    //3.创建文件: Create采用模式0666(任何人都可读写,不可执行)创建一个名为name的文件,如果文件已存在会截断它(为空文件)
    //file1, err2 := os.Create("C:\\Users\\Administrator\\Desktop\\key\\ab.txt")
    //if err2 != nil {
    //  fmt.Println("err:", err2)
    //}
    //fmt.Println(file1)
​
    //file3, err3 := os.Create("ab.txt")
    //if err3 != nil {
    //  fmt.Println("err:", err3)
    //}
    //fmt.Println(file3)
    
    //4.打开文件:
    file3, err := os.Open(fileName1)
    if err != nil {
        fmt.Println("err:", err)
    }
    fmt.Println(file3)  
    
    /*
        第一个参数:文件名称
        第二个参数: 文件的打开方式
            const (
            // Exactly one of O_RDONLY, O_WRONLY, or O_RDWR must be specified.
            O_RDONLY int = syscall.O_RDONLY // open the file read-only.
            O_WRONLY int = syscall.O_WRONLY // open the file write-only.
            O_RDWR   int = syscall.O_RDWR   // open the file read-write.
            // The remaining values may be or'ed in to control behavior.
            O_APPEND int = syscall.O_APPEND // append data to the file when writing.
            O_CREATE int = syscall.O_CREAT  // create a new file if none exists.
            O_EXCL   int = syscall.O_EXCL   // used with O_CREATE, file must not exist.
            O_SYNC   int = syscall.O_SYNC   // open for synchronous I/O.
            O_TRUNC  int = syscall.O_TRUNC  // truncate regular writable file when opened.
        )
        第三个参数:文件的权限:文件不存在创建文件,需要指定权限
    */
    file4, err := os.OpenFile(fileName1, os.O_RDONLY|os.O_WRONLY, os.ModePerm)
    if err != nil { 
        fmt.Println("err:", err)
    }
    fmt.Println(file4)
    // 5.关闭文件
    file4.Close()
    
    //6.删除文件或文件夹
    err5 := os.Remove(fileName1)
    if err5 != nil {    
        fmt.Println("err:", err5)
    }
}
五.读文件
复制代码
package main
​
import (
    "bufio"
    "fmt"
    "io/ioutil"
    "os"
)
​
func main() {
    file, err := os.Open("C:\\Users\\Administrator\\Desktop\\key1.txt")
    if err != nil {
        panic(err)
    }
    defer file.Close()
​
    scanner := bufio.NewScanner(file)
    for scanner.Scan() {
        fmt.Println(scanner.Text())
    }
    // 错误处理
    if err := scanner.Err(); err != nil {
        fmt.Println("Error:", err)
    }
​
    content, err := ioutil.ReadFile("C:\\Users\\Administrator\\Desktop\\key1.txt")
    if err != nil {
        panic(err)
    }
    fmt.Println("---------------------------------------------------------------")
    fmt.Println(string(content))
}
参考1:Golang 中的 bufio 包详解(四):bufio.Scanner_golang scanner-CSDN博客
参考2:Go 语言读文件的九种方法 - 王一白 - 博客园
参考3:Go语言读取文件_go 读取文件-CSDN博客
第四章 I/O操作
一、IO包

io包中提供I/O原始操作的一系列接口。它主要包装了一些已有的实现,如os包中的那些,并将这些抽象成为实用性的功能和一些其他相关的接口。

由于这些接口和原始的操作以不同的实现包装了低级操作,我们不应假定它们对于并行执行是安全的。

在IO包中最重要的是两个接口:Reader和Writer接口,首先来介绍这两个接口。

Reader接口的定义,Read()方法用于读取数据。

复制代码
type Reader interface{
    Read(p []byte)(n int, err error)
}

Read将len(p)个字节读取到p中,它返回读取的字节数n(0<=n<=len(p))以及任何遇到的错误。即使Read返回的n<len(p),它也会在调用过程中使用p的全部作为暂存空间。若一些数据可用但不到len(p)个字节,Read会照例返回可用的东西,而不是等待更多。

当Read在成功读取n >0个字节后遇到一个错误或EOF情况,它就会返回读取的字节数。它会从相同的调用中返回(非nil的)错误或从随后的调用中返回错误(和 n==0)。这种一般情况的一个例子就是Reader在输入流结束时会返回一个非零的字节数,可能的返回不是err == EOR就是err==nil.无论如何,下一个Read都应当返回0, EOR.

调用者应当总在考虑到错误err前处理n>0的字符。这样做可以在读取一些字节,以及允许的EOF行为后正确地处理I/O错误。

Read的实现会阻止返回零字节的计数和一个nil错误,调用者应将这种情况视作空操作。

Package io - The Go Programming Language

复制代码
package main
​
import (
    "fmt"
    "io"
    "os"
)
​
func main() {
    /*
        读取数据:
            Reader接口:
                Read(p []byte)(n int, error)
    */
    //读取本地key1.txt文件中的数据
    //step1:打开文件
    filename := "C:\\Users\\Administrator\\Desktop\\key1.txt"
    file, err := os.Open(filename)
    if err != nil {
        fmt.Println("open file err:", err)
        return
    }
    //step3:关闭文件
    defer file.Close()
    //setp2: 读取数据
    bs := make([]byte, 100, 1024)
    //第一次读取
    n, err := file.Read(bs)
    fmt.Println(err)
    fmt.Println(n)
    fmt.Println(bs)         //[47 47 32 82 111 115 101 80 114 111 106 101 99 116 46...
    fmt.Println(string(bs)) // // RoseProject.cpp : 此文件包含 "main" 函数。程序执行将在此处开始并结束。
​
    n = -1
    for {
        n, err = file.Read(bs)
        if n == 0 || err == io.EOF {
            fmt.Println("读取到了文件的末尾, 结束读取操作..")
            break
        }
        fmt.Println(string(bs[:n]))
    }
}

Writer接口的定义,Write()方法用于写出数据。

复制代码
type Writer interface {
    Write(p []byte)(n int, err error)
}

Write将len(p)个字节从p中写入到基本数据流中。它返回从p中被写入的字节数n(0<=n<=len(p))以及任何遇到的引起写入提前停止的错误。若Write返回的n<len(p),它就必须返回一个非nil的错误。Write不能修改此切片的数据,即便它是临时的。

复制代码
package main
​
import (
    "fmt"
    "log"
    "os"
)
​
func main() {
    /*
        写出数据
    */
    fileName := "test.txt"
    //step1: 打开文件
    //step2: 写出数据
    //step3: 关闭文件
    //file, err:= os.Open(fileName)
    file, err := os.OpenFile(fileName, os.O_CREATE|os.O_APPEND|os.O_WRONLY, os.ModePerm)
    if err != nil {
        fmt.Println(err)
        return
    }
    defer file.Close()
    //写出数据
    bs := []byte("hello world")
    n, err := file.Write(bs)
    fmt.Println(n)
    HandleErr(err)
​
    //直接写出字符串
    n, err = file.WriteString("hello xian")
    HandleErr(err)
    
}
func HandleErr(err error) {
    if err != nil {
        log.Fatal(err)
    }
}
二、文件复制

(1) 用io包下的Read()和Write()方法实现

通过io包下的Read()和Write()方法,边读边写,就能够实现文件的复制。这个方法是按块读取文件,块的大小也会影响到程序的性能。

复制代码
package main
​
import (
    "fmt"
    "io"
    "os"
)
​
func main() {
    /*
        拷贝文件
    */
    srcFile := "C:\\Users\\Administrator\\Desktop\\key1.txt"
    destFile := "C:\\Users\\Administrator\\Desktop\\key2.txt"
    total, err := CopyFile(srcFile, destFile)
    fmt.Println(total, err)
    
}
​
//该函数:用于通过io操作实现文件的拷贝,返回值是拷贝的总数量(字节),错误
func CopyFile(srcFile, destFile string) (int, error) {
    file1, err := os.Open(srcFile)
    if err != nil {
        return 0, err
    }
    file2, err := os.OpenFile(destFile, os.O_WRONLY|os.O_CREATE, 0666)
    if err != nil {
        return 0, err
    }
    defer file1.Close()
    defer file2.Close()
​
    //读定
    bs := make([]byte, 1024, 1024)
    n := -1
    total := 0
    for {
        n, err = file1.Read(bs)
        if err == io.EOF || n == 0 {
            fmt.Println("拷贝完毕...")
            break
        } else if err != nil {
            fmt.Println("报错了...")
            return total, err
        }
        total += n
        file2.Write(bs[:n])
    }
    return total, nil
}

(2) io包下的Copy()方法实现

复制代码
func CopyFile2(srcFile, destFile string) (int64, error) {
    file1, err := os.Open(srcFile)
    if err != nil {
        return 0, err
    }
    file2, err := os.OpenFile(destFile, os.O_RDWR|os.O_CREATE, 0666)
    if err != nil {
        return 0, err
    }
    defer file1.Close()
    defer file2.Close()
    return io.Copy(file1, file2)
}

注:ioutil包从1.16开始被取消,同样的功能可由os和io包下相应的功能实现。

三、断点续传

(1)Seeker接口

复制代码
package main
​
import (
    "fmt"
    "io"
    "log"
    "os"
)
​
func main() {
    /*
                Seek(offset int64, whence int) (int64, error), 设置指针光标的位置
                第一个参数: 偏移量
                第二个参数:如何设置
                          0:seekstart, 表示相对于文件开始
                          1:seekcurrent, 表示相对于当前位置的偏移量
                          2:seekend, 表示相对于末尾
                          // Seek whence values.
                            const (
                                SeekStart   = 0 // seek relative to the origin of the file
                                SeekCurrent = 1 // seek relative to the current offset
                                SeekEnd     = 2 // seek relative to the end
                            )
    */
    fileName := "C:\\Users\\Administrator\\Desktop\\key1.txt"
    file, err := os.OpenFile(fileName, os.O_RDWR, os.ModePerm)
    if err != nil {
        log.Fatal(err)
    }
    defer file.Close()
    //读写
    bs := []byte{0}
    file.Read(bs)
    fmt.Println(string(bs))
​
    file.Seek(4, io.SeekStart)
    file.Read(bs)
    fmt.Println(string(bs))
​
    file.Seek(3, 0) //SeekStart
    file.Read(bs)
    fmt.Println(string(bs))
​
    file.Seek(4, io.SeekCurrent)
    file.Read(bs)
    fmt.Println(string(bs))
​
    file.Seek(0, io.SeekEnd)
    file.WriteString("hello world")
}
​

(2)断点续传

复制代码
package main
​
import (
    "fmt"
    "io"
    "log"
    "os"
    "strconv"
)
​
func main() {
    /*
        断点续传
            文件传递:文件复制
        将文件复制到当前的工程下:边复制,边记录复制的总量
    */
    srcFile := ""
    destFile := ""
    fmt.Println("srcFile:", srcFile)
    fmt.Println("destFile:", destFile)
    tempFile := destFile + "_temp.txt"
    fmt.Println("tempFile:", tempFile)
    file1, err := os.Open(srcFile)
    HandleError(err)
    file2, err := os.OpenFile(destFile, os.O_WRONLY|os.O_CREATE, 07777)
    HandleError(err)
    file3, err := os.OpenFile(tempFile, os.O_WRONLY|os.O_CREATE, 07777)
    HandleError(err)
    defer file1.Close()
    defer file2.Close()
​
    //step1.先读取临时文件中的数量,再seek
    file3.Seek(0, io.SeekStart)
    bs := make([]byte, 100, 100)
    n1, err := file3.Read(bs)
    HandleError(err)
    countStr := string(bs[:n1])
    count, err := strconv.ParseInt(countStr, 10, 64)
    HandleError(err)
    fmt.Println("count:", count)
​
    //step2:设置读,写的位置
    file1.Seek(count, io.SeekStart)
    file2.Seek(count, io.SeekStart)
    data := make([]byte, 1024, 1024)
    n2 := -1            //读取的数据量
    n3 := -1            //写出的数据量
    total := int(count) //读取的总量
​
    //step3:复制文件
    for {
        n2, err = file1.Read(data)
        if err == io.EOF || n2 == 0 {
            fmt.Println("文件复制完毕。。。", total)
            file3.Close()
            os.Remove(tempFile)
            break
        }
        n3, err = file2.Write(data[:n2])
        total += n3
​
        //将复制的总量,存储到临时文件中
        file3.Seek(0, io.SeekStart)
        file3.WriteString(strconv.Itoa(total))
        fmt.Println(total)
    }
}
​
func HandleError(err error) {
    if err != nil {
        log.Fatal(err)
    }
}
第五章 bufio包

go语言在io操作中,还提供了一个buffo的包,使用这个包可以大幅提高文件读写的效率。

一、bufio包原理

bufio是通过缓冲来提高效率。

io操作本身的效率并不低,低的是频繁的访问本地磁盘的文件。所以bufio就提供了缓冲区(分配一块内存),读和写都先在缓冲区中,最后再读写文件,最后再读写文件,来降低访问本地磁盘的次数,从而提高效率。

二、bufio.Read
复制代码
package main
​
import (
    "bufio"
    "fmt"
    "os"
)
​
func main() {
    /*
        bufio:高效io读写
            buffer缓存
            io: input / output
        将io包下的Reader, Write对象进行包装,带缓存的包装,提高读写的效率
            ReadBytes()
            ReadString()
            ReadLine()
    */
    fileName := "C:\\Users\\Administrator\\Desktop\\key1.txt"
    file, err := os.Open(fileName)
    if err != nil {
        fmt.Println(err)
        return
    }
    defer file.Close()
    
    //创建Reader对象
    b1 := bufio.NewReader(file)
    //1.Read(), 高效读取
    //p := make([]byte, 1024)
    //n1, err := b1.Read(p)
    //fmt.Println(n1)
    //fmt.Println(string(p[:n1]))
    //2.ReadLine()
    //data, flag, err := b1.ReadLine()
    //fmt.Println(flag)
    //fmt.Println(err)
    //fmt.Println(data)
    //fmt.Println(string(data))
    
    //3.ReadString()
    //s1, err := b1.ReadString('\n')
    //fmt.Println(err)
    //fmt.Println(s1)
    //
    //for {
    //  s1, err = b1.ReadString('\n')
    //  if err == io.EOF {  
    //      fmt.Println("读取完毕")
    //      break
    //  }
    //  fmt.Println(s1)
    //}
    
    //4.ReadBytes()
    //data,err := b1.ReadBytes('\n')
    //fmt.Println(err)
    //fmt.Println(string(data))
    
    //Scanner
    //s2 := ""
    //fmt.Scanln(&s2)
    //fmt.Println(s2)
    
    b2 := bufio.NewReader(os.Stdin)
    s2, _ := b2.ReadBytes('\n')
    fmt.Println(s2)
}
三、bufio.write
复制代码
package main
​
import (
    "bufio"
    "fmt"
    "os"
)
​
func main() {
    /*
        bufio:高效io缓存
            buffer缓存
            io: input/output
        将io包下的Reader,Write对象进行包装,带缓存的包装,提高读写的效率
            func(b *Writer) Write(p []byte) (nn int, err error)
            func(b *Writer) WriteByte(c byte) error
            func(b *Writer) WriteRunc(r runc) (size int, err error)
            func(b *Writer) WriteString(s string) (int, error)
    */
    fileName := ""
    file, err := os.OpenFile(fileName, os.O_CREATE | os.O_WRONLY, os.ModePerm)
    if err != nil {
        fmt.Println(err)
        return
    }
    defer file.Close()
    w1 := bufio.NewWriter(file)
    // n, err := w1.WriteString("helloWorld")
    // fmt.Println(err)
    // fmt.Println(n)
    // w1.Flush()   //刷新缓冲区
​
    for i :=1; i<=1000;i++ {
        w1.WriteString(fmt.Sprintf("%d:helloworld", i))
    }
    w1.Flush() 
}
第六章 并发编程

Go是并发语言,而不是并行语言。

一、golang中的并发编程
1、多任务

操作系统可以同时运行多个任务

2、并发

go是并发语言,而不是并行语言, 并发性Concurrency是同时处理许多事情的能力。

并行性parallelism, 就是同时做很多事情。

并行性Parallelism不会总是导致更快的执行时间。这是因为并行运行的组件可能需要相互通信。

3、进程、线程、协程

进程 (Process) , 线程(Thread) , 协程(Coroutine, 也叫轻量级线程)

进程

进程是一个程序在一个数据集中的一次动态执行过程,可以简单理解为"正在执行的程序",它是CPU资源分配和调度的独立单元。

进行一般由程序、数据集、进程控制块三部分组成。编写的程序用来描述进程要完成哪些功能以及如何完成;数据集则是程序在执行过程中所需要使用的资源;进程控制块用来记录的外部特征,描述进程的执行变化 过程,系统可以利用它来控制和管理进程,它是系统感知进程存在的唯一标志。进程的局限是创建、撤销和切换的开销比较大。

线程

线程是在进程之后发展出来的概念。线程也叫轻量级进程,它是一个基本的CPU执行单元,也是程序执行过程中的最小单元,由线程ID、程序计数器、寄存器集合和堆栈共同组成。一个进程可以包含多个线程。

线程的优点是减小了程序并发执行时的开销,提高了操作系统的并发性能,缺点是线程没有自己的系统资源,只拥有在运行时必不可少的资源,但同一进程的各线程可以共享进程所拥有的系统资源,如果把进程比作一个车间,那么线程就好比是车间里面的工人,不过对于某些独占性资源存在锁机制,处理不当可能会产生"死锁"。

协程

协程是一种用户态的轻量级线程,又称微线程,英文名Coroutine, 协和的调度完全由用户控制。人们通常将协程和子程序(函数)比较着理解。

子程序调用总是一个入口,一次返回,一旦退出即完成了子程序的执行。

与传统的系统级线程和进程相比,协程的最大优势在于其"轻量级",可以轻松创建上百万个而不会导致系统资源衰竭,而线程和进程通常最多也不能超过1万的。这也是协程也叫轻量级线程的原因。

协程与多线程相比,其优势体现在:协程的执行效率极高.因为子程序切换不是线程切换,而是由程序自身控制,因此,没有线程切换的开销,和多线程比,线程数量越多,协程的性能优势就越明显.

Go语言对于并发的实现是靠协和, Goroutine

二、Goroutine
1.什么是Goroutine

go中使用Goroutine来实现并发concurrently.

Goroute是Go语言特有的名词。区别于进程Process,线程Thread,协程Coroutine是专门创造的。

Coroutine是与其他函数或方法同时运行的函数或方法。Coroutione可以被认为是轻量级的线程。与线程相比,创建Goroutine的成本很小,它就是一段码,一个函数入口。以及在推上为其分本的一个堆栈(初始大小为4K,会随着程序的执行自动增长删除)。因此非常廉价,Go应用程序可以并发运行数千个Goroutine。

Goroutine在线程上的优势

1.在线程相比,Doroutines非常便宜.它们只是堆栈大小的几个kb,堆栈可以根据应用程序的需要增长和收缩,而在线程的情况下,堆栈大小必须指定并且固定.

2.Goroutine被多路复用到较少的OS线程.在一个程序中可能只有一个线程与数千个Coroutines.如果线程中的任何Coroutine都表示等待用户输入,则会创建一个OS线程,剩下的Coroutine被转移到新的OS线程,所有这些都由运行时进行处理, 程序员从这些复杂的细节中抽像出来,并得到一个与并发工作相关的开净的API

3.当使用Coroutines访问共享内存时,通过设计的通道可以防止竞态条件发生.通道可以被认为是Coroutines通信的管道.

2.主goroutine

封装main函数的Goroutine称为主.

主goroutine所做的事情并不是执行main函数那么简单,它首先要做的是:设定每一个goroutine所能申请的栈空间的最大尺寸.在32位的计算机系统中此最大尺寸为250MB,而在64位的计算机系统中此尺寸为1GB,如果有某个Goroutine的栈空间大于这个限制.那么运行时系统就会引发一个栈溢出(stack overflow)的运行时恐慌.随后,这个go程序的运行也会终止.

主goroutine进行一系列的初始化工作,涉及的工作内容大致如下:

1.创建一个特殊的defer语句,用于在主goroutine退出时做必要的善后处理.因为主goroutine也可能非正常的结束.

2.启动专用于在后台清扫内存垃圾的goroutine,并设置GC可用的标识.

3.执行main包中的init函数

4.执行main函数

执行完main函数后,它还会检查主goroutine是否引发了运行时恐慌.并行进必要的处理.最后主goroutine会结束自己以及当前进程的运行.

3.如何使用Goroutines

在函数或方法调用前面加上关键字go, 将会同时运行一个新的Goroutine.

实例代码:

复制代码
package main
​
import (
    "fmt"
)
​
func hello() {
    fmt.Println("hello world goroutine")
}
​
func main() {
    go hello()
    fmt.Println("main function")
}

运行结果可能会输出"main function"

需要了解的Goroutine的规则

  1. 当新的Goroutine开始时,Goroutine调用立即返回.与函数不同,go不等待Goroutine执行结束.当Goroutine调用,并且Goroutine的任何返回值被忽略之后,go立即执行到下一行代码.

  2. main的Goroutine应该为其他的Goroutine执行.如果main的Goroutine终止了,程序将被终止,而其他Goroutine将不会运行.

4.启动多个Goroutines
复制代码
package main
​
import (
    "fmt"
    "time"
)
​
func numbers() {
    for i := 1; i <= 5; i++ {
        time.Sleep(250 * time.Millisecond)
        fmt.Printf("%d ", i)
    }
}
func alphabets() {
    for i := 'a'; i <= 'e'; i++ {
        time.Sleep(400 * time.Millisecond)
        fmt.Printf("%c ", i)
    }
}
​
func main() {
    go numbers()
    go alphabets()
    time.Sleep(3000 * time.Millisecond)
    fmt.Println("main terminated")
}
三、Go语言的并发模型
1、线程模型

在现代操作系统中,线程是处理器调度和分本的基本单位,进程则作为资源拥有的基本单位。每个进程是由私有的虚拟地址空间、代码、数据和其它各种系统资源组成。线程是进程内部的一个执行单元。每一个进程至少有一个主执行线程,它无需由用户去主动创建,是由系统自动创建的。用户根据需要在应用程序中创建其他线程,多个线程并发地运行于同一个进程中。

线程的实现模型主要有3个,分别是:用户级线程模型、内核级线程模型和两级线程模型。它们之间最大的差异就在于线程与内核调度实体(Kernel Scheduling Entity. 简称KSE) 之间的对应关系上。顾名思义,内核调度实体就是可以被内核的调度器调度的对象。有被称为内核级线程,是操作系统内核的最小调度单元。

(1).内核级线程模型:

用户线程与KSE是1对1关系.大部分编程语言的线程库(如linux的pthread, Java的java.lang.Thread,C++11的std::thread等等)都是对操作系统的线程(内核级线程)的一层封装,创建出来的每个线程与一个不同的KSE静态关联,因此其调度完全由OS调度器来做。

(2).用户线程模型

用户线程与KSE是多对1关系(M:1),这种线程的创建,销毁以及多个线程之间的协调等操作都是由用户自己实现的线程库来负责,对OS内核透明,一个进程中所有创建的线程都与同一个KSE在运行时动态关联。实现地的协程基本都是这样实现的。

(3).两级线程模型

用户线程与KSE是多对多关系(M:N), 这种实现综合了前两种模型的优点,为一个进程中创建多个KSE,并且线程可以与不同的KSE在运行时进行动态关联,当某个KSE由其上工作的线程的阻塞操作被内核调度出CPU时,当前与其关联的其余用户线程可以重新与其他KSE建立关联关系。 Go语言中的并发就是使用的这种实现方式,Go为了实现该模型自己实现了一个运行时调度器来负责Go中的"线程"与KSE的动态关联。此模型有时也被称为 混合型线程模型,即用户调度器实现用户线程到KSE的"调度",内核调度器实现KSE到CPU上的调度。

2、Go并发调度:G-P-M模型

在操作系统提供的内核线程之上,Go搭建了一个特有的两级线程模型。goroutine机制实现了M:N的线程模型,goroutine机制是协和(coroutine)的一种实现,golang内置的调度器,可以让多核CPU中每个CPU执行一个协程。

理解Goroutine机制的原理,就是现解Go语言scheduler的实现

Go语言中支撑整个schedule实现的主要有4个重要结构,分别是M、G、P、Sched,前三个定义在runtine.h中,Sched定义在proc.c中。

Sched结构就是调度器,它维护有存储M和G的队列以及调度器的一些状态信息等。

M结构是Machine,系统线程,它由操作系统管理的,goroutine就是跑在M之上的,M是一个很大的结构,里面维护小对象内存cache(mcache)\当前执行的goroutine、随机数发生器等等非常多的信息。

P结构是Processor,处理器,它的主要用途就是用来执行goroutine的,它维护了一个goroutine队列,即runqueue.Processor是让我们从N:1调度到M:N调度的重要部分。

G是goroutine实现的核心结构,它包含了栈,指令指针,以及其他对调度goroutine很重要的信息,例如其阻塞的channel.

runtime包

地址:Package runtime - The Go Programming Language

复制代码
package main
​
import (
    "fmt"
    "runtime"
    "time"
)
​
func init() {
    //获取逻辑cpu的数量
    fmt.Println("逻辑CPU的数量--->", runtime.NumCPU())
    //设置go程序执行的最大的cpu数量: [1, 256]
    n := runtime.GOMAXPROCS(8)
    fmt.Println(n)
}
​
func main() {
    //获取goroot目录
    fmt.Println("GOROOT--->", runtime.GOROOT()) //C:\Program Files\Go
    //获取操作系统
    fmt.Println("os/platform", runtime.GOOS) //os/platform windows
​
    //gosched
    go func() {
        for i := 0; i < 5; i++ {
            fmt.Println("goroutine....")
        }
    }()
​
    for i := 0; i < 4; i++ {
        //让出时间片,先让别的goroutine时间片
        runtime.Gosched()
        fmt.Println("main....")
    }
​
    //创建goroutine
    go func() {
        fmt.Println("goroutine开始...")
        //调用fun
        fun()
        fmt.Println("goroutine结束...")
    }()
​
    time.Sleep(3 * time.Second)
}
func fun() {
    defer fmt.Println("derfer....")
    //return //终止函数
    runtime.Goexit()    //结束当前的goroutine
    fmt.Println("fun函数....")
}
四、临界资源安全问题

1、临界资源

临界资源:指并发环境中多个进程/线程/协程共享的资源.

但在并发编程中对临界资源的处理不当,往往会导致数据不一致的问题

复制代码
package main
​
import (
    "fmt"
    "time"
)
​
func main() {
    /*
      临界资源:
    */
    a := 1
    go func() {
        a = 2
        fmt.Println("goruntine...", a)
    }()
    a = 3
    time.Sleep(1)
    fmt.Println("main goroutine...", a)
}

go run -race demo03_race.go

出现问题:go: -race requires cgo; enable cgo by setting CGO_ENABLED=1

参考1:CGO详解:Go语言与C/C++的交互-CSDN博客

参考2:01.MinGW下载及其安装-CSDN博客

复制代码
package main
​
import (
    "fmt"
    "math/rand"
    "time"
)
​
// 全局变量,表示票
var ticket = 100 //100张票
func main() {
    /**
    4个goroutine, 模拟4个售票口
​
    */
    go saleTickets("售票窗口1")
    go saleTickets("售票窗口2")
    go saleTickets("售票窗口3")
    go saleTickets("售票窗口4")
    time.Sleep(5 * time.Second)
}
​
func saleTickets(name string) {
    rand.Seed(time.Now().UnixNano())
    for {
        if ticket > 0 {
            time.Sleep(time.Duration(rand.Intn(1000)) * time.Millisecond)
            fmt.Println(name, "售出: ", ticket)
            ticket--
        } else {
            fmt.Println("售完")
            break
        }
    }
}

可以使用同步锁来解决这个问题

在Go的并发编程中有一句很经典的话:不要以共享内存的方式去通信,而要以通信的方式去共享内存。

sync包WaitGroup

Package sync provides basic synchronization primitives such as mutual exclusion locks. Other than the Once and WaitGroup types, most are intended for use by low-level library routines. Higher-level synchronization is better done via channels and communication.

sync仅使用来完成基础的同步,高水平的同步最好使用channel

Package sync - The Go Programming Language

Package sync - The Go Programming Language

复制代码
package main
​
import (
    "fmt"
    "sync"
)
​
var wg sync.WaitGroup //创建同步等待组的对象
func main() {
    /*
        WaitGroup: 同步等待组
            Add(), 设置等待组中要执行的了goroutine的数量
            wait(), 让主goroutine出于等待
    */
    wg.Add(2)
    go fun1()
    go fun2()
​
    fmt.Println("main 进入阻塞状态。。 等待wg中的子goroutine结束..")
    wg.Wait() //表示main goroutine进入等待,意味着阻塞
    fmt.Println("main..解除阻塞")
}
​
func fun1() {
    for i := 1; i < 10; i++ {
        fmt.Println("fun1()函数中打印...A", i)
    }
    wg.Done() //给wg等待组中的counter数值减1,同 wg.Add(-1)
}
​
func fun2() {
    defer wg.Done()
    for j := 1; j < 10; j++ {
        fmt.Println("\tfun2()函数打印...", j)
    }
}

互斥锁

复制代码
package main
​
import (
    "fmt"
    "math/rand"
    "sync"
    "time"
)
​
// 全局变量,表示票
var ticket = 10      //100张票
var mutex sync.Mutex //创建锁头
var wg sync.WaitGroup
​
func main() {
    /**
    4个goroutine, 模拟4个售票口
    在使用互斥锁的时候,对资源操作完,一定要解锁。否则会出现死锁,程序异常等情况,推荐使用defer语句来解锁
    */
    wg.Add(4)
    go saleTickets("售票窗口1")
    go saleTickets("售票窗口2")
    go saleTickets("售票窗口3")
    go saleTickets("售票窗口4")
    wg.Wait()
    fmt.Println("程序结束了....")
    //time.Sleep(5 * time.Second)
}
​
func saleTickets(name string) {
    rand.Seed(time.Now().UnixNano())
    defer wg.Done()
    for {
        //上锁
        mutex.Lock()
        if ticket > 0 {
            time.Sleep(time.Duration(rand.Intn(1000)) * time.Millisecond)
            fmt.Println(name, "售出: ", ticket)
            ticket--
        } else {
            mutex.Unlock() //条件不满足也要解锁
            fmt.Println("售完")
            break
        }
        mutex.Unlock()
    }
}

读写锁

复制代码
package main
​
import (
    "fmt"
    "sync"
    "time"
)
​
var rwMutex *sync.RWMutex
var wg *sync.WaitGroup
​
func main() {
    rwMutex = new(sync.RWMutex)
    wg = new(sync.WaitGroup)
​
    //wg.Add(2)
    //go readData(1)
    //go readData(2)
    //wg.Wait()
    wg.Add(3)
    go writeData(1)
    go readData(2)
    go writeData(3)
    wg.Wait()
    fmt.Println("main.over......")
}
func writeData(i int) {
    defer wg.Done()
    fmt.Println("开始写:write start。。。")
    rwMutex.Lock()   //写操作上锁
    fmt.Println("正在写: write.....")
    time.Sleep(1 * time.Second)
    rwMutex.Unlock()
    fmt.Println(i, "写结束: write over....")
}
func readData(i int) {
    defer wg.Done()
    fmt.Println(i, "开始读:read start....")
    rwMutex.RLock() //读操作上锁
    fmt.Println("正在读取数据: reading。。。。。")
    time.Sleep(1 * time.Second)
    rwMutex.RUnlock() //读操作解锁
    fmt.Println(i, "读结束:read over。。。。")
}

基本遵循两大原则:

1.可以随便读,多个goroutine同时读。

2.写的时候,啥也不能干,不能读也不能写。

五、channel通道

通道可以被认为是coroutines通信的管道。

"不要通过共享内存来通信,而应该通过通信来共享内存"

1.什么是通道

通道的概念

通道就是goroutine之间的通道。它可以让goroutine之间相互通信。

每个通道都有与其相关的类型,该类型是通道允许传输的数据类型。通道的零值为nil.

nil通道没有任何用处,因此通道必须使用类似于map和切片的方法来定义。

通道的声明

声明一个通道和定义一个变量的语法一样

复制代码
//声明通道
var 通道名 chan 数据类型
//创建通道: 如果通道为nil(就是不存在),就需要先创建通道
通道名 = make(chan 数据类型)
​
a:=make(chan int)

2.通道的使用语法

发送和接收

复制代码
data := <- a    //read from channel a
a <- data       //write to channel a

在通道上箭头的方向指定数据是发送还是接收

别外:

复制代码
v, ok := <-a  //从一个channel中读取
复制代码
package main
​
import "fmt"
​
func main() {
    var ch1 chan bool
    ch1 = make(chan bool)
​
    go func() {
        for i := 0; i < 10; i++ {
            fmt.Println("子goroutine中,i: ", i)
        }
        //偱环结束后, 向通道中写数据,表示要结束了...
        ch1 <- true
        fmt.Println("结束...")
    }()
​
    data := <-ch1
    fmt.Println("main....data--->", data)
    fmt.Println("main....over....")
}

Channel通道在使用的时候,有以下几个注意点

1.用于goroutine,传递消息的。

2.通道,每个都胡相关联的数据类型 nil chan, 不能使用,类似于nil map, 不能直接存储键值对

3.使用通道传递数据: <-

chan <-data, 发送数据到通道,向通道中写数据

data <- chan, 从通道中获取数据,从通道中读数据

4.阻塞:

发送数据:chan <- data, 阻塞的, 直到另一条goroutine,读取数据来解除阻塞

读取数据:data <- chan, 也是阻塞的,直到另一条goroutine,写出数据解除阻塞

5.本身channel就是同步的,意味着同一时间,只能有一条goroutine来操作.

最后:通道是goroutine之间的连接,所以通道的发送和接收必须处在不同的goroutine中。

复制代码
package main
​
import (
    "fmt"
    "time"
)
​
func main() {
    ch1 := make(chan int)
    go func() {
        fmt.Println("子goroutine开始执行....")
​
        data := <-ch1 //从ch1中读取数据
        fmt.Println("data: ", data)
    }()
​
    ch1 <- 10
    time.Sleep(3 * time.Second)
    fmt.Println("main..over...")
​
    ch := make(chan int)
    ch <- 100 //阻塞     //fatal error: all goroutines are asleep - deadlock! 
}

死锁

使用通道时要考虑的一个重要因素是死锁。如果Goroutine在一个通道上发送数据,那么预计其他的Goroutine应该接收数据。如果这种情况不发生,那么程序将在运行时出现死锁。

类似的如果Goroutine正在等待从通道接收数据,那么另一些Goroutine将会在该通道上写入数据,否则程序将会死锁。

复制代码
fatal error: all goroutines are asleep - deadlock! 
六、关闭通道和通道上范围循环

1.关闭通道

发送者可以通过关闭信道,来通知接收方不会有更多的数据被发送到channel上。

复制代码
chose(ch)

接收者可以在接收来自通道的数据时使用额外的变量来检查通道是否已经关闭。

语法结构

复制代码
v, ok := <-ch

类似map操作,存储key, value键值对

v, ok := map[key] //根据key从map中获取value, 如果key存在,v就是对应的数据,如果key不存在,v是默认值

在上面的语句中,如果ok的值是true, 表示成功的从通道中读取了一个数据value.如果ok是false,这意味着我们正在从一个封闭的通道读取数据。从闭通道中读取的值将是通道类型的零值。

复制代码
package main
​
import "fmt"
​
func main() {
    /*
        关闭通道:close(ch)
        子goroutine: 写出10个数据
            每写一个,阻塞一次,主goroutine读取一次,解除阻塞
        主goroutine, 读取数据
            每次读取数据,阻塞一次,子goroutine,写出一个,解除阻塞
    */
    ch1 := make(chan int)
    go sendData(ch1)
​
    //读取通道的数据
    for {
        v, ok := <-ch1
        if !ok {
            fmt.Println("已经读取了所有的数据。。。", ok)
            break
        }
        fmt.Println("读取的数据:", v, ok)
    }
    fmt.Println("main...over...")
}
​
func sendData(ch1 chan int) {
    //发送方: 10条数据
    for i := 10; i < 10; i++ {
        ch1 <- i //将i写入到通道中
    }
    close(ch1)
}

2.通道上的范围循环

可以循环从通道上 获取数据,直到通道关闭。for循环的 for range形式可用于从通道接收值,直到它关闭为止。

复制代码
package main
​
import (
    "fmt"
    "time"
)
​
func main() {
    /**
    通过range访问通道
​
    */
    ch1 := make(chan int)
    go sendData(ch1)
​
    //for循环的for range, 来访问通道
    for v := range ch1 { // v<-ch1
        fmt.Println("读取数据:", v)
    }
    fmt.Println("main...over...")
}
​
func sendData(ch1 chan int) {
    for i := 0; i < 10; i++ {
        time.Sleep(1 * time.Second)
        ch1 <- i
    }
    close(ch1) //通知对方,通道关闭
}
七、缓冲通道

1、非缓冲通道

发送和接收到一个未缓冲的通道是阻塞的。

一次发送操作对应一次接收操作,对于一个goroutine来讲,它的一次发送,在另一个goroutine接收之间都是阻塞的。同样的,对于接收来讲,在另一个goroutine发送之前,它也是阻塞。

2、缓冲通道

缓冲通道就是指一个通道,带有一个缓冲区。发送到一个缓冲通道只有在缓冲区满时才被阻塞。类似地,从缓冲通道接收的信息只有在缓冲区为空时才会阻塞。

可以通过将额外的容量参数传递给make函数来创建缓冲通道,该函数指定缓冲区的大小。

语法:

复制代码
ch := make(chan type, capacity)

上述语法的容量应该大于0,以便通道具有缓冲区。默认情况下,无缓冲通道的容量为0,因此在之前创建通道时省略了容量参数。

复制代码
package main
​
import (
    "fmt"
    "strconv"
)
​
func main() {
    /*
          非缓冲通道: make(chan T)
             一次发送,一次接收,都是阻塞的
          缓冲通道: make(chan T, capacity)
             发送: 缓冲区的数据满了,才会阻塞
             接收: 缓冲区的数据空了,才会阻塞
    */
    ch1 := make(chan int) //非缓冲通道
    fmt.Println(len(ch1), cap(ch1))
    //ch1 <- 100  //阻塞式的,需要有其他的goroutine解除阻塞,否则deadlock
​
    ch2 := make(chan int, 5) //缓冲通道,缓冲区大小是5
    ch2 <- 100
    fmt.Println(len(ch2), cap(ch2))
    ch2 <- 200
    ch2 <- 300
    ch2 <- 400
    ch2 <- 500
    fmt.Println(len(ch2), cap(ch2))
    //ch2 <- 600     // fatal error: all goroutines are asleep - deadlock!
​
    fmt.Println("-------------------------------")
    ch3 := make(chan string, 4)
    go sendData(ch3)
​
    for {
        v, ok := <-ch3
        if !ok {
            fmt.Println("读完了...", ok)
            break
        }
        fmt.Println("\t读取的数据是:", v)
    }
    fmt.Println("main...over...")
}
​
func sendData(ch chan string) {
    for i := 0; i < 10; i++ {
        ch <- "数据" + strconv.Itoa(i)
        fmt.Printf("子goroutine中写出第 %d 个数据\n", i)
    }
    close(ch)
}

八、定向通道

1、双向通道

通道,channel,是用于实现goroutine之间的通信的。一个goroutine可以向通道中发送数据,另一个goroutine可以从该通道中获取数据。把这种既可以发送数据,也可以读取数据的通道叫做双向通道。

复制代码
data := <-a   //read from channel a
a <- data  //write to channel a

示例:

复制代码
package main
​
import "fmt"
​
func main() {
    /*
          双向:
             chan
                chan <- data, 发送数据, 写出
                data <- chan, 获取数据, 读取
          单向:定向
                chan <- T, 只支持写
                <-chan T, 只读
    */
    ch1 := make(chan string)
    done := make(chan bool)
    go sendData(ch1, done)
​
    data := <-ch1 //读取
    fmt.Println("子goroutine传来:", data)
​
    ch1 <- "主main"
    <-done
    fmt.Println("main...over...")
}
​
func sendData(ch1 chan string, done chan bool) {
    ch1 <- "你是大太阳" //发送
    data := <-ch1  //读取
    fmt.Println("main goroutine传来:", data)
    done <- true
}
​

2、单向通道

单向通道,也就是定向通道。

创建单向通道,这些通道只能发送或者接收数据。

复制代码
package main
​
import "fmt"
​
func main() {
    /*
        单向:定向
        chan <- T, 只支持写
        <- chan T, 只读
    
     定向通道:
        双向: ----> 函数:只读,只写
    */
    ch1 := make(chan int)   //双向,读, 写
    ch2 := make(chan<- int) //单向,只能写,不能读
    //ch3 := make(<- chan int)  //单向,只能读,不能写
​
    //ch2 <- 1000
    //data := <-ch2   //invalid operation: cannot receive from send-only channel ch2 (variable of type chan<- int)
    //ch3 <- 2000       // Invalid operation: ch3 <- 2000 (send to the receive-only type <-chan int)
    go fun1(ch1) //可读,可写
    go fun1(ch2) //只写
​
    data := <-ch1
    fmt.Println("fun1函数中写出的数据是:", data)
    go fun2(ch1)
    ch1 <- 200
    fmt.Println("main...over....")
}
​
// 该函数,只能操作只写的通道
func fun1(ch chan<- int) {
    //在函数内部,对于ch1通道,只能写数据,不能读取数据
    ch <- 100
    fmt.Println("fun1函数结束")
}
​
// 该函数,只能操作只读的通道
func fun2(ch <-chan int) {
    data := <-ch
    fmt.Println("fun2函数,从ch中读取的数据是: ", data)
}
第七章 time包中的通道相关函数

Package time - The Go Programming Language

主要是定时器,标准库中的Timer让用户可以定义自己的超时逻辑,尤其是在应对select处理多个channel的超时、单channel读写的超时等情形时尤为方便。

Timer是一次性的时间触发事件,这点与Ticker不同,Ticker是按一定时间间隔持续触发时间事件。

Timer常见的创建方式:

复制代码
t := time.NewTimer(d)
t := time.AfterFunc(d, f)
c := time.After(d)

Timer有3个要素:

复制代码
定时时间:d
触发动作:f
时间channel: t.C

一、time.NewTimer()

Package time - The Go Programming Language

二、timer.Stop

复制代码
package main
​
import (
    "fmt"
    "time"
)
​
func main() {
    /*
          1. func NewTime(d Duratione) *Timer
                  创建一个计时器,d时间以后触发
    */
    timer := time.NewTimer(3 * time.Second)
    fmt.Printf("%T\n", timer) //*time.Timer
    fmt.Println(time.Now())   //2025-03-13 15:19:12.7081165 +0800 CST m=+0.011347601
​
    //此处等待channel中的数值,会阻塞3秒
    ch2 := timer.C
    fmt.Println(<-ch2)
​
    //新建一个计时器
    timer2 := time.NewTimer(5 * time.Second)
    //开始goroutine,来处理触发后的事件
    go func() {
        <-timer2.C
        fmt.Println("Timer 2 结束了...开始....")
    }()
​
    time.Sleep(3 * time.Second)
    flag := timer2.Stop()
    if flag {
        fmt.Println("Timer 2 停止了...")
    }
}

三、time.After()

复制代码
package main
​
import (
    "fmt"
    "time"
)
​
func main() {
    /*
          2. func After(d Duration) <-chan Time
                  返回一个通道: chan, 存储的是d时间间隔之后的当前时间
             相当于: return NewTimer(d).C
    */
    ch := time.After(3 * time.Second)
    fmt.Printf("%T\n", ch)
    fmt.Println(time.Now()) //2025-03-13 15:40:49.4947533 +0800 CST m=+0.009225401
​
    time2 := <-ch
    fmt.Println(time2)      //2025-03-13 15:40:52.5092669 +0800 CST m=+3.023739001
}

select语句

select是Go中的一个控制结构。select语句类似于switch语句,但是select会随机执行一个可运行的case。如果没有case语句可运行,它将阻塞,直到有case可运行.

一、语法结构

select语句的语法结构和switch语句很相似,也有case语句和default语句。

复制代码
select {
    case communication clause;
        statement(s);
    case communication clause:
        statement(s);
    /*你可以定义任意数量的case*/
    default:   /*可选*/
        statement(s);
}

说明:

每个case都必须是一个通信。

所有channel表达式都会被求值

所有被发送的表达式都会被求值

如果有多个case都可以运行,select会随机公平地选出一个执行。其他不会执行。

否则:如果有default子句,则执行该语句。

如果没有default字句,select将阻塞,直到某个通信可以运行; Go不会重新对channel或值进行求值。

复制代码
package main
​
import (
    "fmt"
    "time"
)
​
func main() {
    /*
        分支语句:if, switch, select
        select语句类型于switch语句
            但是select语句会随机执行一个可运行的case
            如果没有case可以运行,要看是否有default,如果有就执行default,否则就进入阻塞,直到有case可以运行
    */
    ch1 := make(chan int)
    ch2 := make(chan int)
​
    go func() {
        time.Sleep(3 * time.Second)
        ch1 <- 100
    }()
​
    go func() {
        time.Sleep(3 * time.Second)
        ch2 <- 200
    }()
​
    select {
    case num1 := <-ch1:
        fmt.Println("ch1中获取的数据。。。", num1)
    case num2, ok := <-ch2:
        if ok {
            fmt.Println("ch2中读取的数据...", num2)
        } else {
            fmt.Println("ch2通道已经关闭")
        }
    default:
        fmt.Println("default语句")
    }
    fmt.Println("main...over....")
}
复制代码
package main
​
import (
    "fmt"
    "time"
)
​
func main() {
    ch1 := make(chan int)
    ch2 := make(chan int)
​
    go func() {
        time.Sleep(3 * time.Second)
        ch1 <- 100
    }()
​
    select {
    case <-ch1:
        fmt.Println("case1可以执行....")
    case <-ch2:
        fmt.Println("case2可以执行")
    case <-time.After(3 * time.Second):
        fmt.Println("case3执行...timeout...")
        //default:
        //  fmt.Println("执行了default....")
    }
}

CSP模型

go语言的最大两个高点,一个是goroutine,一个是chan,二者合体的典型应用CSP,基本简化了并行程序的开发难度,为可认可的并行开发神器。

一、CSP是什么

CSP是Communicating Sequential Process的简称,中文可以叫做通信顺序进程,是一种并发编程模型,是一个很强大的并发数据模型,是上个世纪七十年代提出的,用于描述两个独立的并发实体通过 共享的通讯 channel (管道)进行通信的并发模型。相对于Actor模型,CSP中channel是第一类对象,它不关注发送消息的实体,而关注与发送消息时使用的channel。

严格来说, CSP是一门形式语言(类似于 λ calculus), 用于描述并发系统中的互动模式,也因此成为一众面向并发的编程语言的理论源头,并衍生出了Occam/Limbo/Golang...

而具体到编程语言,如Golang,其实只用到了CSP的很少一部分,即理论中的Process/Channel (对应到语言中的 goroutine/channel);这两个并发原语之间没有从属关系,Process可以订阅任意个Channel, Channel也并不关心是哪个Process在利用它进行通信;Process围绕Channel进行读写,形成一套有序阻塞和可预测的并发模型。

二、Golang CSP

与主流语言通过共享内存来进行并发控制方式不同,go语言采用了CSP模式。这是一种用于描述两个独立的并发实体通过共享的通讯Channel(管道)进行通信的并发模型。

Golang就是借用CSP模型的一些概念之实现并发进行理论支持,其实从实际上出发,go语言并没有,完全实现了CSP模型的所有理论,仅仅是借用了process和channel这两个概念。Process是在go语言上的表现就是goroutine是实际并必执行的实体,每个实体之间是通过channel通讯来实现数据共享。

Go语言的CSP模型是由协程Goroutine与通道Channel实现:

Go协程goroutine: 是一种轻量线程,它不是操作系统的线程,而是将一个操作系统线程分段使用,通过调度器实现协作式调度。是一种绿色线程,微线程,它与Coroutine协程也有区别,能够在发现堵塞后启动新的微线程。

通道channel: 类似Unix的Pipe, 用于协程之间通讯和同步。协程之间虽然解耦,但是它们和Channel有着耦合。

三、Channel

Goroutine和channel是Go语言并发编程的两大基石。Goroutine用于执行并发任务,channel用于goroutine之间的同步、通信。

反射机制

反射reflect: Go语言提供了一种机制在运行时更新变量和检查它们的值、调用它们的方法,但是在编译时并不知道这些变量的具体类型,这称为反射机制。

相关基础

interface是Go语言实现抽象的一个非常强大的工具。当向接口变量赋予一个实体类型的时候,接口会存储实体的类型信息,反射就是通过接口的类型信息实现的,反射建立在类型的基础上。

Go语言在reflect包里定义了各种类型,实现了反射的各种函数,通过它们可以在运行时检测类型的信息、改变类型的值。下表是Go语言相关的一些特性。

特点 说明
go语言是静态类型语言 编译时类型已经确定,比如对已基本数据类型的再定义后的类型,反射时候需要确认返回的是何种类型
空接口interface{} go的反射机制是要通过接口来进行的,而类似于java的Object的空接口可以和任何类型进行交互,因此对基本数据类型等的反射也直接利用了这一特点

Go语言的类型:

变量包括(type, value) 两部分

type包括static type 和 concrete type, 简单来说 static type 是你在编码是看见的类型(如int, string),concrete type是runtime系统看见的类型

类型断言能否成功,取决于变量的concrete type, 而不是static type.因此,一个reader变量如果它的concrete type 也实现了write 方法的话,它也可以被类型断言为writer.

Go语言的反射就是建立在类型之上的,Golang的指定类型的变量的类型是静态的(也就是指定int, string这些的变量, 它的 type 是static type), 在创建变量的时候就已经确定,反射主要与 Golang 的 interface 类型相关(它的 type 是concrete type), 只有interface类型才有反射之说。

在Golang的实现中,每个interface变量都有一个对应pair,pair中记录了实际变量的值和类型:

复制代码
(value, type)

value是实际变量值, type是实际变量的类型。一个interface{}类型的变量包含了2个指针, 一个指针指向值的类型【对应concrete type】, 另外一个指针指向实际的值【对应vale】。

例如, 创建类型为 * os.File的变量,然后将其赋给一个接口变量r:

复制代码
tty, err := os.OpenFile("/dev/tty", os.O_RDWR, 0)
var r io.Reader
r = try

接口变量r的pair中将记录如下信息:(tty, *os.File), 这个pair在接口变量的连续赋值过程中是不变的,将接口变量 r 赋给另一个接口变量 w:

复制代码
var w io.Writer
w = r.(io.Writer)

接口变量w的pair与r的pair相同,都是:(tty, *os.File), 即使w是空接口类型,pair也是不变的。

interface及其pair的存在,是Golang中实现反射的前提,理解了pair,就更容易理解反射。反射就是用来检测存储在接口变量内部(value; 类型concrete type)pair对的一种机制。

所以我们要理解两个基本概念 Type 和 Value, 它们也是Go语言包中 reflect 空间里最重要的两个类型。

第八章 反射的使用

一般用到的包是reflect包

一、reflect的基本功能TypeOf和ValueOf

既然反射就是用来检测存储在接口变量内部( 值value, 类型 concrete type) pair对的一种机制。在Golang的reflect 反射包中有什么样的方式可以直接获取到变量内部的信息呢? 它提供了两种类型(或者说两个方法) 可以很容易的访问接口变量内容,分别是reflect.ValueOf() 和 reflect.TypeOf(),

reflect.TypeOf() 是获取pair中的type, reflect.ValueOf()获取pair中的value

使用reflect一般分成三步:

首先需要把它转化成 reflect 对象(reflect.Type 或者 reflect.Value, 根据不同的情况调用不同的函数)

复制代码
t := reflect.TypeOf(i)      //得到类型的元数据,通过t我们能获取类型定义里面的所有元素
v := reflect.ValueOf(i)     //得到实际的值,通过v我们获取存储在里面的值,还可以去改变值

不建议使用反射:

1.与反射相关的代码,经常是难以阅读的。在软件工程中,代码可读性也是一个非常重要的指标。

2.Go语言作为一门静态语言,编码过程中,编译器能提前发现一些类型错误,但是对于反射代码是无能为力的。所以包含反射相关的代码,很可能会运行很久,才会出错,这时候经常是直接panic, 可能会造成严重的后果。

3.反射对性能影响还是比较大的,比正常代码运行速度慢一到两个数量级。所以,对于一个项目中处于运行效率关键位置的代码,尽量避免使用反射特性。

复制代码
package main
​
import (
    "fmt"
    "reflect"
)
​
func main() {
    // 反射操作: 通过反射,可以获取一个接口类型变量的 类型和数值
    var x float64 = 3.4
    fmt.Println("type: ", reflect.TypeOf(x))
    fmt.Println("value:", reflect.ValueOf(x))
​
    fmt.Println("----------------------------------")
    //根据反射的值,来获取对应的类型和数值
    v := reflect.ValueOf(x)
    fmt.Println("kind is float64:", v.Kind() == reflect.Float64)
    fmt.Println("type: ", v.Type())
    fmt.Println("value: ", v.Float())
}

go是静态类型语言,每个变量都拥有一个静态类型,这意味着每个变量的类型在编译时都是确定的:int, float32, *AutoType, []byte, chan []int 诸如此类

在反射的概念中,编译时就知道变量类型的是静态类型;运行时才知道一个变量类型的叫做动态类型。

静态类型 静态类型就是变量声明时的赋予的类型

复制代码
type MyInt int  //int 就是静态类型
​
type A struct {
    Name string   //string就是静态
}
​
var i *int    //*int就是静态类型

动态类型

动态类型: 运行时给这个变量赋值时,这个值的类型(如果值为nil的时候没有动态类型)。一个变量的动态类型在运行时可能改变,这主要依赖于它的赋值(前提是这个变量是接口类型)

复制代码
var A interface{}   //静态类型interface{}
A = 10              //静态类型为interface{} 动态为int
A = "String"        //静态类型为interface{} 动态为string
var M *int
A= M                //A的值可以改变

根据Go官方关于反射的博客,反射有三大定律

  1. Reflection goes from interface value to reflection object.

  2. Reflection goes from reflection object to interface value.

  3. To modify a reflection object, the value must be settable.

第一条是最基本的:反射可以从接口值得到反射对象。

反射是一种检测存储在interface中的类型和值机制。这可以通过TypeOf函数和ValueOf函数得到。

第二条实际上和第一条是相反的机制,反射可以从反射对象获得接口值。

它将ValueOf的返回值通过Interface()函数反向转变成interface变量

前两条就是说 接口变量 和 反射类型对象可以相互转化,反射类型对象实际上就是指的前面说的 reflect.Type 和 reflect.Value.

第三条不太好懂:如果需要操作一个反射变量,则其值必须可以修改。

反射变量可设置的本质是它存储了原变量本身,这样对反射变量的操作,就会反映到原变量本身,反之,如果反射变量不能代表原变量,那么操作了反射变量,不会对原变量产生任何影响,这会给使用者带来疑惑。所以第二种情况在语言层面是不被允许的。

二、反射的使用

1.从reflect.Value中获取接口interface的信息

当执行reflect.Value(interface)之后,就得到了一个类型为"reflect.Value"变量,可以通过它本身的interface()方法获得接口变量的真实内容,然后可以通过类型判断进行转换,转换为原有真实类型。有可能是已知原有类型,也有可能是未知原有类型,

已知原有类型

已知类型后转换为其对应的类型的做法如下,直接通过interface方法然后强制转换:

复制代码
realValue := value:Interface().(已知的类型)
复制代码
package main
​
import (
    "fmt"
    "reflect"
)
​
func main() {
    var num float64 = 1.23
    //"接口类型变量"  --> "反射类型变量"
    value := reflect.ValueOf(num)
​
    //"反射类型对象"  --> "接口类型变量"
    convertValue := value.Interface().(float64)
    fmt.Println(convertValue)
    
    /*
        反射类型对象--->接口类型变量,理解为"强制转换"
        golang对类型要求非常严格,类型一定要完全符合
        一个是*float64, 一个float64,如果弄混,直接panic
     */
    pointer := reflect.ValueOf(&num)
    converPointer := pointer.Interface().(float64)
    fmt.Println(converPointer)    //panic: interface conversion: interface {} is *float64, not float64
    converPointer1 := pointer.Interface().(*float64)
    fmt.Println(converPointer1)
}

未知原有类型

很多情况下,可能并不知道其具体类型,那么这个时候,需要进行遍历探测其Filed来得知

复制代码
package main
​
import (
    "fmt"
    "reflect"
)
​
type Person struct {
    Name string
    Age  int
    Sex  string
}
​
func (p Person) Say(msg string) {
    fmt.Println("hello, ", msg)
}
​
func (p Person) PrintInfo() {
    fmt.Println("姓名: %s, 年龄: %d, 性别: %s\n", p.Name, p.Age, p.Sex)
}
​
// 获取input的信息
func GetMessage(input interface{}) {
    getType := reflect.ValueOf(input)                   //先获取input的类型
    fmt.Println("get Type is: ", getType.Type().Name()) //Person
    fmt.Println("get Kind is: ", getType.Kind())        //struct
​
    getValue := reflect.ValueOf(input)
    fmt.Println("get all Fields is: ", getValue)
​
    //获取字段
    /*
        step1: 先获取Type对象:reflect.Type
            NumField()
            Field(index)
        step2: 通过Filed()获取每一个Filed字段
        step3: Interface(), 得到对应的Value
    */
    typeInfo := getType.Type()
    for i := 0; i < typeInfo.NumField(); i++ {
        field := typeInfo.Field(i)
        value := getType.Field(i).Interface() //获取第一个数值
        fmt.Printf("字段名称 %s, 字段类型: %s, 字段数值:%v\n", field.Name, field.Type, value)
    }
​
    //获取方法
    for i := 0; i < typeInfo.NumMethod(); i++ {
        method := typeInfo.Method(i)
        fmt.Printf("方法名称:%s, 方法数值: %v\n", method.Name, method.Type)
    }
​
    // 假设 getValue 是 reflect.Value 类型
    if getValue.Kind() == reflect.Ptr {
        getValue = getValue.Elem() // 处理指针类型
    }
​
    //deepseek写的代码
    //getType1 := getValue.Type()
    //
     遍历字段
    //for i := 0; i < getType1.NumField(); i++ {
    //  field := getType1.Field(i) // 修正拼写错误
    //  valueField := getValue.Field(i)
    //  var value interface{}
    //  if valueField.CanInterface() {
    //      value = valueField.Interface()
    //  } else {
    //      value = "不可导出字段"
    //  }
    //  fmt.Printf("字段名称: %s, 字段类型: %s, 字段数值: %v\n", field.Name, field.Type, value)
    //}
    //
     遍历方法(注意:方法必须是导出的)
    //for i := 0; i < getType1.NumMethod(); i++ {
    //  method := getType1.Method(i)
    //  fmt.Printf("方法名称: %s, 方法类型: %v\n", method.Name, method.Type)
    //}
​
}
func main() {
    p1 := Person{"王麻子", 18, "女"}
    GetMessage(p1)
}
复制代码
package main
​
import (
    "fmt"
    "reflect"
)
​
type Person struct {
    Name string
    Age  int
    Sex  string
}
​
func (p Person) Say(msg string) {
    fmt.Println("hello, ", msg)
}
​
func (p Person) PrintInfo() {
    fmt.Println("姓名: %s, 年龄: %d, 性别: %s\n", p.Name, p.Age, p.Sex)
}
​
// 获取input的信息
func GetMessage(input interface{}) {
    getType := reflect.ValueOf(input)                   //先获取input的类型
    fmt.Println("get Type is: ", getType.Type().Name()) //Person
    fmt.Println("get Kind is: ", getType.Kind())        //struct
​
    getValue := reflect.ValueOf(input)
    fmt.Println("get all Fields is: ", getValue)
​
    //获取字段
    /*
        step1: 先获取Type对象:reflect.Type
            NumField()
            Field(index)
        step2: 通过Filed()获取每一个Filed字段
        step3: Interface(), 得到对应的Value
    */
    typeInfo := getType.Type()
    for i := 0; i < typeInfo.NumField(); i++ {
        field := typeInfo.Field(i)
        value := getType.Field(i).Interface() //获取第一个数值
        fmt.Printf("字段名称 %s, 字段类型: %s, 字段数值:%v\n", field.Name, field.Type, value)
    }
​
    //获取方法
    for i := 0; i < typeInfo.NumMethod(); i++ {
        method := typeInfo.Method(i)
        fmt.Printf("方法名称:%s, 方法数值: %v\n", method.Name, method.Type)
    }
​
    // 假设 getValue 是 reflect.Value 类型
    if getValue.Kind() == reflect.Ptr {
        getValue = getValue.Elem() // 处理指针类型
    }
​
    //deepseek写的代码
    //getType1 := getValue.Type()
    //
     遍历字段
    //for i := 0; i < getType1.NumField(); i++ {
    //  field := getType1.Field(i) // 修正拼写错误
    //  valueField := getValue.Field(i)
    //  var value interface{}
    //  if valueField.CanInterface() {
    //      value = valueField.Interface()
    //  } else {
    //      value = "不可导出字段"
    //  }
    //  fmt.Printf("字段名称: %s, 字段类型: %s, 字段数值: %v\n", field.Name, field.Type, value)
    //}
    //
     遍历方法(注意:方法必须是导出的)
    //for i := 0; i < getType1.NumMethod(); i++ {
    //  method := getType1.Method(i)
    //  fmt.Printf("方法名称: %s, 方法类型: %v\n", method.Name, method.Type)
    //}
​
}
func main() {
    p1 := Person{"王麻子", 18, "女"}
    GetMessage(p1)
}

说明:

通过运行结果可以得知获取未知类型的interface的具体变量及其类型的步骤为:

1.先获取interface的reflect.Type, 然后通过NumField进行遍历

2.再通过reflect.Type的Field获取其Field

3.最后通过Field的Interface()得到对应的value

通过运行结果可以得知获取未知类型的interface的所属方法(函数)的步骤为:

1.先获取interface的reflect.Type.然后通过NumMethod进行遍历

2.再分别通过reflect.Type的Method获取对应的真实的方法(函数)

3.最后对结果取其Name和Type得知具体的方法名

4.也就是说反射可以将"反射类型对象"再重新转换为"接口类型变量"

5.struct或者struct的嵌套都是一样的判断处理方式。

Kind有slice,map, pointer指针,struct, interface, string, Array, Function, int或其他基本类型组成。

用Kind和type之前要做好区分 。如果定义一个type Person struct{} , 那么Kind就是struct, Type就是Person.

反射变量对应的Kind方法的返回值是基类型,并不是静态类型。

复制代码
type myint int
var x myint =100
v := reflect.ValueOf(x)

变量v的Kind依旧是reflect.int,而不是myint这个静态类型。Type可以表示静态类型,而kind不可以。

通过reflect.Value设置实际变量的值

reflect.Value是通过reflect.ValueOf(x)获得的,只有当X是指针的时候,才可以通过reflect.Value修改实际变量的X的值,即:要修改反射类型的对象就一定要保证其值是"add ressable"的。

也就是说:要想修改一个变量的值,那么必须通过该变量的指针地址,取消指针的引用。通过refPtrVal := reflect.ValueOf(&var)的方式获取指针类型,你使用refPtrVal.elem().set (一个新的reflect.Value)来进行更改,传递给set()的值也必须是一个reflect.value.

这里需要一个方法:

解释起来就是: Elem返回接口v包含的值或指针V指向的值。如果v的类型不是interface或ptr,它会恐慌。如果v为零,则返回零值。

如果变量是一个指针 、map、slice、channel、Array. 那么你可以使用reflect.Typeof(v).Elem()来确定包含的类型。

复制代码
package main
​
import (
    "fmt"
    "reflect"
)
​
func main() {
    var num float64 = 1.23
​
    //需要操作指针
    //通过reflect.ValueOf() 获取num的Value对象
    pointer := reflect.ValueOf(&num) //注意参数必须是指针才能修改值
    newValue := pointer.Elem()
​
    fmt.Println("类型: ", newValue)
    fmt.Println("是否可以修改数据:", newValue.CanSet())
​
    //重新赋值
    newValue.SetFloat(3.14)
    fmt.Println(num)
    //如果reflect.ValueOf的参数不是指针
    value := reflect.ValueOf(num)
    value.Elem() //如果非指针  panic: reflect: call of reflect.Value.Elem on float64 Value
}

说明:

1.需要传入的参数是 * float64这个指针,然后可以通过 pointer.Elem()去获取所指向的Value,注意一定要是指针.

2.如果传入的参数不是指针,而是变量,那么

通过 Elem 获取原始值对应的对象则直接panic

通过CanSet方法查询是否可以设置返回false

3.newValue.CanSet()表示是否可以重新设置其值,如果输出的是true则可修改,否则不能修改,修改完之后再进行打印发现真的已经修改了。

4.reflect.Value.Elem() 表示获取原始值对应的反射对象,只有原始对象才能修改,当前反射对象是不能修改的

5.也就是说如果要修改反射类型对象,其值必须是"addressable" 【对应的要传入的是指针,同时要通过Elem方法获取原始值对应的反射对象】

6.struct 或者 struct 的嵌套都是一样的判断处理方式

复制代码
package main
​
import (
    "fmt"
    "reflect"
)
​
type Student struct {
    Name   string
    Age    int
    School string
}
​
func main() {
    s1 := Student{"万年老妖", 20000, "怪兽学校"}
    //通过反射,更改对象的数值,前提也是数据可以被更改
    fmt.Printf("%T\n", s1)
    p1 := &s1
    fmt.Printf("%T\n", p1)
    fmt.Println(s1.Name)
    fmt.Println((*p1).Name, p1.Name)
​
    //改变数值
    value := reflect.ValueOf(&s1)
    if value.Kind() == reflect.Ptr {
        newValue := value.Elem()
        fmt.Println(newValue.CanSet())
​
        f1 := newValue.FieldByName("Name")
        f1.SetString("仙人板板")
        f3 := newValue.FieldByName("School")
        f3.SetString("天宫")
        fmt.Println(s1)
    }
}

reflect对象进行方法的调用

通过reflect.Value来进行方法的调用

通过reflect来扩展让用户能够自定义方法

Call()方法

通过反射,调用方法

复制代码
package main
​
import (
    "fmt"
    "reflect"
)
​
type Person struct {
    Name string
    Age  int
    Sex  string
}
​
func (p Person) Say(msg string) {
    fmt.Println("hello, ", msg)
}
​
func (p Person) PrintInfo() {
    fmt.Println("姓名: %s, 年龄: %d, 性别: %s\n", p.Name, p.Age, p.Sex)
}
​
func (p Person) Test(i, j int, s string) {
    fmt.Println(i, j, s)
}
​
func main() {
    /*
          通过反射来进行方法的调用
          思路:
            step1: 接口变量--->对象反射对象: Value
            step2: 获取对应的方法对象: MethodByName()
            step3: 将方法对象进行调用: Call()
    */
    p1 := Person{"招桃花", 300, "怪"}
    value := reflect.ValueOf(p1)
    fmt.Printf("kind: %s, type: %s\n", value.Kind(), value.Type()) //kind: struct, type: main.Person
​
    methodValue1 := value.MethodByName("PrintInfo")
    fmt.Printf("kind:%s, type:%s\n", methodValue1.Kind(), methodValue1.Type()) //kind:func, type:func()
​
    //没有参数,进行调用
    methodValue1.Call(nil) //没胡参数,直接写nil
​
    args1 := make([]reflect.Value, 0) //或者创建一个空的切片也可以
    methodValue1.Call(args1)
​
    methodValue2 := value.MethodByName("Say")
    fmt.Printf("kind:%s, type:%s\n", methodValue2.Kind(), methodValue2.Type()) //kind:func, type:func(string)
​
    args2 := []reflect.Value{reflect.ValueOf("反射机制")} //hello,  反射机制
    methodValue2.Call(args2)
​
    methodValue3 := value.MethodByName("Test") //kind:func, type:func(int, int, string)
    fmt.Printf("kind:%s, type:%s\n", methodValue3.Kind(), methodValue3.Type())
    args3 := []reflect.Value{reflect.ValueOf(100), reflect.ValueOf(100), reflect.ValueOf("hello world-famous")} //100 100 hello world-famous
    methodValue3.Call(args3)
}

通过反射,调用函数

函数像普通的变量一样。先通过ValueOf()来获取函数的反射对象,可以判断它的Kind, 是一个func,那么就可心执行Call()进行函数的调用。

复制代码
package main
​
import (
    "fmt"
    "reflect"
    "strconv"
)
​
func main() {
    //函数反射
    /*
        思路:函数也是看做接口变量类型
        step1:函数 ---> 反射对象, Value
        step2: kind ---> func
        step3: call()
    */
    f1 := fun1
    value := reflect.ValueOf(f1)
    fmt.Printf("kind: %s, type: %s\n", value.Kind(), value.Type()) //kind: func, type: func()
    value2 := reflect.ValueOf(fun2)
    fmt.Printf("kind: %s, type: %s\n", value2.Kind(), value2.Type()) //kind: func, type: func(int, string)
​
    value3 := reflect.ValueOf(fun3)
    fmt.Printf("kind: %s, type: %s\n", value3.Kind(), value3.Type()) //kind: func, type: func(int, string) string
​
    //通过反射调用函数
    value.Call(nil)
    value2.Call([]reflect.Value{reflect.ValueOf(1000), reflect.ValueOf("老臭虫")})
    resultValue := value3.Call([]reflect.Value{reflect.ValueOf(2000), reflect.ValueOf("家里猫")})
    fmt.Println("%T\n", resultValue)
​
    fmt.Println(len(resultValue))
    fmt.Println("kind: %s, type:%s\n", resultValue[0].Kind(), resultValue[0].Type())
    s := resultValue[0].Interface().(string)
    fmt.Println(s)
    fmt.Printf("%T\n", s)
}
​
func fun1() {
    fmt.Println("函数fun1(), 无参的.......")
}
​
func fun2(i int, s string) {
    fmt.Println("函数fun2(), 有参的.......")
}
​
func fun3(i int, s string) string {
    fmt.Println("函数fun3(), 有参的, 也有返回值", i, s)
    return s + strconv.Itoa(i)
}

说明:

1.要通过反射来调用起对应的方法,必须要先通过reflect.ValueOf(interface)来获取到reflect.Value, 得到"反射类型对象"后才能做下一步处理

2.reflect.Value.MethodByName这个MethodByName, 需要指定准确真实的方法名字,如果错误将直接panic, MethodByName返回一个函数值对应的reflect.Value方法的名字。

3.[]reflect.Value,这个是最终需要调用的方法的参数,可以没有或者一个或者多个,根据实际参数来定。

4.reflect.Value的Call这个方法,这个方法将最终调用真实的方法,参数务必保持一致,如果reflect.Value.Kind不是一个方法,那么将直接panic

5.本来可以用对象访问直接调用的,但是如果要通过反射,那么首先要将方法注册,也就是MethodByName, 然后通过反射用methodValue.Call

相关推荐
SimonKing9 分钟前
无需重启!动态修改日志级别的神技,运维开发都哭了
java·后端·程序员
架构精进之路28 分钟前
多智能体系统不是银弹
后端·架构·aigc
red_redemption1 小时前
自由学习记录(87)
学习
涡能增压发动积1 小时前
MySQL数据库为何逐渐黯淡,PostgreSQL为何能新王登基
人工智能·后端
架构精进之路1 小时前
多智能体系统架构解析
后端·架构·ai编程
Java中文社群1 小时前
重磅!Ollama发布UI界面,告别命令窗口!
java·人工智能·后端
程序员清风2 小时前
程序员代码有Bug别怕,人生亦是如此!
java·后端·面试
咸甜适中2 小时前
rust语言 (1.88) egui (0.32.1) 学习笔记(逐行注释)(十五)网格布局
笔记·学习·rust·egui
就是帅我不改2 小时前
告别996!高可用低耦合架构揭秘:SpringBoot + RabbitMQ 让订单系统不再崩
java·后端·面试
用户6120414922132 小时前
C语言做的区块链模拟系统(极简版)
c语言·后端·敏捷开发