目錄
第一章 go語言中包的使用
一.main包
二.package
三.import
四.goPath環境變量
五.init包初始化
六.管理外部包
第二章 time包
第三章 File文件操作
一.FileInfo接口
二.權限
三.打開模式
四.File操作
五.讀文件
參考1:Golang 中的 bufio 包詳解(四):bufio.Scanner_golang scanner-CSDN博客
參考2:Go 語言讀文件的九種方法 - 王一白 - 博客園
參考3:Go語言讀取文件_go 讀取文件-CSDN博客
第四章 I/O操作
一、IO包
二、文件復制
三、斷點續傳
第五章 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包中的通道相關函數
第八章 反射的使用
一、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關鍵字,可以單個導入或者批量導入,
//單個導入
import "package"
?
//批量導入
import ("package1""package2"
)
B: 點操作
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 stringAge intAdd 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/sqldb, 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天, day1天=24小時, hour1小時=60分鐘, minute1分鐘=60秒, second1秒=1000毫秒, millisecond1毫秒=1000微秒, microsecond -->μs1微秒=1000納秒, nanosecond --> ns1納秒=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) + 1fmt.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.txtSize() int64 ? ? //文件大小,字節數12540Mode() FileMode ? //文件權限 -rw-rw-rw-ModTile() time.Time ? //修改時間 2018-04-13 16:30:53 +0800 CSTIsDir() bool ? ? ? //是否文件夾Sys() interface{} ? //基礎數據源接口 (can return nil) }
package main ? import ("fmt""os" ) ? func main() {/*FileInfo: 文件信息interfaceName(), 文件名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.路徑相對路徑:relativeab.txt相對于當前工程絕對路徑: absoluteC:\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)) //truefmt.Println(filepath.IsAbs(fileName2)) //falsefmt.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 = -1for {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 := -1total := 0for {n, err = file1.Read(bs)if err == io.EOF || n == 0 {fmt.Println("拷貝完畢...")break} else if err != nil {fmt.Println("報錯了...")return total, err}total += nfile2.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 fileSeekCurrent = 1 // seek relative to the current offsetSeekEnd ? ? = 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) //SeekStartfile.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.先讀取臨時文件中的數量,再seekfile3.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) errorfunc(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的規則
-
當新的Goroutine開始時,Goroutine調用立即返回.與函數不同,go不等待Goroutine執行結束.當Goroutine調用,并且Goroutine的任何返回值被忽略之后,go立即執行到下一行代碼.
-
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 ?//goschedgo func() {for i := 0; i < 5; i++ {fmt.Println("goroutine....")}}() ?for i := 0; i < 4; i++ {//讓出時間片,先讓別的goroutine時間片runtime.Gosched()fmt.Println("main....")} ?//創建goroutinego func() {fmt.Println("goroutine開始...")//調用funfun()fmt.Println("goroutine結束...")}() ?time.Sleep(3 * time.Second) } func fun() {defer fmt.Println("derfer....")//return //終止函數runtime.Goexit() ? //結束當前的goroutinefmt.Println("fun函數....") }
四、臨界資源安全問題
1、臨界資源
臨界資源:指并發環境中多個進程/線程/協程共享的資源.
但在并發編程中對臨界資源的處理不當,往往會導致數據不一致的問題
package main ? import ("fmt""time" ) ? func main() {/*臨界資源:*/a := 1go func() {a = 2fmt.Println("goruntine...", a)}()a = 3time.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 boolch1 = make(chan bool) ?go func() {for i := 0; i < 10; i++ {fmt.Println("子goroutine中,i: ", i)}//偱環結束后, 向通道中寫數據,表示要結束了...ch1 <- truefmt.Println("結束...")}() ?data := <-ch1fmt.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 <- 10time.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 := <-ch1if !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<-ch1fmt.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) //緩沖通道,緩沖區大小是5ch2 <- 100fmt.Println(len(ch2), cap(ch2))ch2 <- 200ch2 <- 300ch2 <- 400ch2 <- 500fmt.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 := <-ch3if !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() {/*雙向:chanchan <- 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"<-donefmt.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 := <-ch1fmt.Println("fun1函數中寫出的數據是:", data)go fun2(ch1)ch1 <- 200fmt.Println("main...over....") } ? // 該函數,只能操作只寫的通道 func fun1(ch chan<- int) {//在函數內部,對于ch1通道,只能寫數據,不能讀取數據ch <- 100fmt.Println("fun1函數結束") } ? // 該函數,只能操作只讀的通道 func fun2(ch <-chan int) {data := <-chfmt.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.Timerfmt.Println(time.Now()) ? //2025-03-13 15:19:12.7081165 +0800 CST m=+0.011347601 ?//此處等待channel中的數值,會阻塞3秒ch2 := timer.Cfmt.Println(<-ch2) ?//新建一個計時器timer2 := time.NewTimer(5 * time.Second)//開始goroutine,來處理觸發后的事件go func() {<-timer2.Cfmt.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 := <-chfmt.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, selectselect語句類型于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.4fmt.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官方關于反射的博客,反射有三大定律
Reflection goes from interface value to reflection object.
Reflection goes from reflection object to interface value.
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 float64converPointer1 := pointer.Interface().(*float64)fmt.Println(converPointer1) }
未知原有類型
很多情況下,可能并不知道其具體類型,那么這個時候,需要進行遍歷探測其Filed來得知
package main ? import ("fmt""reflect" ) ? type Person struct {Name stringAge intSex 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()) //Personfmt.Println("get Kind is: ", getType.Kind()) ? ? ? //struct ?getValue := reflect.ValueOf(input)fmt.Println("get all Fields is: ", getValue) ?//獲取字段/*step1: 先獲取Type對象:reflect.TypeNumField()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 stringAge intSex 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()) //Personfmt.Println("get Kind is: ", getType.Kind()) ? ? ? //struct ?getValue := reflect.ValueOf(input)fmt.Println("get all Fields is: ", getValue) ?//獲取字段/*step1: 先獲取Type對象:reflect.TypeNumField()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 ? stringAge ? intSchool string } ? func main() {s1 := Student{"萬年老妖", 20000, "怪獸學校"}//通過反射,更改對象的數值,前提也是數據可以被更改fmt.Printf("%T\n", s1)p1 := &s1fmt.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 stringAge intSex 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: 接口變量--->對象反射對象: Valuestep2: 獲取對應的方法對象: 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-famousmethodValue3.Call(args3) }
通過反射,調用函數
函數像普通的變量一樣。先通過ValueOf()來獲取函數的反射對象,可以判斷它的Kind, 是一個func,那么就可心執行Call()進行函數的調用。
package main ? import ("fmt""reflect""strconv" ) ? func main() {//函數反射/*思路:函數也是看做接口變量類型step1:函數 ---> 反射對象, Valuestep2: kind ---> funcstep3: call()*/f1 := fun1value := 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