第61篇 笔记-Go 基础
目錄
一、go原則
二、組織代碼項目
三、數據類型
四、fmt 格式
五、package 包
5.1 可見性與作用域
5.2 包的導入
六、函數
6.1 main()?函數
6.2 init() 初始化函數
6.3 自定義函數
6.4 更改函數參數值
七、控制語句
7.1 if 語句
7.2 switch 語句
7.3 select 語句
7.4 break?語句
7.5?continue 語句
7.6?goto 語句
7.7 defer 函數
7.8?panic 函數與 recover 函數
7.9 示例:用戶登錄(流程控制)
7.10 示例:錯誤處理(控制流中斷)
八、數組
8.1 聲明與初始化
8.2 賦值與訪問
8.3 range遍歷
九、切片
9.1 切片內部結構
9.2 切片項
9.3 append() 和 copy() 函數
9.4 容量cap變化規律
9.5 示例:斐波納契數列
十、映射
10.1 定義 Map
10.2 添加與刪除
10.3 判斷存在
十一、結構體
11.1?聲明和初始化
11.2?結構嵌入
11.3 JSON 序列化
十二、方法
12.1 聲明方法
12.2 方法的重載
12.3 方法的封裝
12.4 函數與方法的區別
十三、接口
13.1 示例:interface聲明與使用方法
13.2 示例:創建用于管理在線商店的程序包
13.3 示例:通過實現接口對結構體切片進行排序
十四、類型斷言
十五、隨機數
15.1 偽隨機示例:math/rand
15.2 真隨機示例:crypto/rand
十六、日期與時間
16.1 type Time
16.2 示例:日期與時間
16.3 type Duration
十七、cmd編譯
17.1 兩種編譯方法
17.2 go build命令
17.3 go build示例
17.4 go install命令
17.5 go install示例
十八、錯誤處理策略
18.1 示例
18.2 處理策略:
18.3 用于錯誤處理的推薦做法
十九、日志記錄
19.1 log包
19.2 記錄到文件
二十、反射
二十一、nil
一、go原則
(1)Go 致力于使事情變得簡單,用更少的代碼行執行更多操作。
(2)并發是首選,函數可作為輕量線程運行。
(3)編譯和執行速度快,目標是與 C 一樣快。
(4)Go 要求強制轉換是顯式的,否則會引發編譯錯誤。
(5)未使用的代碼不是警告,而是錯誤,代碼將不會編譯。
(6)有一種官方格式設置,有助于保持項目之間的一致性。
(7)Go 并不適用于框架,因為它更傾向于使用標準庫。
(8)Go 確保向后兼容性。
(9)Go 許可證是完全開放源代碼。
?
二、組織代碼項目
Go 在組織項目文件方面與其他編程語言不同。 首先,Go 在工作區的概念之下工作,其中,工作區就是應用程序源代碼所在的位置。 在 Go 中,所有項目共享同一個工作區。 不過,從版本 1.11 開始,Go 開始更改此方法。? 現在,Go 工作區位于 $HOME/go,但如果需要,可以為所有項目設置其他位置。
若要定義其他工作區位置,請將值設置為 $GOPATH 環境變量。 開始創建更復雜的項目時,需要為環境變量設置一個值,以避免將來出現問題。
在 macOS 或 Linux 中,可以:
// 通過將以下命令添加到?~/.profile?來配置工作區: export GOPATH=$HOME/go// 然后運行以下命令以更新環境變量: source ~/.profile在Windows 中,創建一個文件夾(例如?C:\Projects\Go),你將在其中創建所有 Go 項目。 打開 PowerShell 提示符,然后運行以下命令:
[Environment]::SetEnvironmentVariable("GOPATH", "C:\Projects\Go", "User")在 Go 中,可以通過打印 $GOPATH 環境變量的值來獲取工作區位置,以供將來參考。 或者,可以通過運行以下命令獲取與 Go 相關的環境變量:
go env在 Go 工作區中,可以找到以下文件夾:
- bin:包含應用程序中的可執行文件。
- src:包括位于工作站中的所有應用程序源代碼。
- pkg:包含可用庫的已編譯版本。 編譯器可以鏈接這些庫,而無需重新編譯它們。
?
三、數據類型
Go 有四類數據類型:
值類型與引用類型用法的區別:
默認值:
- int?類型的:?0(及其所有子類型,如?int64)
- float32?和?float64?類型的:?+0.000000e+000
- bool?類型的:?false
- string?類型的:空值
?
四、fmt 格式
通用:%v 值的默認格式表示%+v 類似%v,但輸出結構體時會添加字段名%#v 值的Go語法表示%T 值的類型的Go語法表示%% 百分號布爾值:%t 單詞true或false整數:%b 表示為二進制%c 該值對應的unicode碼值%d 表示為十進制%o 表示為八進制%q 該值對應的單引號括起來的go語法字符字面值,必要時會采用安全的轉義表示%x 表示為十六進制,使用a-f%X 表示為十六進制,使用A-F%U 表示為Unicode格式:U+1234,等價于"U+%04X"浮點數與復數的兩個組分:%b 無小數部分、二進制指數的科學計數法,如-123456p-78;參見strconv.FormatFloat%e 科學計數法,如-1234.456e+78%E 科學計數法,如-1234.456E+78%f 有小數部分但無指數部分,如123.456%F 等價于%f%g 根據實際情況采用%e或%f格式(以獲得更簡潔、準確的輸出)%G 根據實際情況采用%E或%F格式(以獲得更簡潔、準確的輸出)字符串和[]byte%s 直接輸出字符串或者[]byte%q 該值對應的雙引號括起來的go語法字符串字面值,必要時會采用安全的轉義表示%x 每個字節用兩字符十六進制數表示(使用a-f)%X 每個字節用兩字符十六進制數表示(使用A-F) 指針:%p 表示為十六進制,并加上前導的0x 沒有%u。整數如果是無符號類型自然輸出也是無符號的。類似的,也沒有必要指定操作數的尺寸(int8,int64)。寬度與精度:%f: 默認寬度,默認精度%9f 寬度9,默認精度%.2f 默認寬度,精度2%9.2f 寬度9,精度2%9.f 寬度9,精度0?
五、package 包
(1)包(package)是多個Go源碼的集合,go語言有很多內置包,比如fmt,os,io等;
(2)一個文件夾下的所有源碼文件只能屬于同一個包,同樣屬于同一個包的源碼文件不能放在多個文件夾下;
(3)包名一般是小寫的,使用一個簡短且有意義的名稱;
(4)main包是一個可執行的包,是應用程序的入口包,編譯完會生成一個可執行文件;
(5)編譯不包含 main 包的源碼文件時不會得到可執行文件;
(6)包名一般要和所在的目錄同名,也可以不同,包名中不能包含 “-”等特殊符號;
(7)包一般使用域名作為目錄名稱,這樣能保證包名的唯一性;
(8)GitHub 項目的包一般會放到?GOPATH/src/github.com/userName/projectName?目錄下;
(9)任何源代碼文件必須屬于某個包,同時源碼文件的第一行有效代碼必須是?package?語句,通過該語句聲明自己所在的包。
?
5.1 可見性與作用域
變量作用域的好處:
如果想在一個包中引用另外一個包里的標識符(如變量、常量、類型、函數等)時,該標識符必須是對外可見的(public)。
在Go語言中只需要將標識符的首字母大寫就可以。
// 首字母小寫,外部包不可見,只能在當前包內使用 var num = 10//首字母大寫外部包可見,可在其他包中使用 const Name ?= "ares"// 首字母小寫,外部包不可見,只能在當前包內使用 type person struct {name string }type Student struct {Name ?string ? ? ? ? ? //可在包外訪問的方法class string ? ? ? ? ? //僅限包內訪問的字段 }type Payer interface {init() ? ? ? ? ? ? ? ? //僅限包內訪問的方法Pay() ? ? ? ? ? ? ? ? ?//可在包外訪問的方法 }// 首字母大寫,外部包可見,可在其他包中使用 func Add(x, y int) int {return x + y }func age() { ? ? ? ? ? ? ? // 首字母小寫,外部包不可見,只能在當前包內使用var Age = 18 ? ? ? ? ? // 函數局部變量,外部包不可見,只能在當前函數內使用fmt.Println(Age) }5.2 包的導入
(1)使用import關鍵字;
(2)import導入語句通常放在文件開頭包聲明語句的下面;
(3)導入的包名需要使用雙引號包裹起來;
(4)當你導入多個包時,導入的順序會按照字母排序;
(5)導入包即等同于包含了這個包的所有的代碼對象;
(6)導入的包必須要被使用,否則程序編譯時也會報錯。
導入格式:
Go包的引入格式常見的有四種,下面以引用"fmt"包為例說明:
import "fmt" ? ? ? ? ? ? // 標準引用 import fmt_go "fmt" ? ? ?// 別名引用,fmt_go是引用fmt包的別名,此時使用fmt包的功能時需要用fmt_go.方法來使用 import . "fmt" ? ? ? ? ? // 省略方式,這種引用相當于把包fmt的命名空間合并到當前程序的命名空間了,因此可以直接引用,不用在加上前綴fmt. import _ "fmt" ? ? ? ? ? // 匿名引用,僅執行包的初始化函數,不使用包內數據導入路徑:
import導入時,會從GO的安裝目錄(也就是GOROOT環境變量設置的目錄)和GOPATH環境變量設置的目錄中,檢索 src/package 來導入包。如果不存在,則導入失敗。
- GOROOT,就是GO內置的包所在的位置;
- GOPATH,就是我們自己定義的包的位置。
關于我們自己定義的包的導入路徑有多種說法,本文進行了實際測試;
假設文件結構:
D:\golang\.|└──src├──add│ ? └───add.go└──main└───main.go└───sub└───sub.go包的引用策略:
// 第一種情況 // 如果設置了 GOPATH = D:\golang // 包名默認是從 $GOPATH/src/ 后開始計算的 import ( ? ?"add""main/sub" )// 第二種情況 // 如果沒有設置 GOPATH = D:\golang // 只能使用相對路徑 import ( ? ?"../add" ? ? ? ? ? ? ? //上級目錄"./sub" ? ? ? ? ? ? ? ?//本級目錄往下 )特別注意:本文測試中,兩種方法與?GOPATH 的設置相對應;即:
- 如果源碼在 GOPATH 目錄下,包的引用只能使用第一種方法,不能使用相對路徑(不知道這個是什么邏輯);
- 如果源碼不在 GOPATH 目錄下,只能使用相對路徑,而不能使用第一種方法;
?
六、函數
6.1 main()?函數
與之交互的函數是?main()?函數。 Go 中的所有可執行程序都具有此函數,因為它是程序的起點。
你的程序中只能有一個?main()?函數。 如果創建的是 Go 包,則無需編寫?main()?函數。?
main()?函數沒有任何參數,并且不返回任何內容。 但這并不意味著其不能從用戶讀取值,如命令行參數。
6.2 init() 初始化函數
在Go語言程序執行時導入包語句會自動觸發包內部init()函數的調用。需要注意的是: init()函數沒有參數也沒有返回值。
import "fmt"var x = 100func init() {fmt.Println(x) //100 } func main() {fmt.Println("Hello!") //Hello! }包中init函數的執行時機:
? ? ? ? 全局聲明 ===> init() ====> main()
init()函數執行順序:
init()?函數與 main() 函數對比:
(1)都是go語言中的保留函數。init()用于初始化信息,main()用于座位程序入口;
(2)兩個函數定義的時候,不能有參數和返回值,只能由go程序自動調用,不能被引用;
(3)init()函數可以定義在任意包中,可以有多個。main()函數只能在main包下,并且只能有一個;
(4)存在依賴的包之間不能循環導入;
(5)一個包可以被其他多個包import,但是只能被初始化一次。
執行順序:
- 先執行init()函數,后執行main()函數
- 對于同一個go文件中,調用順序是從上向下的,也就是先寫的先被執行,后寫的后被執行
- 對于同一個包下,將文件名稱按照字符串進行排序,之后順序調用哥哥文件中的init()函數
- 不同包下,如果不存在依賴,按照main包中的import順序來調用對應包中的init()函數;如果存在依賴,最后被依賴 的最先被初始化,導入順序:main-->A-->B-->C,執行順序,C-->B-->A--main
6.3 自定義函數
下面是用于創建函數的語法:
func name(parameters) (results) {body-content }示例:
func main() {sum, _ := calc(os.Args[1], os.Args[2]) // 放棄calc函數的第 2 個返回值println("Sum:", sum) }6.4 更改函數參數值
將值傳遞給函數時,該函數中的每個更改都不會影響調用方。
Go 是“按值傳遞”編程語言。 這意味著每次向函數傳遞值時,Go 都會使用該值并創建本地副本(內存中的新變量)。
在函數中對該變量所做的更改都不會影響你向函數發送的更改。
指針:
如果你希望在?自定義?函數中進行的更改會影響?main?函數中的變量,則需要使用指針。?
指針是包含另一個變量的內存地址的變量。 當你發送指向某個函數的指針時,不會傳遞值,而是傳遞地址內存。 因此,對該變量所做的每個更改都會影響調用方。
在 Go 中,有兩個運算符可用于處理指針:
- &?運算符使用其后對象的地址。
- *?運算符取消引用指針。 也就是說,你可以前往指針中包含的地址訪問其中的對象。
示例:
package mainfunc main() {firstName := "John"updateName(&firstName)println(firstName) }func updateName(name *string) {*name = "David" }運行前面的代碼。 請注意,輸出現在顯示的是?David,而不是?John。
首先要做的就是修改函數的參數,以指明你要接收指針。 為此,請將參數類型從?string?更改為?*string。(后者仍是字符串,但現在它是指向字符串的指針。)然后,將新值分配給該變量時,需要在該變量的左側添加星號 (*) 以暫停該變量的值。 調用?updateName?函數時,系統不會發送值,而是發送變量的內存地址。 這就是前面的代碼在變量左側帶有?&?符號的原因。
?
七、控制語句
7.1 if 語句
(1)不需使用括號將條件包含起來;
(2)大括號{}必須存在,即使只有一行語句;
(3)左括號必須在if或else的同一行;
(4)在if之后,條件語句之前,可以添加變量初始化語句,使用";"進行分隔;
(5)在有返回值的函數中,最終的return不能在條件語句中。
?
7.2 switch 語句
(1)switch 語句用于基于不同條件執行不同動作,每一個 case 分支都是唯一的,從上至下逐一測試,直到匹配為止;
(2)case 后的各個表達式的值的數據類型,必須和 switch 的表達式數據類型一致;
(3)case 后的表達式如果是常量(字面量),則要求不能重復;
(4)switch 語句執行的過程從上至下,直到找到匹配項,匹配項后面也不需要再加 break;
(5)switch 默認情況下 case 最后自帶 break 語句,匹配成功后就不會執行其他 case,如果我們需要執行后面的 case,可以使用?fallthrough ;
(6)switch 從第一個判斷表達式為 true 的 case 開始執行,如果 case 帶有 fallthrough,程序會繼續執行下一條 case,且它不會去判斷下一個 case 的表達式是否為 true。
(7)switch 后面可以不帶表達式,類似 if-else 分支來使用;
?
7.3 select 語句
(1)每個 case 都必須是一個通信;
(2)所有 channel 表達式都會被求值;
(3)所有被發送的表達式都會被求值;
(4)如果任意某個通信可以進行,它就執行,其他被忽略;
(5)如果有多個 case 都可以運行,Select 會隨機公平地選出一個執行。其他不會執行;
(6)如果有 default 子句,則執行該語句。如果沒有 default 子句,select 將阻塞,直到某個通信可以運行;Go 不會重新對 channel 或值進行求值。
?
7.4 break?語句
(1)用于循環語句中跳出循環,并開始執行循環之后的語句;
(2)break 在 switch(開關語句)中在執行一條 case 后跳出語句的作用;
(3)在多重循環中,可以用標號 label 標出想 break 的循環。
?
7.5?continue 語句
(1)有點像 break 語句。但是 continue 不是跳出循環,而是跳過當前循環執行下一次循環語句;
(2)for 循環中,執行 continue 語句會觸發 for 增量語句的執行;
(3)在多重循環中,可以用標號 label 標出想 continue 的循環。
?
7.6?goto 語句
(1)無條件地轉移到過程中指定的行。
(2)goto 語句通常與條件語句配合使用。可用來實現條件轉移, 構成循環,跳出循環體等功能。
(3)但是,在結構化程序設計中一般不主張使用 goto 語句, 以免造成程序流程的混亂,使理解和調試程序都產生困難。
?
7.7 defer 函數
(1)在 Go 中,defer?語句會推遲函數(包括任何參數)的運行,直到包含?defer?語句的函數完成。
(2)通常情況下,當你想要避免忘記任務(例如關閉文件或運行清理進程)時,可以推遲某個函數的運行。
(3)可以根據需要推遲任意多個函數。
(4)defer 語句按逆序運行(后進先出),先運行最后一個,最后運行第一個。
?
7.8?panic 函數與 recover 函數
panic 函數:
(1)運行時錯誤會使 Go 程序進入緊急狀態。 可以強制程序進入緊急狀態,但運行時錯誤(例如數組訪問超出范圍、取消對空指針的引用)也可能會導致進入緊急狀態。
(2)內置?panic()?函數會停止正常的控制流。 所有推遲的函數調用都會正常運行。 進程會在堆棧中繼續,直到所有函數都返回。 然后,程序會崩潰并記錄日志消息。 此消息包含錯誤和堆棧跟蹤,有助于診斷問題的根本原因。
(3)調用?panic()?函數時,可以添加任何值作為參數。 通常,你會發送一條錯誤消息,說明為什么會進入緊急狀態。
recover 函數:
(1)有時,你可能想要避免程序崩潰,改為在內部報告錯誤。 或者,你可能想要先清理混亂情況,然后再讓程序崩潰。 例如,你可能想要關閉與某個資源的連接,以免出現更多問題。
(2)Go 提供內置函數?recover(),允許你在出現緊急狀況之后重新獲得控制權。
(3)只能在已推遲的函數中使用此函數。
(4)如果調用?recover()?函數,則在正常運行的情況下,它會返回?nil,沒有任何其他作用。
兩者的關系:panic 與 recover 是 Go 的兩個內置函數,這兩個內置函數用于處理 Go 運行時的錯誤,panic 用于主動拋出錯誤,recover 用來捕獲 panic 拋出的錯誤。
(1)引發?panic?有兩種情況,一是程序主動調用,二是程序產生運行時錯誤,由運行時檢測并退出;
(2)發生?panic?后,程序會從調用?panic?的函數位置或發生?panic?的地方立即返回,逐層向上執行函數的?defer?語句,然后逐層打印函數調用堆棧,直到被?recover?捕獲或運行到最外層函數;
(3)panic?不但可以在函數正常流程中拋出,在?defer?邏輯里也可以再次調用?panic?或拋出?panic;defer?里面的?panic?能夠被后續執行的?defer?捕獲;
(4)recover?用來捕獲?panic,阻止?panic?繼續向上傳遞;
(5)recover?和?defer?一起使用,但是?defer?只有在后面的函數體內直接被調用才能捕獲?panic?來終止異常,否則返回?nil,異常繼續向外傳遞。
//以下三種方法捕獲失敗 defer recover() //無效 defer fmt.Prinntln(recover) //無效defer func(){func(){recover() //無效,嵌套兩層}() }()//以下三種捕獲有效 defer func(){recover() }()func except(){recover() }func test(){defer except()panic("runtime error") }7.9 示例:用戶登錄(流程控制)
package main import "fmt" func main() {// 實現登錄驗證,有三次機會,如果用戶名和密碼正確提示登錄成功// 否則提示還有幾次機會var name string var pwd stringvar loginChance = 3for i := 1 ; i <= 3; i++ {fmt.Println("請輸入用戶名")fmt.Scanln(&name)fmt.Println("請輸入密碼")fmt.Scanln(&pwd)if name == "user" && pwd == "888888" {fmt.Println("恭喜你登錄成功!")break} else {loginChance--fmt.Printf("你還有%v次登錄機會,請珍惜\n", loginChance)}}if loginChance == 0 {fmt.Println("機會用完,沒有登錄成功!")} }7.10 示例:錯誤處理(控制流中斷)
package mainimport "fmt"func main() {defer func() {if r := recover(); r != nil {fmt.Println("Recovered in main", r)}}()g(0)fmt.Println("Program finished successfully!") }func g(i int) {if i > 3 {fmt.Println("Panicking!")panic("Panic in g() (major)")}defer fmt.Println("Defer in g()", i)fmt.Println("Printing in g()", i)g(i + 1) }/* Printing in g() 0 Printing in g() 1 Printing in g() 2 Printing in g() 3 Panicking! Defer in g() 3 Defer in g() 2 Defer in g() 1 Defer in g() 0 Recovered in main Panic in g() (major) */下面是運行代碼時會發生的情況:
總結:
?
八、數組
數組是具有相同唯一類型的一組已編號且長度固定的數據項序列,這種類型可以是任意的原始類型例如整型、字符串或者自定義類型。
8.1 聲明與初始化
// Go 語言數組聲明需要指定元素類型及元素個數,語法格式如下: var variable_name [SIZE] variable_type// 一維數組的定義方式。例如以下定義了數組 balance 長度為 10 類型為 float32: var balance [10] float32// 數組初始化,初始化數組中 {} 中的元素個數不能大于 [] 中的數字: var balance = [5]float32{1000.0, 2.0, 3.4, 7.0, 50.0}// 如果數組長度不確定,可以使用 ... 代替數組的長度,編譯器會根據元素個數自行推斷數組的長度: var balance = [...]float32{1000.0, 2.0, 3.4, 7.0, 50.0} //或 balance := [...]float32{1000.0, 2.0, 3.4, 7.0, 50.0}// 如果設置了數組的長度,我們還可以通過指定下標來初始化元素: // 將索引為 1 和 3 的元素初始化 balance := [5]float32{1:2.0,3:7.0}// 常用的多維數組聲明方式: var variable_name [SIZE1][SIZE2]...[SIZEN] variable_type// 以下實例聲明了三維的整型數組: var threedim [5][10][4]int// 二維數組初始化 a := [3][4]int{ {0, 1, 2, 3} , /* 第一行索引為 0 */{4, 5, 6, 7} , /* 第二行索引為 1 */{8, 9, 10, 11}, /* 第三行索引為 2 */ //注意:必須要有逗號,因為后面一行的?}?不能單獨一行。 }8.2 賦值與訪問
數組元素可以通過索引(位置)來讀取。格式為數組名后加中括號,中括號中為索引的值。例如:
package mainimport "fmt"func main() {var n [10]int /* n 是一個長度為 10 的數組 */var i,j int/* 為數組 n 初始化元素 */ for i = 0; i < 10; i++ {n[i] = i + 100 /* 設置元素為 i + 100 */}/* 輸出每個數組元素的值 */for j = 0; j < 10; j++ {fmt.Printf("Element[%d] = %d\n", j, n[j] )} }/* Element[0] = 100 Element[1] = 101 Element[2] = 102 Element[3] = 103 Element[4] = 104 Element[5] = 105 Element[6] = 106 Element[7] = 107 Element[8] = 108 Element[9] = 109 */二維數組:
package mainimport "fmt"func main() {// Step 1: 創建數組values := [][]int{}// Step 2: 使用 appped() 函數向空的二維數組添加兩行一維數組row1 := []int{1, 2, 3}row2 := []int{4, 5, 6}values = append(values, row1)values = append(values, row2)// Step 3: 顯示兩行數據fmt.Println("Row 1")fmt.Println(values[0])fmt.Println("Row 2")fmt.Println(values[1])// Step 4: 訪問第一個元素fmt.Println("第一個元素為:")fmt.Println(values[0][0]) }/* Row 1 [1 2 3] Row 2 [4 5 6] 第一個元素為: 1 */使用函數修改數組的值:
package main import ("fmt" )// 函數(數組) func test01(arr01 [3]int) {fmt.Println("arr01 arr old = ", arr01)arr01[0] = 88fmt.Println("arr01 arr new = ", arr01) } // 函數(數組指針) func test02(arr02 *[3]int) {fmt.Printf("arr02指針的值 = %p\n", arr02)fmt.Printf("arr02指針的地址 = %p\n", &arr02)fmt.Println("arr02 arr old = ", *arr02) (*arr02)[0] = 88 fmt.Println("arr02 arr new = ", *arr02) } func main() {arr := [3]int{11, 22, 33}fmt.Println("main arr old =", arr)test01(arr)fmt.Println("main arr from test01 =", arr)fmt.Println("----------以上傳的是數組,數組值沒有變化----------")fmt.Println("----------如果想修改數組,需要傳數組指針----------")fmt.Printf("arr 的地址 = %p\n", &arr)test02(&arr)fmt.Println("main arr from test02 =", arr) }/* main arr old = [11 22 33] arr01 arr old = [11 22 33] arr01 arr new = [88 22 33] main arr from test01 = [11 22 33] ----------以上傳的是數組,數組值沒有變化---------- ----------如果想修改數組,需要傳數組指針---------- arr 的地址 = 0xc0000a8120 arr02指針的值 = 0xc0000a8120 arr02指針的地址 = 0xc0000d4020 arr02 arr old = [11 22 33] arr02 arr new = [88 22 33] main arr from test02 = [88 22 33] */創建各個維度元素數量不一致的多維數組:
package?mainimport?"fmt"func?main()?{// 創建空的二維數組animals?:=?[][]string{}// 創建三一維數組,各數組長度不同row1?:=?[]string{"fish",?"shark",?"eel"}row2?:=?[]string{"bird"}row3?:=?[]string{"lizard",?"salamander"}// 使用 append() 函數將一維數組添加到二維數組中animals?=?append(animals,?row1)animals?=?append(animals,?row2)animals?=?append(animals,?row3)// 循環輸出for?i?:=?range?animals?{fmt.Printf("Row: %v\n",?i)fmt.Println(animals[i])} }/* Row: 0 [fish shark eel] Row: 1 [bird] Row: 2 [lizard salamander] */8.3 range遍歷
package main import ( "fmt" ) func main() { // 二維數組 var value = [3][2]int{{1, 2}, {3, 4}, {5, 6}} // 遍歷二維數組,使用 range // 其實,這里的 i, j 表示行游標和列游標 // v2 就是具體的每一個元素 // v 就是每一行的所有元素 for i, v := range value {for j, v2 := range v { fmt.Printf("value[%v][%v]=%v \t ", i, j, v2) } fmt.Print(v) fmt.Println() } }/* value[0][0]=1 value[0][1]=2 [1 2] value[1][0]=3 value[1][1]=4 [3 4] value[2][0]=5 value[2][1]=6 [5 6] */?
九、切片
9.1 切片內部結構
struct Slice { ??byte* ? ?array; ? ? ? // actual datauintgo ? ?len; ? ? ? ?// number of elementsuintgo ? ?cap; ? ? ? ?// allocated number of elements? };切片有 3 個組件:
- 指針,指向基礎數組可訪問的第一個元素(并非一定是數組的第一個元素)。
- 長度,指示切片中的元素數目,表示 slice 的長度。
- 容量,顯示切片開頭與基礎數組結束之間的元素數目。
下圖顯示了什么是切片:
當把 slice 作為參數,本身傳遞的是值,但其內容就?byte* array,實際傳遞的是引用,所以可以在函數內部修改,但如果對 slice 本身做 append,而且導致 slice 進行了擴容,實際擴容的是函數內復制的一份切片,對于函數外面的切片沒有變化。
9.2 切片項
Go 支持切片運算符?s[i:j],其中:
- s?表示數組。
- i?表示指向它將使用的數組(或另一切片)的第一個元素的指針。
- j?表示切片將使用的最后一個元素的位置。
換句話說,切片只能引用元素的子集。
例如,假設需要 4 個變量來表示一年的每個季度。 下圖說明了它在 Go 中的顯示效果:
若要用代碼表示在上圖中看到的內容,可使用以下代碼:
package mainimport "fmt"func main() {months := []string{"January", "February", "March", "April", "May", "June", "July", "August", "September", "October", "November", "December"}quarter1 := months[0:3]quarter2 := months[3:6]quarter3 := months[6:9]quarter4 := months[9:12]fmt.Println(quarter1, len(quarter1), cap(quarter1))fmt.Println(quarter2, len(quarter2), cap(quarter2))fmt.Println(quarter3, len(quarter3), cap(quarter3))fmt.Println(quarter4, len(quarter4), cap(quarter4)) }/* [January February March] 3 12 [April May June] 3 9 [July August September] 3 6 [October November December] 3 3 */示例代碼:
package mainimport "fmt"func main() {/* 創建切片 */numbers := []int{0,1,2,3,4,5,6,7,8} printSlice(numbers)/* 打印原始切片 */fmt.Println("numbers ==", numbers)/* 打印子切片從索引1(包含) 到索引4(不包含)*/fmt.Println("numbers[1:4] ==", numbers[1:4])/* 默認下限為 0*/fmt.Println("numbers[:3] ==", numbers[:3])/* 默認上限為 len(s)*/fmt.Println("numbers[4:] ==", numbers[4:])numbers1 := make([]int,0,5)printSlice(numbers1)/* 打印子切片從索引 0(包含) 到索引 2(不包含) */number2 := numbers[:2]printSlice(number2)/* 打印子切片從索引 2(包含) 到索引 5(不包含) */number3 := numbers[2:5]printSlice(number3)}func printSlice(x []int){fmt.Printf("len=%d cap=%d slice=%v\n",len(x),cap(x),x) }/* len=9 cap=9 slice=[0 1 2 3 4 5 6 7 8] numbers == [0 1 2 3 4 5 6 7 8] numbers[1:4] == [1 2 3] numbers[:3] == [0 1 2] numbers[4:] == [4 5 6 7 8] len=0 cap=5 slice=[] len=2 cap=9 slice=[0 1] len=3 cap=7 slice=[2 3 4] */9.3 append() 和 copy() 函數
package mainimport "fmt"func main() {var numbers []intprintSlice(numbers)/* 允許追加空切片 */numbers = append(numbers, 0)printSlice(numbers)/* 向切片添加一個元素 */numbers = append(numbers, 1)printSlice(numbers)/* 同時添加多個元素 */numbers = append(numbers, 2,3,4)printSlice(numbers)/* 創建切片 numbers1 是之前切片的兩倍容量*/numbers1 := make([]int, len(numbers), (cap(numbers))*2)/* Go 具有內置函數 copy(dst, src []Type) 用于創建切片的副本。 * 創建一個切片副本,它會在后臺生成新的基礎數組,與原數組互不影響* 你需要發送目標切片和源切片,拷貝 numbers 的內容到 numbers1*/copy(numbers1,numbers)printSlice(numbers1) }func printSlice(x []int){fmt.Printf("len=%d cap=%d slice=%v\n",len(x),cap(x),x) }/* len=0 cap=0 slice=[] len=1 cap=1 slice=[0] len=2 cap=2 slice=[0 1] len=5 cap=6 slice=[0 1 2 3 4] len=5 cap=12 slice=[0 1 2 3 4] */9.4 容量cap變化規律
/*每次cap改變的時候指向array內存的指針都在變化。當在使用 append 的時候,如果 cap==len 了這個時候就會新開辟一塊更大內存,然后把之前的數據復制過去。實際go在append的時候放大cap是有規律的。在 cap 小于1024的情況下是每次擴大到 2 * cap ,當大于1024之后就每次擴大到 1.25 * cap 。 */package mainimport ("fmt""unsafe" )func main() { // 每次cap改變,指向array的ptr就會變化一次s := make([]int, 1)fmt.Printf("len:%d cap: %d array ptr: %v \n", len(s), cap(s), *(*unsafe.Pointer)(unsafe.Pointer(&s)))for i := 0; i < 5; i++ {s = append(s, i)fmt.Printf("len:%d cap: %d array ptr: %v \n", len(s), cap(s), *(*unsafe.Pointer)(unsafe.Pointer(&s)))}fmt.Println("Array:", s) }/* len:1 cap: 1 array ptr: 0xc0000aa058 len:2 cap: 2 array ptr: 0xc0000aa0a0 len:3 cap: 4 array ptr: 0xc0000a8140 len:4 cap: 4 array ptr: 0xc0000a8140 len:5 cap: 8 array ptr: 0xc0000c20c0 len:6 cap: 8 array ptr: 0xc0000c20c0 Array: [0 0 1 2 3 4] */9.5 示例:斐波納契數列
斐波那契數列(Fibonacci sequence),又稱黃金分割數列,因數學家萊昂納多·斐波那契(Leonardoda Fibonacci)以引入;
指的是這樣一個數列:0、1、1、2、3、5、8、13、21、34、55、89、……
在數學上,斐波那契數列以如下被以遞推的方法定義:F(0)=0,F(1)=1,?F(n)=F(n - 1)+F(n - 2)(n?≥ 2,n?∈ N*)
?
?
十、映射
(1)Map 是一種無序的鍵值對的集合。
(2)Map 最重要的一點是通過 key 來快速檢索數據,key 類似于索引,指向數據的值。
(3)Map 是一種集合,可以像迭代數組和切片那樣迭代它。
(4)Map 是無序的,我們無法決定它的返回順序,這是因為 Map 是使用 hash 表來實現的。
10.1 定義 Map
可以使用內建函數 make 也可以使用 map 關鍵字來定義 Map:
/* 聲明變量,默認 map 是 nil ,nil map 不能用來存放鍵值對*/ var map_variable map[key_data_type]value_data_type/* 使用 make 函數 ,創建空映射,可以用來存放鍵值對*/ map_variable := make(map[key_data_type]value_data_type)/* 定義并初始化*/ studentsAge := map[string]int{"john": 32, "bob": 31,}10.2 添加與刪除
delete() 函數用于刪除集合的元素, 參數為 map 和其對應的 key。實例如下:
package mainimport "fmt"func main() {/* 創建map */countryCapitalMap := map[string]string{"France": "Paris", "Italy": "Rome"}/* 添加映射項 */countryCapitalMap ["Japan"] = "Tokyo"countryCapitalMap ["India"] = "New delhi"fmt.Println("原始地圖")/* 打印地圖 *//* 方法1:可以直接打印輸出映射fmt.Println(countryCapitalMap ) *//* 方法2:可使用for range打印輸出映射 */ for country := range countryCapitalMap {fmt.Println(country, "首都是", countryCapitalMap [ country ])}/* for range雙參數輸出for country , capital:= range countryCapitalMap {fmt.Printf("%s\t%s\n", country , capital)}*//*刪除元素*/ delete(countryCapitalMap, "France")fmt.Println("法國條目被刪除")fmt.Println("刪除元素后地圖")/*打印地圖*/for country := range countryCapitalMap {fmt.Println(country, "首都是", countryCapitalMap [ country ])} }/* 原始地圖 India 首都是 New delhi France 首都是 Paris Italy 首都是 Rome Japan 首都是 Tokyo 法國條目被刪除 刪除元素后地圖 Italy 首都是 Rome Japan 首都是 Tokyo India 首都是 New delhi */10.3 判斷存在
訪問映射中沒有的項時 Go 不會返回錯誤,這是正常的。
但有時需要知道某個項是否存在。 在 Go 中,映射的下標表示法可生成兩個值。 第一個是項的值。 第二個是指示鍵是否存在的布爾型標志。
package mainimport "fmt"func main() {studentsAge := make(map[string]int)studentsAge["john"] = 32studentsAge["bob"] = 31age, exist := studentsAge["christy"]if exist {fmt.Println("Christy's age is", age)} else {fmt.Println("Christy's age couldn't be found")} }?
十一、結構體
11.1?聲明和初始化
/* 使用 struct 關鍵字,還要使用希望新的數據類型具有的字段及其類型的列表*/ type Employee struct {ID intFirstName stringLastName stringAddress string }/* 可像操作其他類型一樣使用新類型聲明一個變量*/ var john Employee/* * 可像操作其他類型一樣使用新類型聲明一個變量;* 請注意,該方式必須為結構中的每個字段指定一個值。 */ employee01 := Employee{1001, "John", "Doe", "Doe's Street"}/* * 可更具體地了解要在結構中初始化的字段。* 為每個字段分配值的順序不重要。 * 如果未指定任何其他字段的值,也并不重要。 Go 將根據字段數據類型分配默認值。 */ employee02 := Employee{LastName: "Doe", FirstName: "John"}11.2?結構嵌入
有時,你需要減少重復并重用一種常見的結構。通過 Go 中的結構,可將某結構嵌入到另一結構中。
package mainimport "fmt"type Person struct {ID intFirstName stringLastName stringAddress string }/* 第1種方法:重構結構體*/ type Employee struct {PersonManagerID int }/* 第2種方法:創建新字段*/ type Contractor struct {Information PersonCompanyID int }func main() {/** 第1種方法定義的使用* 無需指定 Person 字段的情況下訪問 Employee 結構中的 FirstName 字段,因為它會自動嵌入其所有字段。 * 但在你初始化結構時,必須明確要給哪個字段分配值*/employee := Employee{Person: Person{FirstName: "John",},}employee.LastName = "Doe"fmt.Println(employee.FirstName)/** 第2種方法定義的使用* 若要引用 Person 結構中的字段,你需要包含員工變量中的 Information 字段*/var contractor Contractor contractor.Information.FirstName = "John"fmt.Println(contractor.FirstName) }11.3 JSON 序列化
可使用結構來對 JSON 中的數據進行編碼和解碼。 Go 對 JSON 格式提供很好的支持,該格式已包含在標準庫包中。
JSON 編碼示例:
package main import ("fmt""encoding/json" )type Monster struct {Name string `json:"monster_name"` // 結構體的 tag 標簽,反射機制Age int `json:"monster_age"`Birthday stringSal float64Skill string }// 將 struct 進行序列化 func testStruct() {monster := Monster{Name :"牛魔王",Age : 500 ,Birthday : "2011-11-11",Sal : 8000.0,Skill : "牛魔拳",}// 將 monster 序列化data, err := json.Marshal(&monster)if err != nil {fmt.Printf("序列號錯誤 err=%v\n", err)}// 輸出序列化后的結果fmt.Printf("monster序列化后 = %v\n", string(data)) }// 將 map 進行序列化 func testMap() {var a map[string]interface{}a = make(map[string]interface{})a["name"] = "紅孩兒"a["age"] = 30a["address"] = "洪崖洞"//將a這個 map 進行序列化data, err := json.Marshal(a)if err != nil {fmt.Printf("序列化錯誤 err=%v\n", err)}// 輸出序列化后的結果fmt.Printf("a map 序列化后 = %v\n", string(data)) }// 將切片進行序列化, []map[string]interface{} func testSlice() {var slice []map[string]interface{}var m1 map[string]interface{}m1 = make(map[string]interface{})m1["name"] = "jack"m1["age"] = "7"m1["address"] = "北京"slice = append(slice, m1)var m2 map[string]interface{}m2 = make(map[string]interface{})m2["name"] = "tom"m2["age"] = "20"m2["address"] = [2]string{"上海","深圳"}slice = append(slice, m2)// 將切片進行序列化操作data, err := json.Marshal(slice)if err != nil {fmt.Printf("序列化錯誤 err=%v\n", err)}// 輸出序列化后的結果fmt.Printf("slice 序列化后 = %v\n", string(data)) }// 對基本數據類型序列化,對基本數據類型進行序列化意義不大 func testFloat64() {var num1 float64 = 2345.67data, err := json.Marshal(num1)if err != nil {fmt.Printf("序列化錯誤 err=%v\n", err)}// 輸出序列化后的結果fmt.Printf("num1 序列化后 = %v\n", string(data)) }func main() {// 演示將結構體, map , 切片進行序列化testStruct()testMap()testSlice()testFloat64() }/* monster序列化后 = {"monster_name":"牛魔王","monster_age":500,"Birthday":"2011-11-11","Sal":8000,"Skill":"牛魔拳"} a map 序列化后 = {"address":"洪崖洞","age":30,"name":"紅孩兒"} slice 序列化后 = [{"address":"北京","age":"7","name":"jack"},{"address":["上海","深圳"],"age":"20","name":"tom"}] num1 序列化后 = 2345.67 */JSON 解碼示例:
package main import ("fmt""encoding/json" )// 定義一個結構體 type Monster struct {Name string Age int Birthday string Sal float64Skill string }// 將 json 字符串,反序列化成 struct func unmarshalStruct() {// json str 一般不是直接寫入,在項目開發中,是通過網絡傳輸獲取到,或者是讀取文件獲取到str := "{\"Name\":\"牛魔王\",\"Age\":500,\"Birthday\":\"2011-11-11\",\"Sal\":8000,\"Skill\":\"牛魔拳\"}"var monster Monstererr := json.Unmarshal([]byte(str), &monster)if err != nil {fmt.Printf("unmarshal err=%v\n", err)}fmt.Printf("反序列化后 monster = %v monster.Name = %v \n", monster, monster.Name) }// 將 json 字符串,反序列化成 map func unmarshalMap() {str := "{\"address\":\"洪崖洞\",\"age\":30,\"name\":\"紅孩兒\"}"// 定義一個 mapvar a map[string]interface{} // 注意:反序列化 map,不需要 make,因為 make 操作被封裝到 Unmarshal 函數err := json.Unmarshal([]byte(str), &a)if err != nil {fmt.Printf("unmarshal err=%v\n", err)}fmt.Printf("反序列化后 map a = %v\n", a)}// 將 json 字符串,反序列化成切片 func unmarshalSlice() {str := "[{\"address\":\"北京\",\"age\":\"7\",\"name\":\"jack\"}," + "{\"address\":[\"上海\",\"深圳\"],\"age\":\"20\",\"name\":\"tom\"}]" //定義一個slicevar slice []map[string]interface{}//反序列化,不需要make,因為make操作被封裝到 Unmarshal函數err := json.Unmarshal([]byte(str), &slice)if err != nil {fmt.Printf("unmarshal err=%v\n", err)}fmt.Printf("反序列化后 slice = %v\n", slice) }func main() {unmarshalStruct()unmarshalMap()unmarshalSlice() }/* 反序列化后 monster = {牛魔王 500 2011-11-11 8000 牛魔拳} monster.Name = 牛魔王 反序列化后 map a = map[address:洪崖洞 age:30 name:紅孩兒] 反序列化后 slice = [map[address:北京 age:7 name:jack] map[address:[上海 深圳] age:20 name:tom]] */類型示例:
package mainimport ("encoding/json""fmt" )/* 敲黑板,劃重點:當要將結構體對象轉換為 JSON 時,對象中的屬性首字母必須是大寫,才能正常轉換為 JSON。*/ type Person struct {ID intFirstName string `json:"name"` // JSON 輸出顯示 name 而不是 FirstName LastName stringAddress string `json:"address,omitempty"` // 忽略空字段 }type Employee struct {PersonManagerID int }func main() {employees := []Employee{Employee{Person: Person{LastName: "Doe", FirstName: "John",},},Employee{Person: Person{LastName: "Campbell", FirstName: "David",},},}data, _ := json.Marshal(employees) // 若要將結構編碼為 JSON,請使用 json.Marshal 函數fmt.Printf("%s\n", data)var decoded []Employeejson.Unmarshal(data, &decoded) // 若要將 JSON 字符串解碼為數據結構,請使用 json.Unmarshal 函數fmt.Printf("%v", decoded) }/* [{"ID":0,"name":"John","LastName":"Doe","ManagerID":0},{"ID":0,"name":"David","LastName":"Campbell","ManagerID":0}] [{{0 John Doe } 0} {{0 David Campbell } 0}] */?
十二、方法
Go 中的方法是一種特殊類型的函數,但存在一個簡單的區別:你必須在函數名稱之前加入一個額外的參數。 此附加參數稱為?接收方。
如你希望分組函數并將其綁定到自定義類型,則方法非常有用。 Go 中的這一方法類似于在其他編程語言中創建類,因為它允許你實現面向對象編程 (OOP) 模型中的某些功能,例如嵌入、重載和封裝。
12.1 聲明方法
package mainimport "fmt"/* 在聲明方法之前,必須先創建結構*/ type triangle struct {size int }type square struct {size int }/* 附加的額外參數(t triangle)就是接收方*/ func (t triangle) perimeter() int {return t.size * 3 }/* 此方法屬于不同的結構,可以為其指定相同的名稱*/ func (s square) perimeter() int {return s.size * 4 }func main() {/* 如果嘗試按平常的方式調用 perimeter() 函數,則此函數將無法正常工作,因為此函數的簽名表明它需要接收方。* 因此,調用此方法的唯一方式是先聲明一個結構,獲取此方法的訪問權限。* 通過對 perimeter() 函數的兩次調用,編譯器將根據接收方類型來確定要調用的函數。* 這有助于在各程序包之間保持函數的一致性和名稱的簡短,并避免將包名稱作為前綴。*/t := triangle{3}s := square{4}fmt.Println("Perimeter (triangle):", t.perimeter())fmt.Println("Perimeter (square):", s.perimeter()) }/* Perimeter (triangle): 9 Perimeter (square): 16 */方法的一個關鍵方面在于,可以為任何類型定義方法,而不只是針對自定義類型(如結構)進行定義。 但是,你不能通過屬于其他包的類型來定義結構。 因此,不能在基本類型(如?string)上創建方法。
12.2 方法的重載
package mainimport "fmt"/* 結構體:三角形*/ type triangle struct {size int }/* 結構體:彩色三角形* 方法可以重用來自一個結構的屬性,以避免出現重復并保持代碼庫的一致性。 * 即使接收方不同,也可以調用已嵌入結構的方法。 */ type coloredTriangle struct {trianglecolor string }/* 三角形大小增加3倍*/ func (t triangle) perimeter() int {return t.size * 3 }/* 重載方法* 可以在 coloredTriangle 結構中更改 perimeter() 方法的實現* 因為方法需要額外參數(接收方),所以,可以使用一個同名的方法,只要此方法專門用于要使用的接收方即可。 */ func (t coloredTriangle) perimeter() int {return t.size * 3 * 2 }func main() {t := triangle{3} // 定義一個三角形fmt.Println("Perimeter (triangle):", t.perimeter())t1 := coloredTriangle{triangle{5}, "blue"} // 定義一個彩色三角形fmt.Println("Size:", t1.size)/* 如果沒有寫重載方法 func (t coloredTriangle) perimeter() int,此處會從 triangle 結構調用 perimeter() 方法,而不必重新創建彩色三角形的方法* 如果寫了重載方法 func (t coloredTriangle) perimeter() int,此處會從 coloredTriangle 結構調用 perimeter() 方法*/fmt.Println("Perimeter", t1.perimeter()) /* 如果你仍需要從 triangle 結構調用 perimeter() 方法,則可通過對其進行顯示訪問來執行此操作。*/fmt.Println("Perimeter (normal)", t1.triangle.perimeter()) }/* Perimeter (triangle): 9 Size: 5 Perimeter 30 Perimeter (normal) 15 */在 Go 中,你可以?重載?方法,并在需要時仍訪問?原始?方法。
12.3 方法的封裝
在 Go 中,只需使用大寫標識符,即可公開方法(public),使用非大寫的標識符將方法設為私有方法(private)。
Go 中的封裝僅在程序包之間有效。 換句話說,你只能隱藏來自其他程序包的實現詳細信息,而不能隱藏程序包本身。
比如,創建新程序包?geometry?:
package geometrytype Triangle struct {size int }func (t *Triangle) doubleSize() {t.size *= 2 }func (t *Triangle) SetSize(size int) {t.size = size }func (t *Triangle) Perimeter() int {t.doubleSize()return t.size * 3 }你可以使用上述程序包,具體如下所示:
func main() {t := geometry.Triangle{}t.SetSize(3)fmt.Println("Perimeter", t.Perimeter()) }此時你應獲得以下輸出:
Perimeter 18如要嘗試從?main()?函數中調用?size?字段或?doubleSize()?方法,程序將死機,如下所示:
func main() {t := geometry.Triangle{}t.SetSize(3)fmt.Println("Size", t.size)fmt.Println("Perimeter", t.Perimeter()) }在運行前面的代碼時,你將看到以下錯誤:
./main.go:12:23: t.size undefined (cannot refer to unexported field or method size)12.4 函數與方法的區別
(1)含義不同
- 函數function是?段具有獨?功能的代碼,可以被反復多次調?,從?實現代碼復?。??法method是?個類的?為功能,只有該類的對象才能調?。
(2)?法有接受者,?函數?接受者
- Go語?的?法method是?種作?于特定類型變量的函數,這種特定類型變量叫做Receiver(接受者、接收者、接收器);
- 接受者的概念類似于傳統?向對象語?中的this或self關鍵字;
- Go語?的接受者強調了?法具有作?對象,?函數沒有作?對象;
- ?個?法就是?個包含了接受者的函數;
- Go語?中, 接受者的類型可以是任何類型,不僅僅是結構體, 也可以是struct類型外的其他類型。
(3)函數不可以重名,??法可以重名
- 只要接受者不同,則?法名可以?樣。
(4)調用方式不一樣
- 方法由struct對象通過(.點號)調用,而函數是直接調用。
?
十三、接口
與其他編程語言中的接口不同,Go 中的接口是滿足隱式實現的。?
示例:編寫自定義?String()?方法來打印自定義字符串,具體如下所示:
package mainimport "fmt"type Person struct {Name, Country string }func (p Person) String() string {return fmt.Sprintf("%v is from %v", p.Name, p.Country) }func main() {rs := Person{"John Doe", "USA"}ab := Person{"Mark Collins", "United Kingdom"}fmt.Printf("%s\n%s\n", rs, ab) }/* John Doe is from USA Mark Collins is from United Kingdom */如你所見,你已使用自定義類型(結構)來寫入?String()?方法的自定義版本。 這是在 Go 中實現接口的一種常用方法。
13.1 示例:interface聲明與使用方法
package main import "fmt"// 聲明/定義一個接口 type Usb interface { Start() Stop() }type Phone struct {} // 讓Phone 實現 Usb接口的方法 func (p Phone) Start() {fmt.Println("手機開始工作。。。") } func (p Phone) Stop() {fmt.Println("手機停止工作。。。") }type Camera struct {}// 讓Camera 實現 Usb接口的方法 func (c Camera) Start() {fmt.Println("相機開始工作。。。") } func (c Camera) Stop() {fmt.Println("相機停止工作。。。") }// Computer,可以識別 Phone 和 Camera 的接口 type Computer struct {} /* 編寫一個 Working 方法,接收一個Usb接口類型變量* 該變量必須實現了 Usb 接口聲明的所有方法* 通過usb接口變量來調用Start和Stop方法* 這里實際體現了多態特性,因為同一個參數可以接收多個類型變量 */ func (c Computer) Working(usb Usb) {usb.Start() usb.Stop() }func main() {// 創建結構體變量computer := Computer{}phone := Phone{}camera := Camera{}// 接口的使用computer.Working(phone)computer.Working(camera) }/* 手機開始工作。。。 手機停止工作。。。 相機開始工作。。。 相機停止工作。。。*/13.2 示例:創建用于管理在線商店的程序包
編寫一個程序,此程序使用自定義程序包來管理在線商店的帳戶。
創建一個名為?Account?的自定義類型,此類型包含帳戶所有者的名字和姓氏。 此類型還必須加入?ChangeName?的功能。
創建另一個名為?Employee?的自定義類型,此類型包含用于將貸方數額存儲為類型?float64?并嵌入?Account?對象的變量。 類型還必須包含?AddCredits、RemoveCredits?和?CheckCredits?的功能。 你需要展示你可以通過?Employee?對象更改帳戶名稱。
將字符串方法寫入?Account?對象,以便按包含名字和姓氏的格式打印?Employee?名稱。
最后,編寫使用已創建程序包的程序,并測試此挑戰中列出的所有功能。 也就是說,主程序應更改名稱、打印名稱、添加貸方、刪除貸方以及檢查余額。
下方是適用于商店程序包的代碼:
package storeimport ("errors""fmt" )type Account struct {FirstName stringLastName string }type Employee struct {AccountCredits float64 }func (a *Account) ChangeName(newname string) {a.FirstName = newname }func (e Employee) String() string {return fmt.Sprintf("Name: %s %s\nCredits: %.2f\n", e.FirstName, e.LastName, e.Credits) }func CreateEmployee(firstName, lastName string, credits float64) (*Employee, error) {return &Employee{Account{firstName, lastName}, credits}, nil }func (e *Employee) AddCredits(amount float64) (float64, error) {if amount > 0.0 {e.Credits += amountreturn e.Credits, nil}return 0.0, errors.New("Invalid credit amount.") }func (e *Employee) RemoveCredits(amount float64) (float64, error) {if amount > 0.0 {if amount <= e.Credits {e.Credits -= amountreturn e.Credits, nil}return 0.0, errors.New("You can't remove more credits than the account has.")}return 0.0, errors.New("You can't remove negative numbers.") }func (e *Employee) CheckCredits() float64 {return e.Credits }下方是主程序用于測試所有功能的代碼:
package mainimport ("fmt""store" )func main() {bruce, _ := store.CreateEmployee("Bruce", "Lee", 500)fmt.Println(bruce.CheckCredits())credits, err := bruce.AddCredits(250)if err != nil {fmt.Println("Error:", err)} else {fmt.Println("New Credits Balance = ", credits)}_, err = bruce.RemoveCredits(2500)if err != nil {fmt.Println("Can't withdraw or overdrawn!", err)}bruce.ChangeName("Mark")fmt.Println(bruce) }13.3 示例:通過實現接口對結構體切片進行排序
package main import ("fmt""sort""math/rand" )type Hero struct{ // 聲明Hero結構體Name stringAge int }type HeroSlice []Hero // 聲明一個Hero結構體切片類型// 實現Interface 接口1(sort.Sort要求) func (hs HeroSlice) Len() int {return len(hs) }// 實現Interface 接口2(sort.Sort要求) // Less方法就是決定你使用什么標準進行排序 func (hs HeroSlice) Less(i, j int) bool {return hs[i].Age < hs[j].Age // 按Hero的Age從小到大排序//return hs[i].Name < hs[j].Name // 按Hero的Name排序,實際上可以按結構體的任意字段進行排序 }// 實現Interface 接口3(sort.Sort要求) func (hs HeroSlice) Swap(i, j int) {hs[i], hs[j] = hs[j], hs[i] // 交換 }func main() { var intSlice = []int{0, -1, 10, 7, 90} // 定義一個數組/切片sort.Ints(intSlice) // 對 intSlice切片進行排序,可以使用系統提供的一般方法fmt.Println(intSlice)/* 對結構體切片進行排序,本代碼重點內容 */var heroes HeroSlice // 定義一個切片for i := 0; i < 10 ; i++ { // 給切片賦值hero := Hero{Name : fmt.Sprintf("英雄|%d", rand.Intn(100)),Age : rand.Intn(100),}heroes = append(heroes, hero) // 將 hero append到 heroes切片}for _ , v := range heroes { // 排序前的順序fmt.Println(v)}sort.Sort(heroes) // 調用sort.Sort,必須實現interface,才可以調用fmt.Println("-----------排序后------------")for _ , v := range heroes { // 排序后的順序fmt.Println(v)}}/* [-1 0 7 10 90] {英雄|81 87} {英雄|47 59} {英雄|81 18} {英雄|25 40} {英雄|56 0} {英雄|94 11} {英雄|62 89} {英雄|28 74} {英雄|11 45} {英雄|37 6} -----------排序后------------ {英雄|56 0} {英雄|37 6} {英雄|94 11} {英雄|81 18} {英雄|25 40} {英雄|11 45} {英雄|47 59} {英雄|28 74} {英雄|81 87} {英雄|62 89} */sort.Interface接口的說明:
// https://studygolang.com/pkgdoc type Interface interface {// Len方法返回集合中的元素個數Len() int// Less方法報告索引i的元素是否比索引j的元素小Less(i, j int) bool// Swap方法交換索引i和j的兩個元素Swap(i, j int) }一個滿足sort.Interface接口的(集合)類型可以被本包的函數進行排序。方法要求集合中的元素可以被整數索引。
?
十四、類型斷言
golang中的所有程序都實現了interface{}的接口,這意味著,所有的類型如 string , int , int64 甚至是自定義的 struct 類型都就此擁有了 interface{} 的接口。
類型斷言(Type Assertion)是一個使用在接口值上的操作,用于檢查接口類型變量所持有的值是否實現了期望的接口或者具體的類型。
在Go語言中類型斷言的語法格式如下:
value, ok := x.(T)其中,x 表示一個接口的類型,T 表示一個具體的類型(也可為接口類型)。
該斷言表達式會返回 x 的值(也就是 value)和一個布爾值(也就是 ok),可根據該布爾值判斷 x 是否為 T 類型:
- 如果 T 是具體某個類型,類型斷言會檢查 x 的動態類型是否等于具體類型 T。如果檢查成功,類型斷言返回的結果是 x 的動態值,其類型是 T。
- 如果 T 是接口類型,類型斷言會檢查 x 的動態類型是否滿足 T。如果檢查成功,x 的動態值不會被提取,返回值是一個類型為 T 的接口值。
- 無論 T 是什么類型,如果 x 是 nil 接口值,類型斷言都會失敗。
接口變量的類型也可以使用一種特殊形式的 swtich 來檢測。下面的代碼片段展示了一個類型分類函數,它有一個可變長度參數,可以是任意類型的數組,它會根據數組元素的實際類型執行不同的動作:
func classifier(items ...interface{}) {for i, x := range items {switch x.(type) {case bool:fmt.Printf("Param #%d is a bool\n", i)case float64:fmt.Printf("Param #%d is a float64\n", i)case int, int64:fmt.Printf("Param #%d is a int\n", i)case nil:fmt.Printf("Param #%d is a nil\n", i)case string:fmt.Printf("Param #%d is a string\n", i)default:fmt.Printf("Param #%d is unknown\n", i)}} }?
十五、隨機數
真隨機和偽隨機概念:
根據以上幾個標準,其對應的隨機數也就分為以下幾類:
15.1 偽隨機示例:math/rand
rand包實現了偽隨機數生成器。
隨機數從資源生成。包水平的函數都使用的默認的公共資源。該資源會在程序每次運行時都產生確定的序列。
如果需要每次運行產生不同的序列,應使用Seed函數進行初始化。
默認資源可以安全的用于多go程并發。
package main import ("fmt""math/rand""time" )func main() {// 返回一個取值范圍在[0,n)的偽隨機int值,如果n<=0會panic // func Intn(n int) intrand1 := rand.Intn(100) //每次生成一個確定的值fmt.Println("rand1 = ", rand1)// 設置種子,使用給定的seed將默認資源初始化到一個確定的狀態;如未調用Seed,默認資源的行為就好像調用了Seed(1)// func Seed(seed int64)// Unix將t表示為Unix時間,即從時間點January 1, 1970 UTC到時間點t所經過的時間(單位秒)// func (t Time) Unix() int64for i:=1; i<=10; i++{rand.Seed(time.Now().Unix())rand2 := rand.Intn(100)fmt.Println("rand2 = ", rand2)}// UnixNano將t表示為Unix時間,即從時間點January 1, 1970 UTC到時間點t所經過的時間(單位納秒)// func (t Time) UnixNano() int64for i:=1; i<=10; i++{rand.Seed(time.Now().UnixNano())rand3 := rand.Intn(100)fmt.Println("rand3 = ", rand3)}}/* rand1 = 81 //每次運行均輸出該值 rand2 = 61 //每秒內輸出相同的值 rand2 = 61 rand2 = 61 rand2 = 61 rand2 = 61 rand2 = 61 rand2 = 61 rand2 = 61 rand2 = 61 rand2 = 61 rand3 = 95 //每納秒內輸出相同的值 rand3 = 95 rand3 = 95 rand3 = 82 rand3 = 82 rand3 = 82 rand3 = 82 rand3 = 82 rand3 = 82 rand3 = 93 */15.2 真隨機示例:crypto/rand
package mainimport ("crypto/rand""fmt""math/big" )// crypto/rand包實現了用于加解密的更安全的隨機數生成器 // 返回一個在[0, max)區間服從均勻分布的隨機值,如果max<=0則會panic // func Int(rand io.Reader, max *big.Int) (n *big.Int, err error)func main() {// 生成 10 個 [0, 100) 范圍的真隨機數for i := 0; i < 10; i++ {result, _ := rand.Int(rand.Reader, big.NewInt(100))fmt.Println(result)} }/* 36 45 17 24 63 19 28 66 38 50 */?
十六、日期與時間
time包提供了時間的顯示和測量用的函數。日歷的計算采用的是公歷。
16.1 type Time
type Time struct {// 內含隱藏或非導出字段 }(1)Time代表一個納秒精度的時間點;
(2)程序中應使用Time類型值來保存和傳遞時間,而不能用指針。就是說,表示時間的變量和字段,應為 time.Time 類型,而不是 *time.Time. 類型;
(3)一個Time類型值可以被多個go程同時使用;
(4)時間點可以使用 Before、After 和 Equal 方法進行比較;
(5)Sub方法讓兩個時間點相減,生成一個 Duration 類型值(代表時間段);
(6)Add方法給一個時間點加上一個時間段,生成一個新的Time類型時間點;
(7)Time零值代表時間點January 1, year 1, 00:00:00.000000000 UTC;因為本時間點一般不會出現在使用中,IsZero方法提供了檢驗時間是否顯式初始化的一個簡單途徑。
(8)每一個時間都具有一個地點信息(及對應地點的時區信息),當計算時間的表示格式時,如 Format、Hour 和 Year 等方法,都會考慮該信息。Local、UTC和In方法返回一個指定時區(但指向同一時間點)的Time。修改地點/時區信息只是會改變其表示;不會修改被表示的時間點,因此也不會影響其計算。
16.2 示例:日期與時間
package main import ("fmt""time" )func main() {// 日期和時間相關函數和方法使用// 1. 獲取當前時間now := time.Now()fmt.Printf("now=%v now type=%T\n", now, now)// 2.通過now可以獲取到年月日,時分秒fmt.Printf("年=%v\n", now.Year())fmt.Printf("月=%v\n", now.Month()) //月份默認顯示類型fmt.Printf("月=%v\n", int(now.Month())) //月份類型轉換fmt.Printf("日=%v\n", now.Day())fmt.Printf("時=%v\n", now.Hour())fmt.Printf("分=%v\n", now.Minute())fmt.Printf("秒=%v\n", now.Second())fmt.Printf("星期=%v\n", now.Weekday())fmt.Println(now.Date()) //日期,返回三個參數fmt.Println(now.Clock()) //時間,返回三個參數// 3.格式化日期時間fmt.Printf("當前日期和時間 %d-%d-%d %d:%d:%d \n", now.Year(), now.Month(), now.Day(), now.Hour(), now.Minute(), now.Second())dateStr := fmt.Sprintf("當前日期和時間 %d-%d-%d %d:%d:%d \n", now.Year(), now.Month(), now.Day(), now.Hour(), now.Minute(), now.Second())fmt.Printf("dateStr=%v\n", dateStr)// 格式化日期時間的第二種方式// Format根據layout指定的格式返回t代表的時間點的格式化文本表示。layout定義了參考時間:// Mon Jan 2 15:04:05 -0700 MST 2006fmt.Printf(now.Format("2006-01-02 15:04:05"))fmt.Println()fmt.Printf(now.Format("2006-01-02"))fmt.Println()fmt.Printf(now.Format("15:04:05"))fmt.Println()fmt.Printf(now.Format("2006"))fmt.Println()// 4.每隔0.1秒中打印一個數字,打印到10時就退出i := 0for {i++fmt.Println(i)// time.Sleep(time.Second) // 休眠,每1秒time.Sleep(time.Millisecond * 100) // 休眠,每0.1秒if i == 10 {break}}// 5.Unix和UnixNano的使用fmt.Printf("unix時間戳 = %v\nunixnano時間戳 = %v\n", now.Unix(), now.UnixNano())}16.3 type Duration
type Duration int64Duration類型代表兩個時間點之間經過的時間,以納秒為單位。可表示的最長時間段大約290年。
const (Nanosecond Duration = 1Microsecond = 1000 * NanosecondMillisecond = 1000 * MicrosecondSecond = 1000 * MillisecondMinute = 60 * SecondHour = 60 * Minute )常用的時間段。沒有定義一天或超過一天的單元,以避免夏時制的時區切換的混亂。
要將Duration類型值表示為某時間單元的個數,用除法:
second := time.Second fmt.Print(int64(second/time.Millisecond)) // prints 1000要將整數個某時間單元表示為Duration類型值,用乘法:
seconds := 10 fmt.Print(time.Duration(seconds)*time.Second) // prints 10s?
十七、cmd編譯
17.1 兩種編譯方法
主要有兩種 cmd 編譯方法:
(1)go build:用于測試編譯包,在項目目錄下生成可執行文件(有main包)
(2)go install:主要用來生成庫和工具
- 一是編譯包文件(無main包),將編譯后的包文件放到 pkg 目錄下($GOPATH/pkg)。
- 二是編譯生成可執行文件(有main包),將可執行文件放到 bin 目錄($GOPATH/bin)。
相同點
- 都能生成可執行文件
不同點
- go build 不能生成包文件, go install 可以生成包文件
- go build 生成可執行文件默認在當前目錄下(可以通過參數指定生成目錄), go install 生成可執行文件默認在bin目錄下($GOPATH/bin)
17.2 go build命令
go build [-o 輸出名] [-i] [編譯標記] [包名](1)如果參數為***.go文件或文件列表,則編譯為一個個單獨的包;
(2)當編譯單個main包(文件),則生成可執行文件;
(3)當編譯單個或多個包非主包時,只構建編譯包,但丟棄生成的對象(.a),僅用作檢查包可以構建;
(4)當編譯包時,會自動忽略'_test.go'的測試文件。
17.3 go build示例
代碼相對于 GOPATH 的目錄關系如下:
D:\golang\.|└── src├─── chapter11| └──── utils| └──── gobuild| ├──── lib.go| └──── main.go└─── chapter12└──── event└──── main.go(1)如果源碼中沒有依賴 GOPATH 的包引用,那么這些源碼可以使用無參數 go build
// 在代碼所在目錄(./src/chapter11/gobuild)下使用 go build 命令 > cd src/chapter11/gobuild/ > go build- go build 在編譯開始時,會搜索當前目錄的 go 源碼。這個例子中,go build 會找到 lib.go 和 main.go 兩個文件。
- 編譯這兩個文件后,生成當前目錄名的可執行文件并放置于當前目錄下,這里生成的可執行文件是 gobuild.exe 。
(2)編譯同目錄的多個源碼文件時,可以在 go build 的后面提供多個文件名,go build 會編譯這些源碼,輸出可執行文件
> cd src/chapter11/gobuild/> go build main.go lib.go //編譯結果,生成main.exe > go build lib.go main.go //編譯結果,生成lib.exe > go build -o test.exe main.go lib.go //編譯結果,生成test.exe- 使用“go build+文件列表”方式編譯時,可執行文件默認選擇文件列表中第一個源碼文件作為可執行文件名輸出。
- 使用“go build+文件列表”方式編譯時,文件列表中的每個文件必須是同一個包的 Go 源碼。
(3)“go build+包”方式編譯;在設置 GOPATH 后,可以直接根據包名進行編譯
D:\> cd golangD:\golang> go build chapter11/gobuild // 后面接要編譯的包名;包名是相對于 GOPATH 下的 src 目錄開始的,生成默認的文件名 main.exe,保存在目錄 GOPATH 下 D:\golang> go build -o test.exe chapter11/gobuild // -o執行指定輸出文件名為 test.exe,保存在目錄 GOPATH 下 D:\golang> go build -o bin/test.exe chapter11/gobuild // -o執行指定輸出目錄bin(GOPATH/bin),輸出文件名為 test.exe注意 :GOPATH 下的目錄結構,源碼必須放在 GOPATH 下的 src 目錄下。所有目錄中不要包含中文。
17.4 go install命令
(1)go install 命令的功能和?go build 命令類似,附加參數絕大多數都可以與 go build 通用。
(2)go install 只是將編譯的中間文件放在 GOPATH 的 pkg 目錄下,以及固定地將編譯結果放在 GOPATH 的 bin 目錄下。
(3)這個命令在內部實際上分成了兩步操作:
- 第一步是生成結果文件(可執行文件或者 .a 包),
- 第二步會把編譯好的結果移到 $GOPATH/pkg 或者 $GOPATH/bin。
go install 的編譯過程有如下規律:
(1)go install 是建立在 GOPATH 上的,無法在獨立的目錄里使用 go install;
(2)GOPATH 下的 bin 目錄放置的是使用 go install 生成的可執行文件,可執行文件的名稱來自于編譯時的包名;
(3)go install 輸出目錄始終為 GOPATH 下的 bin 目錄,無法使用?-o?附加參數進行自定義;
(4)GOPATH 下的 pkg 目錄放置的是編譯期間的中間文件。
17.5 go install示例
D:\> cd golangD:\golang> go install chapter11/gobuild編譯完成后的目錄結構如下:
D:\golang\.|├── bin│ └─── gobuild.exe|├── pkg│ └─── chapter11│ └─── gobuild│ └── lib.a|└── src├─── chapter11| └──── utils| └──── gobuild| ├──── lib.go| └──── main.go└─── chapter12└──── event└──── main.go?
十八、錯誤處理策略
Go 的錯誤處理方法只是一種只需要?if?和?return?語句的控制流機制。
18.1 示例
package mainimport ("fmt""os" )type Employee struct {ID intFirstName stringLastName stringAddress string }func main() {employee, err := getInformation(1001)if err != nil {// Something is wrong. Do something.} else {fmt.Print(employee)} }func getInformation(id int) (*Employee, error) {employee, err := apiCallEmployee(1000)return employee, err }func apiCallEmployee(id int) (*Employee, error) {employee := Employee{LastName: "Doe", FirstName: "John"}return &employee, nil }18.2 處理策略:
當函數返回錯誤時,該錯誤通常是最后一個返回值。 正如上一部分所介紹的那樣,調用方負責檢查是否存在錯誤并處理錯誤。 因此,一個常見策略是繼續使用該模式在子例程中傳播錯誤。 例如,子例程(如上一示例中的?getInformation)可能會將錯誤返回給調用方,而不執行其他任何操作,如下所示:
func getInformation(id int) (*Employee, error) {employee, err := apiCallEmployee(1000)if err != nil {return nil, err // Simply return the error to the caller.}return employee, nil }你可能還需要在傳播錯誤之前添加更多信息。 為此,可以使用?fmt.Errorf()?函數,該函數與我們之前看到的函數類似,但它返回一個錯誤。 例如,你可以向錯誤添加更多上下文,但仍返回原始錯誤,如下所示:
func getInformation(id int) (*Employee, error) {employee, err := apiCallEmployee(1000)if err != nil {return nil, fmt.Errorf("Got an error when getting the employee information: %v", err)}return employee, nil }另一種策略是在錯誤為暫時性錯誤時運行重試邏輯。 例如,可以使用重試策略調用函數三次并等待兩秒鐘,如下所示:
func getInformation(id int) (*Employee, error) {for tries := 0; tries < 3; tries++ {employee, err := apiCallEmployee(1000)if err == nil {return employee, nil}fmt.Println("Server is not responding, retrying ...")time.Sleep(time.Second * 2)}return nil, fmt.Errorf("server has failed to respond to get the employee information") }最后,可以記錄錯誤并對最終用戶隱藏任何實現詳細信息,而不是將錯誤打印到控制臺。
創建可重用的錯誤:
有時錯誤消息數會增加,你需要維持秩序。 或者,你可能需要為要重用的常見錯誤消息創建一個庫。 在 Go 中,你可以使用 errors.New() 函數創建錯誤并在若干部分中重復使用這些錯誤,如下所示:
var ErrNotFound = errors.New("Employee not found!")func getInformation(id int) (*Employee, error) {if id != 1001 {return nil, ErrNotFound}employee := Employee{LastName: "Doe", FirstName: "John"}return &employee, nil }18.3 用于錯誤處理的推薦做法
在 Go 中處理錯誤時,請記住下面一些推薦做法:
- 始終檢查是否存在錯誤,即使預期不存在。 然后正確處理它們,以免向最終用戶公開不必要的信息。
- 在錯誤消息中包含一個前綴,以便了解錯誤的來源。 例如,可以包含包和函數的名稱。
- 創建盡可能多的可重用錯誤變量。
- 了解使用返回錯誤和 panic 之間的差異。 不能執行其他操作時再使用 panic。 例如,如果某個依賴項未準備就緒,則程序運行無意義(除非你想要運行默認行為)。
- 在記錄錯誤時記錄盡可能多的詳細信息,并打印出最終用戶能夠理解的錯誤。
?
十九、日志記錄
19.1 log包
Go 提供了一個用于處理日志的簡單標準包。 可以像使用?fmt?包一樣使用此包。 該標準包不提供日志級別,且不允許為每個包配置單獨的記錄器。 如果需要編寫更復雜的日志記錄配置,可以使用記錄框架執行此操作。?
log.Print()?函數將日期和時間添加為日志消息的前綴。
import ("log" )func main() {log.Print("Hey, I'm a log!") }/* 2020/12/19 13:39:17 Logging in Go! */log.Fatal()?函數記錄錯誤并結束程序,就像使用?os.Exit(1)?一樣。
package mainimport ("fmt""log" )func main() {log.Fatal("Hey, I'm an error log!")fmt.Print("Can you see me?") }/* 2020/12/19 13:53:19 Hey, I'm an error log! exit status 1 */注意最后一行?fmt.Print("Can you see me?")?未運行。 這是因為?log.Fatal()?函數調用停止了該程序。
在使用?log.Panic()?函數時會出現類似行為,該函數也調用?panic()?函數,如下所示:
package mainimport ("fmt""log" )func main() {log.Panic("Hey, I'm an error log!")fmt.Print("Can you see me?") }/* 2020/12/19 13:53:19 Hey, I'm an error log! panic: Hey, I'm an error log!goroutine 1 [running]: log.Panic(0xc000060f58, 0x1, 0x1)/usr/local/Cellar/go/1.15.5/libexec/src/log/log.go:351 +0xae main.main()/Users/christian/go/src/helloworld/logs.go:9 +0x65 exit status 2 */你仍獲得日志消息,但現在還會獲得錯誤堆棧跟蹤。
另一重要函數是?log.SetPrefix()。 可使用它向程序的日志消息添加前綴。 例如,可以使用以下代碼片段:
package mainimport ("log" )func main() {log.SetPrefix("main(): ")log.Print("Hey, I'm a log!")log.Fatal("Hey, I'm an error log!") }/* main(): 2021/01/05 13:59:58 Hey, I'm a log! main(): 2021/01/05 13:59:58 Hey, I'm an error log! exit status 1 */19.2 記錄到文件
除了將日志打印到控制臺之外,你可能還希望將日志發送到文件,以便稍后或實時處理這些日志。
為什么想要將日志發送到文件? 首先,你可能想要對最終用戶隱藏特定信息。 他們可能對這些信息不感興趣,或者你可能公開了敏感信息。 在文件中添加日志后,可以將所有日志集中在一個位置,并將它們與其他事件關聯。 此模式為典型模式:具有可能是臨時的分布式應用程序,例如容器。
讓我們使用以下代碼測試將日志發送到文件:
package mainimport ("log""os" )func main() {file, err := os.OpenFile("info.log", os.O_CREATE|os.O_APPEND|os.O_WRONLY, 0644)if err != nil {log.Fatal(err)}defer file.Close()log.SetOutput(file)log.Print("Hey, I'm a log!") }運行前面的代碼時,在控制臺中看不到任何內容。 在目錄中,你應看到一個名為 info.log 的新文件,其中包含使用?log.Print()?函數發送的日志。 請注意,需要首先創建或打開文件,然后將?log?包配置為將所有輸出發送到文件。 然后,可以像通常做法那樣繼續使用?log.Print()?函數。
?
二十、反射
示例:
package main import ("fmt""reflect" )// 定義了一個Monster結構體 type Monster struct {Name string `json:"name"`Age int `json:"monster_age"`Score float32 Sex string}// 方法的排序默認是按照函數名的排序(ASCII碼) // 方法,返回兩個數的和,在 3 個方法中排第1,標號0 func (s Monster) GetSum(n1, n2 int) int {return n1 + n2 } // 方法, 接收四個值,給s賦值,在 3 個方法中排第3,標號2 func (s Monster) Set(name string, age int, score float32, sex string) {s.Name = names.Age = ages.Score = scores.Sex = sex }// 方法,顯示s的值,在 3 個方法中排第2,標號1 func (s Monster) Print() {fmt.Println("------start------")fmt.Println(s)fmt.Println("-------end-------") }func TestStruct(a interface{}) {typ := reflect.TypeOf(a) // 獲取reflect.Type 類型fmt.Println("reflect.TypeOf",typ)val := reflect.ValueOf(a) // 獲取reflect.Value 類型fmt.Println("reflect.ValueOf",val)kd := val.Kind() // 獲取到a對應的類別fmt.Println("val.Kind",kd)fmt.Println()if kd != reflect.Struct { // 如果傳入的不是struct,就退出fmt.Println("expect struct")return}num := val.NumField() // 獲取到該結構體有幾個字段fmt.Printf("struct has %d fields\n", num) // 變量結構體的所有字段for i := 0; i < num; i++ {fmt.Printf("Field %d: 值為=%v\n", i, val.Field(i)) // 獲取到該結構體的字段tagVal := typ.Field(i).Tag.Get("json") // 獲取到struct標簽, 注意需要通過reflect.Type來獲取tag標簽的值if tagVal != "" { // 如果該字段有tag標簽就顯示,否則就不顯示fmt.Printf("Field %d: tag為=%v\n", i, tagVal)}} fmt.Println()numOfMethod := val.NumMethod() // 獲取到該結構體有多少個方法fmt.Printf("struct has %d methods\n", numOfMethod)val.Method(1).Call(nil) // 調用第 2 個方法;方法的排序默認是按照 函數名的排序(ASCII碼)// 調用結構體的第1個方法Method(0)var params []reflect.Value // 聲明了 []reflect.Valueparams = append(params, reflect.ValueOf(10))params = append(params, reflect.ValueOf(40))res := val.Method(0).Call(params) // 傳入的參數是 []reflect.Value, 返回[]reflect.Valuefmt.Println("res=", res[0].Int()) // 返回結果, 返回的結果是 []reflect.Value }func main() {//創建了一個Monster實例var a Monster = Monster{Name: "huangshulang",Age: 400,Score: 30.8,}//將Monster實例傳遞給TestStruct函數TestStruct(a) }/* reflect.TypeOf main.Monster reflect.ValueOf {huangshulang 400 30.8 } val.Kind structstruct has 4 fields Field 0: 值為=huangshulang Field 0: tag為=name Field 1: 值為=400 Field 1: tag為=monster_age Field 2: 值為=30.8 Field 3: 值為=struct has 3 methods ------start------ {huangshulang 400 30.8 } -------end------- res= 50 */?
二十一、nil
(1)nil 指針解引用會令程序崩潰;
(2)方法可以通過簡單的措施類防范接收 nil 值;
示例:
package mainimport "fmt"// 定義一個結構體 type person struct{age int }// 結構體方法中,處理nil func (p *person) birthday(){if p != nil {p.age++} }func main(){// 普通指針var nowhere *intfmt.Println(nowhere) // nil//fmt.Println(*nowhere) // panic.go //go:nosplitif nowhere != nil{ // 解決方法fmt.Println(*nowhere)} // 結構體var nobody *personfmt.Println(nobody) // nilnobody.birthday() // 如果方法中沒有處理nil,panic.go// 函數var fn func(a,b int) intfmt.Println(fn == nil) // true,因為 fn 沒有被賦予任何函數// 切片var soup []stringfmt.Println(soup == nil) // true fmt.Println(len(soup)) // len 可以處理 nil 切片for _,ingredient := range soup{ // range 可以處理 nil 切片fmt.Println(ingredient) // 0}soup = append(soup,"onion","celery") // append 可以處理 nil 切片fmt.Println(soup) // [onion celery]// 接口var v interface{} // 接口變量的值和類型都是 nil,該變量是 nilfmt.Printf("%T %v %v \n",v,v,v == nil) // nil nil truevar p *intv = p // 接口變量的值是 nil,但類型不是nil,該變量就不是 nilfmt.Printf("%T %v %v \n",v,v,v == nil) // *int nil false }/* <nil> <nil> true true 0 [onion celery] <nil> <nil> true *int <nil> false */?
總結
以上是生活随笔為你收集整理的第61篇 笔记-Go 基础的全部內容,希望文章能夠幫你解決所遇到的問題。