1. 初識Go語言
1.1 Go語言介紹
1.1.1 Go語言是什么
2009年11月10日,Go語言正式成為開源編程語言家庭的一員。
Go語言(或稱Golang)是雲計算時代的C語言。Go語言的誕生是為了讓程序員有更高的生產效率,Go語言專門針對多處理器系統應用程序的編程進行了優化,使用Go編譯的程序可以媲美C或C++代碼的速度,而且更加安全、支持並行進程。
開發人員在為項目選擇語言時,不得不在快速開發和性能之間做出選擇。C和C++這類語言提供了很快的執行速度,而 Ruby 和 Python 這類語言則擅長快速開發。Go語言在這兩者間架起了橋梁,不僅提供了高性能的語言,同時也讓開發更快速。
1.1.2 Go語言優勢
l 可直接編譯成機器碼,不依賴其他庫,glibc的版本有一定要求,部署就是扔一個文件上去就完成了。
l 靜態類型語言,但是有動態語言的感覺,靜態類型的語言就是可以在編譯的時候檢查出來隱藏的大多數問題,動態語言的感覺就是有很多的包可以使用,寫起來的效率很高。
l 語言層面支持並發,這個就是Go最大的特色,天生的支持並發。Go就是基因里面支持的並發,可以充分的利用多核,很容易的使用並發。
l 內置runtime,支持垃圾回收,這屬於動態語言的特性之一吧,雖然目前來說GC(內存垃圾回收機制)不算完美,但是足以應付我們所能遇到的大多數情況,特別是Go1.1之后的GC。
l 簡單易學,Go語言的作者都有C的基因,那么Go自然而然就有了C的基因,那么Go關鍵字是25個,但是表達能力很強大,幾乎支持大多數你在其他語言見過的特性:繼承、重載、對象等。
l 豐富的標准庫,Go目前已經內置了大量的庫,特別是網絡庫非常強大。
l 內置強大的工具,Go語言里面內置了很多工具鏈,最好的應該是gofmt工具,自動化格式化代碼,能夠讓團隊review變得如此的簡單,代碼格式一模一樣,想不一樣都很困難。
l 跨平台編譯,如果你寫的Go代碼不包含cgo,那么就可以做到window系統編譯linux的應用,如何做到的呢?Go引用了plan9的代碼,這就是不依賴系統的信息。
l 內嵌C支持,Go里面也可以直接包含C代碼,利用現有的豐富的C庫。
1.1.3 Go適合用來做什么
l 服務器編程,以前你如果使用C或者C++做的那些事情,用Go來做很合適,例如處理日志、數據打包、虛擬機處理、文件系統等。
l 分布式系統,數據庫代理器等。
l 網絡編程,這一塊目前應用最廣,包括Web應用、API應用、下載應用。
l 內存數據庫,如google開發的groupcache,couchbase的部分組建。
l 雲平台,目前國外很多雲平台在采用Go開發,CloudFoundy的部分組建,前VMare的技術總監自己出來搞的apcera雲平台。
1.2 環境搭建
1.2.1 安裝和設置
請參考資料:
鏈接:https://pan.baidu.com/s/1z1HkX6KkoG9hwT9UaBqNZg
提取碼:xcls
1.2.2 標准命令概述
Go語言中包含了大量用於處理Go語言代碼的命令和工具。其中,go命令就是最常用的一個,它有許多子命令。這些子命令都擁有不同的功能,如下所示。
l build:用於編譯給定的代碼包或Go語言源碼文件及其依賴包。
l clean:用於清除執行其他go命令后遺留的目錄和文件。
l doc:用於執行godoc命令以打印指定代碼包。
l env:用於打印Go語言環境信息。
l fix:用於執行go tool fix命令以修正給定代碼包的源碼文件中包含的過時語法和代碼調用。
l fmt:用於執行gofmt命令以格式化給定代碼包中的源碼文件。
l get:用於下載和安裝給定代碼包及其依賴包(提前安裝git或hg)。
l list:用於顯示給定代碼包的信息。
l run:用於編譯並運行給定的命令源碼文件。
l install:編譯包文件並編譯整個程序。
l test:用於測試給定的代碼包。
l tool:用於運行Go語言的特殊工具。
l version:用於顯示當前安裝的Go語言的版本信息。
1.2.3 學習資料
Go語言官網(需要FQ):https://golang.org/
go中文社區:https://studygolang.com
go中文在線文檔:https://studygolang.com/pkgdoc
1.3 第一個Go程序
1.3.1 Hello Go
// hello.go
package main
import (
"fmt"
)
func main() {
fmt.Println("Hello Go!")
}
1.3.2 代碼分析
每個Go源代碼文件的開頭都是一個package聲明,表示該Go代碼所屬的包。包是Go語言里最基本的分發單位,也是工程管理中依賴關系的體現。
要生成Go可執行程序,必須建立一個名字為main的包,並且在該包中包含一個叫main()的函數(該函數是Go可執行程序的執行起點)。
Go語言的main()函數不能帶參數,也不能定義返回值。
在包聲明之后,是一系列的import語句,用於導入該程序所依賴的包。由於本示例程序用到了Println()函數,所以需要導入該函數所屬的fmt包。
所有Go函數以關鍵字func開頭。一個常規的函數定義包含以下部分:
func 函數名(參數列表)(返回值列表) {
// 函數體
}
Go程序的代碼注釋與C++保持一致,即同時支持以下兩種用法:
/* 塊注釋 */
// 行注釋
Go程序並不要求開發者在每個語句后面加上分號表示語句結束,這是與C和C++的一個明顯不同之處。
注意:強制左花括號{的放置位置,如果把左花括號{另起一行放置,這樣做的結果是Go編譯器報告編譯錯誤。
1.3.3 命令行運行程序
2. 基礎類型
2.1 命名
Go語言中的函數名、變量名、常量名、類型名、語句標號和包名等所有的命名,都遵循一個簡單的命名規則:一個名字必須以一個字母(Unicode字母)或下划線開頭,后面可以跟任意數量的字母、數字或下划線。大寫字母和小寫字母是不同的:heapSort和Heapsort是兩個不同的名字。
Go語言中類似if和switch的關鍵字有25個(均為小寫)。關鍵字不能用於自定義名字,只能在特定語法結構中使用。
break default func interface select
case defer go map struct
chan else goto package switch
const fallthrough if range type
continue for import return var
此外,還有大約30多個預定義的名字,比如int和true等,主要對應內建的常量、類型和函數。
內建常量:
true false iota nil
內建類型:
int int8 int16 int32 int64
uint uint8 uint16 uint32 uint64 uintptr
float32 float64 complex128 complex64
bool byte rune string error
內建函數:
make len cap new append copy close delete
complex real imag
panic recover
2.2 變量
變量是幾乎所有編程語言中最基本的組成元素,變量是程序運行期間可以改變的量。
從根本上說,變量相當於是對一塊數據存儲空間的命名,程序可以通過定義一個變量來申請一塊數據存儲空間,之后可以通過引用變量名來使用這塊存儲空間。
2.2.1 變量聲明
Go語言的變量聲明方式與C和C++語言有明顯的不同。對於純粹的變量聲明, Go語言引入了關鍵字var,而類型信息放在變量名之后,示例如下:
var v1 int
var v2 int
//一次定義多個變量
var v3, v4 int
var (
v5 int
v6 int
)
2.2.2 變量初始化
對於聲明變量時需要進行初始化的場景, var關鍵字可以保留,但不再是必要的元素,如下所示:
var v1 int = 10 // 方式1
var v2 = 10 // 方式2,編譯器自動推導出v2的類型
v3 := 10 // 方式3,編譯器自動推導出v3的類型
fmt.Println("v3 type is ", reflect.TypeOf(v3)) //v3 type is int
//出現在 := 左側的變量不應該是已經被聲明過,:=定義時必須初始化
var v4 int
v4 := 2 //err
2.2.3 變量賦值
var v1 int
v1 = 123
var v2, v3, v4 int
v2, v3, v4 = 1, 2, 3 //多重賦值
i := 10
j := 20
i, j = j, i //多重賦值
2.2.4 匿名變量
_(下划線)是個特殊的變量名,任何賦予它的值都會被丟棄:
_, i, _, j := 1, 2, 3, 4
func test() (int, string) {
return 250, "sb"
}
_, str := test()
2.3 常量
在Go語言中,常量是指編譯期間就已知且不可改變的值。常量可以是數值類型(包括整型、浮點型和復數類型)、布爾類型、字符串類型等。
2.3.1 字面常量(常量值)
所謂字面常量(literal),是指程序中硬編碼的常量,如:
123
3.1415 // 浮點類型的常量
3.2+12i // 復數類型的常量
true // 布爾類型的常量
"foo" // 字符串常量
2.3.2 常量定義
const Pi float64 = 3.14
const zero = 0.0 // 浮點常量, 自動推導類型
const ( // 常量組
size int64 = 1024
eof = -1 // 整型常量, 自動推導類型
)
const u, v float32 = 0, 3 // u = 0.0, v = 3.0,常量的多重賦值
const a, b, c = 3, 4, "foo"
// a = 3, b = 4, c = "foo" //err, 常量不能修改
2.3.3 iota枚舉
常量聲明可以使用iota常量生成器初始化,它用於生成一組以相似規則初始化的常量,但是不用每行都寫一遍初始化表達式。
在一個const聲明語句中,在第一個聲明的常量所在的行,iota將會被置為0,然后在每一個有常量聲明的行加一。
const (
x = iota // x == 0
y = iota // y == 1
z = iota // z == 2
w // 這里隱式地說w = iota,因此w == 3。其實上面y和z可同樣不用"= iota"
)
const v = iota // 每遇到一個const關鍵字,iota就會重置,此時v == 0
const (
h, i, j = iota, iota, iota //h=0,i=0,j=0 iota在同一行值相同
)
const (
a = iota //a=0
b = "B"
c = iota //c=2
d, e, f = iota, iota, iota //d=3,e=3,f=3
g = iota //g = 4
)
const (
x1 = iota * 10 // x1 == 0
y1 = iota * 10 // y1 == 10
z1 = iota * 10 // z1 == 20
)
2.4 基礎數據類型
2.4.1 分類
Go語言內置以下這些基礎類型:
| 類型 |
名稱 |
長度 |
零值 |
說明 |
| bool |
布爾類型 |
1 |
false |
其值不為真即為家,不可以用數字代表true或false |
| byte |
字節型 |
1 |
0 |
uint8別名 |
| rune |
字符類型 |
4 |
0 |
專用於存儲unicode編碼,等價於uint32 |
| int, uint |
整型 |
4或8 |
0 |
32位或64位 |
| int8, uint8 |
整型 |
1 |
0 |
-128 ~ 127, 0 ~ 255 |
| int16, uint16 |
整型 |
2 |
0 |
-32768 ~ 32767, 0 ~ 65535 |
| int32, uint32 |
整型 |
4 |
0 |
-21億 ~ 21 億, 0 ~ 42 億 |
| int64, uint64 |
整型 |
8 |
0 |
|
| float32 |
浮點型 |
4 |
0.0 |
小數位精確到7位 |
| float64 |
浮點型 |
8 |
0.0 |
小數位精確到15位 |
| complex64 |
復數類型 |
8 |
|
|
| complex128 |
復數類型 |
16 |
|
|
| uintptr |
整型 |
4或8 |
|
⾜以存儲指針的uint32或uint64整數 |
| string |
字符串 |
|
"" |
utf-8字符串 |
2.4.2 布爾類型
var v1 bool
v1 = true
v2 := (1 == 2) // v2也會被推導為bool類型
//布爾類型不能接受其他類型的賦值,不支持自動或強制的類型轉換
var b bool
b = 1 // err, 編譯錯誤
b = bool(1) // err, 編譯錯誤
2.4.3 整型
var v1 int32
v1 = 123
v2 := 64 // v1將會被自動推導為int類型
2.4.4 浮點型
var f1 float32
f1 = 12
f2 := 12.0 // 如果不加小數點, fvalue2會被推導為整型而不是浮點型,float64
2.4.5 字符類型
在Go語言中支持兩個字符類型,一個是byte(實際上是uint8的別名),代表utf-8字符串的單個字節的值;另一個是rune,代表單個unicode字符。
package main
import (
"fmt"
)
func main() {
var ch1, ch2, ch3 byte
ch1 = 'a' //字符賦值
ch2 = 97 //字符的ascii碼賦值
ch3 = '\n' //轉義字符
fmt.Printf("ch1 = %c, ch2 = %c, %c", ch1, ch2, ch3)
}
2.4.6 字符串
在Go語言中,字符串也是一種基本類型:
var str string // 聲明一個字符串變量
str = "abc" // 字符串賦值
ch := str[0] // 取字符串的第一個字符
fmt.Printf("str = %s, len = %d\n", str, len(str)) //內置的函數len()來取字符串的長度
fmt.Printf("str[0] = %c, ch = %c\n", str[0], ch)
//`(反引號)括起的字符串為Raw字符串,即字符串在代碼中的形式就是打印時的形式,它沒有字符轉義,換行也將原樣輸出。
str2 := `hello
mike \n \r測試
`
fmt.Println("str2 = ", str2)
/*
str2 = hello
mike \n \r測試
*/
2.4.7 復數類型
復數實際上由兩個實數(在計算機中用浮點數表示)構成,一個表示實部(real),一個表示虛部(imag)。
var v1 complex64 // 由2個float32構成的復數類型
v1 = 3.2 + 12i
v2 := 3.2 + 12i // v2是complex128類型
v3 := complex(3.2, 12) // v3結果同v2
fmt.Println(v1, v2, v3)
//內置函數real(v1)獲得該復數的實部
//通過imag(v1)獲得該復數的虛部
fmt.Println(real(v1), imag(v1))
2.5 fmt包的格式化輸出輸入
2.5.1 格式說明
| 格式 |
含義 |
| %% |
一個%字面量 |
| %b |
一個二進制整數值(基數為2),或者是一個(高級的)用科學計數法表示的指數為2的浮點數 |
| %c |
字符型。可以把輸入的數字按照ASCII碼相應轉換為對應的字符 |
| %d |
一個十進制數值(基數為10) |
| %e |
以科學記數法e表示的浮點數或者復數值 |
| %E |
以科學記數法E表示的浮點數或者復數值 |
| %f |
以標准記數法表示的浮點數或者復數值 |
| %g |
以%e或者%f表示的浮點數或者復數,任何一個都以最為緊湊的方式輸出 |
| %G |
以%E或者%f表示的浮點數或者復數,任何一個都以最為緊湊的方式輸出 |
| %o |
一個以八進制表示的數字(基數為8) |
| %p |
以十六進制(基數為16)表示的一個值的地址,前綴為0x,字母使用小寫的a-f表示 |
| %q |
使用Go語法以及必須時使用轉義,以雙引號括起來的字符串或者字節切片[]byte,或者是以單引號括起來的數字 |
| %s |
字符串。輸出字符串中的字符直至字符串中的空字符(字符串以'\0‘結尾,這個'\0'即空字符) |
| %t |
以true或者false輸出的布爾值 |
| %T |
使用Go語法輸出的值的類型 |
| %U |
一個用Unicode表示法表示的整型碼點,默認值為4個數字字符 |
| %v |
使用默認格式輸出的內置或者自定義類型的值,或者是使用其類型的String()方式輸出的自定義值,如果該方法存在的話 |
| %x |
以十六進制表示的整型值(基數為十六),數字a-f使用小寫表示 |
| %X |
以十六進制表示的整型值(基數為十六),數字A-F使用小寫表示 |
2.5.2 輸出
//整型
a := 15
fmt.Printf("a = %b\n", a) //a = 1111
fmt.Printf("%%\n") //只輸出一個%
//字符
ch := 'a'
fmt.Printf("ch = %c, %c\n", ch, 97) //a, a
//浮點型
f := 3.14
fmt.Printf("f = %f, %g\n", f, f) //f = 3.140000, 3.14
fmt.Printf("f type = %T\n", f) //f type = float64
//復數類型
v := complex(3.2, 12)
fmt.Printf("v = %f, %g\n", v, v) //v = (3.200000+12.000000i), (3.2+12i)
fmt.Printf("v type = %T\n", v) //v type = complex128
//布爾類型
fmt.Printf("%t, %t\n", true, false) //true, false
//字符串
str := "hello go"
fmt.Printf("str = %s\n", str) //str = hello go
2.5.3 輸人
var v int
fmt.Println("請輸入一個整型:")
fmt.Scanf("%d", &v)
//fmt.Scan(&v)
fmt.Println("v = ", v)
2.6 類型轉換
Go語言中不允許隱式轉換,所有類型轉換必須顯式聲明,而且轉換只能發生在兩種相互兼容的類型之間。
var ch byte = 97
//var a int = ch //err, cannot use ch (type byte) as type int in assignment
var a int = int(ch)
2.7 類型別名
type bigint int64 //int64類型改名為bigint
var x bigint = 100
type (
myint int //int改名為myint
mystr string //string改名為mystr
)
3. 運算符
3.1 算術運算符
| 運算符 |
術語 |
示例 |
結果 |
| + |
加 |
10 + 5 |
15 |
| - |
減 |
10 - 5 |
5 |
| * |
乘 |
10 * 5 |
50 |
| / |
除 |
10 / 5 |
2 |
| % |
取模(取余) |
10 % 3 |
1 |
| ++ |
后自增,沒有前自增 |
a=0; a++ |
a=1 |
| -- |
后自減,沒有前自減 |
a=2; a-- |
a=1 |
3.2 關系運算符
| 運算符 |
術語 |
示例 |
結果 |
| == |
相等於 |
4 == 3 |
false |
| != |
不等於 |
4 != 3 |
true |
| < |
小於 |
4 < 3 |
false |
| > |
大於 |
4 > 3 |
true |
| <= |
小於等於 |
4 <= 3 |
false |
| >= |
大於等於 |
4 >= 1 |
true |
3.3 邏輯運算符
| 運算符 |
術語 |
示例 |
結果 |
| ! |
非 |
!a |
如果a為假,則!a為真; 如果a為真,則!a為假。 |
| && |
與 |
a && b |
如果a和b都為真,則結果為真,否則為假。 |
| || |
或 |
a || b |
如果a和b有一個為真,則結果為真,二者都為假時,結果為假。 |
3.4 位運算符
| 運算符 |
術語 |
說明 |
示例 |
| & |
按位與 |
參與運算的兩數各對應的二進位相與 |
60 & 13 結果為12 |
| | |
按位或 |
參與運算的兩數各對應的二進位相或 |
60 | 13 結果為61 |
| ^ |
異或 |
參與運算的兩數各對應的二進位相異或,當兩對應的二進位相異時,結果為1 |
60 ^ 13 結果為240 |
| << |
左移 |
左移n位就是乘以2的n次方。 左邊丟棄,右邊補0。 |
4 << 2 結果為16 |
| >> |
右移 |
右移n位就是除以2的n次方。 右邊丟棄,左邊補位。 |
4 >> 2 結果為1 |
3.5 賦值運算符
| 運算符 |
說明 |
示例 |
| = |
普通賦值 |
c = a + b 將 a + b 表達式結果賦值給 c |
| += |
相加后再賦值 |
c += a 等價於 c = c + a |
| -= |
相減后再賦值 |
c -= a 等價於 c = c - a |
| *= |
相乘后再賦值 |
c *= a 等價於 c = c * a |
| /= |
相除后再賦值 |
c /= a 等價於 c = c / a |
| %= |
求余后再賦值 |
c %= a 等價於 c = c % a |
| <<= |
左移后賦值 |
c <<= 2 等價於 c = c << 2 |
| >>= |
右移后賦值 |
c >>= 2 等價於 c = c >> 2 |
| &= |
按位與后賦值 |
c &= 2 等價於 c = c & 2 |
| ^= |
按位異或后賦值 |
c ^= 2 等價於 c = c ^ 2 |
| |= |
按位或后賦值 |
c |= 2 等價於 c = c | 2 |
3.6 其他運算符
| 運算符 |
術語 |
示例 |
說明 |
| & |
取地址運算符 |
&a |
變量a的地址 |
| * |
取值運算符 |
*a |
指針變量a所指向內存的值 |
3.7 運算符優先級
在Go語言中,一元運算符擁有最高的優先級,二元運算符的運算方向均是從左至右。
下表列出了所有運算符以及它們的優先級,由上至下代表優先級由高到低:
| 優先級 |
運算符 |
| 7 |
^ ! |
| 6 |
* / % << >> & &^ |
| 5 |
+ - | ^ |
| 4 |
== != < <= >= > |
| 3 |
<- |
| 2 |
&& |
| 1 |
|| |
4. 流程控制
Go語言支持最基本的三種程序運行結構:順序結構、選擇結構、循環結構。
l 順序結構:程序按順序執行,不發生跳轉。
l 選擇結構:依據是否滿足條件,有選擇的執行相應功能。
l 循環結構:依據條件是否滿足,循環多次執行某段代碼。
4.1 選擇結構
4.1.1 if語句
4.1.1.1 if
var a int = 3
if a == 3 { //條件表達式沒有括號
fmt.Println("a==3")
}
//支持一個初始化表達式, 初始化字句和條件表達式直接需要用分號分隔
if b := 3; b == 3 {
fmt.Println("b==3")
}
4.1.1.2 if ... else
if a := 3; a == 4 {
fmt.Println("a==4")
} else { //左大括號必須和條件語句或else在同一行
fmt.Println("a!=4")
}
4.1.1.3 if ... else if ... else
if a := 3; a > 3 {
fmt.Println("a>3")
} else if a < 3 {
fmt.Println("a<3")
} else if a == 3 {
fmt.Println("a==3")
} else {
fmt.Println("error")
}
4.1.2 switch語句
Go里面switch默認相當於每個case最后帶有break,匹配成功后不會自動向下執行其他case,而是跳出整個switch, 但是可以使用fallthrough強制執行后面的case代碼:
var score int = 90
switch score {
case 90:
fmt.Println("優秀")
//fallthrough
case 80:
fmt.Println("良好")
//fallthrough
case 50, 60, 70:
fmt.Println("一般")
//fallthrough
default:
fmt.Println("差")
}
可以使用任何類型或表達式作為條件語句:
//1
switch s1 := 90; s1 { //初始化語句;條件
case 90:
fmt.Println("優秀")
case 80:
fmt.Println("良好")
default:
fmt.Println("一般")
}
//2
var s2 int = 90
switch { //這里沒有寫條件
case s2 >= 90: //這里寫判斷語句
fmt.Println("優秀")
case s2 >= 80:
fmt.Println("良好")
default:
fmt.Println("一般")
}
//3
switch s3 := 90; { //只有初始化語句,沒有條件
case s3 >= 90: //這里寫判斷語句
fmt.Println("優秀")
case s3 >= 80:
fmt.Println("良好")
default:
fmt.Println("一般")
}
4.2 循環語句
4.2.1 for
var i, sum int
for i = 1; i <= 100; i++ {
sum += i
}
fmt.Println("sum = ", sum)
4.2.2 range
關鍵字 range 會返回兩個值,第一個返回值是元素的數組下標,第二個返回值是元素的值:
s := "abc"
for i := range s { //支持 string/array/slice/map。
fmt.Printf("%c\n", s[i])
}
for _, c := range s { // 忽略 index
fmt.Printf("%c\n", c)
}
for i, c := range s {
fmt.Printf("%d, %c\n", i, c)
}
4.3 跳轉語句
4.3.1 break和continue
在循環里面有兩個關鍵操作break和continue,break操作是跳出當前循環,continue是跳過本次循環。
for i := 0; i < 5; i++ {
if 2 == i {
//break //break操作是跳出當前循環
continue //continue是跳過本次循環
}
fmt.Println(i)
}
注意:break可⽤於for、switch、select,⽽continue僅能⽤於for循環。
4.3.2 goto
用goto跳轉到必須在當前函數內定義的標簽:
func main() {
for i := 0; i < 5; i++ {
for {
fmt.Println(i)
goto LABEL //跳轉到標簽LABEL,從標簽處,執行代碼
}
}
fmt.Println("this is test")
LABEL:
fmt.Println("it is over")
}
5. 函數
5.1 定義格式
函數構成代碼執行的邏輯結構。在Go語言中,函數的基本組成為:關鍵字func、函數名、參數列表、返回值、函數體和返回語句。
Go 語言函數定義格式如下:
func FuncName(/*參數列表*/) (o1 type1, o2 type2/*返回類型*/) {
//函數體
return v1, v2 //返回多個值
}
函數定義說明:
l func:函數由關鍵字 func 開始聲明
l FuncName:函數名稱,根據約定,函數名首字母小寫即為private,大寫即為public
l 參數列表:函數可以有0個或多個參數,參數格式為:變量名 類型,如果有多個參數通過逗號分隔,不支持默認參數
l 返回類型:
① 上面返回值聲明了兩個變量名o1和o2(命名返回參數),這個不是必須,可以只有類型沒有變量名
② 如果只有一個返回值且不聲明返回值變量,那么你可以省略,包括返回值的括號
③ 如果沒有返回值,那么就直接省略最后的返回信息
④ 如果有返回值, 那么必須在函數的內部添加return語句
5.2 自定義函數
5.2.1 無參無返回值
func Test() { //無參無返回值函數定義
fmt.Println("this is a test func")
}
func main() {
Test() //無參無返回值函數調用
}
5.2.2 有參無返回值
5.2.2.1 普通參數列表
func Test01(v1 int, v2 int) { //方式1
fmt.Printf("v1 = %d, v2 = %d\n", v1, v2)
}
func Test02(v1, v2 int) { //方式2, v1, v2都是int類型
fmt.Printf("v1 = %d, v2 = %d\n", v1, v2)
}
func main() {
Test01(10, 20) //函數調用
Test02(11, 22) //函數調用
}
5.2.2.2 不定參數列表
1) 不定參數類型
不定參數是指函數傳入的參數個數為不定數量。為了做到這點,首先需要將函數定義為接受不定參數類型:
//形如...type格式的類型只能作為函數的參數類型存在,並且必須是最后一個參數
func Test(args ...int) {
for _, n := range args { //遍歷參數列表
fmt.Println(n)
}
}
func main() {
//函數調用,可傳0到多個參數
Test()
Test(1)
Test(1, 2, 3, 4)
}
2) 不定參數的傳遞
func MyFunc01(args ...int) {
fmt.Println("MyFunc01")
for _, n := range args { //遍歷參數列表
fmt.Println(n)
}
}
func MyFunc02(args ...int) {
fmt.Println("MyFunc02")
for _, n := range args { //遍歷參數列表
fmt.Println(n)
}
}
func Test(args ...int) {
MyFunc01(args...) //按原樣傳遞, Test()的參數原封不動傳遞給MyFunc01
MyFunc02(args[1:]...) //Test()參數列表中,第1個參數及以后的參數傳遞給MyFunc02
}
func main() {
Test(1, 2, 3) //函數調用
}
5.2.3 無參有返回值
有返回值的函數,必須有明確的終止語句,否則會引發編譯錯誤。
5.2.3.1 一個返回值
func Test01() int { //方式1
return 250
}
//官方建議:最好命名返回值,因為不命名返回值,雖然使得代碼更加簡潔了,但是會造成生成的文檔可讀性差
func Test02() (value int) { //方式2, 給返回值命名
value = 250
return value
}
func Test03() (value int) { //方式3, 給返回值命名
value = 250
return
}
func main() {
v1 := Test01() //函數調用
v2 := Test02() //函數調用
v3 := Test03() //函數調用
fmt.Printf("v1 = %d, v2 = %d, v3 = %d\n", v1, v2, v3)
}
5.2.3.2 多個返回值
func Test01() (int, string) { //方式1
return 250, "sb"
}
func Test02() (a int, str string) { //方式2, 給返回值命名
a = 250
str = "sb"
return
}
func main() {
v1, v2 := Test01() //函數調用
_, v3 := Test02() //函數調用, 第一個返回值丟棄
v4, _ := Test02() //函數調用, 第二個返回值丟棄
fmt.Printf("v1 = %d, v2 = %s, v3 = %s, v4 = %d\n", v1, v2, v3, v4)
}
5.2.4 有參有返回值
//求2個數的最小值和最大值
func MinAndMax(num1 int, num2 int) (min int, max int) {
if num1 > num2 { //如果num1 大於 num2
min = num2
max = num1
} else {
max = num2
min = num1
}
return
}
func main() {
min, max := MinAndMax(33, 22)
fmt.Printf("min = %d, max = %d\n", min, max) //min = 22, max = 33
}
5.3 遞歸函數
遞歸指函數可以直接或間接的調用自身。
遞歸函數通常有相同的結構:一個跳出條件和一個遞歸體。所謂跳出條件就是根據傳入的參數判斷是否需要停止遞歸,而遞歸體則是函數自身所做的一些處理。
//通過循環實現1+2+3……+100
func Test01() int {
i := 1
sum := 0
for i = 1; i <= 100; i++ {
sum += i
}
return sum
}
//通過遞歸實現1+2+3……+100
func Test02(num int) int {
if num == 1 {
return 1
}
return num + Test02(num-1) //函數調用本身
}
//通過遞歸實現1+2+3……+100
func Test03(num int) int {
if num == 100 {
return 100
}
return num + Test03(num+1) //函數調用本身
}
func main() {
fmt.Println(Test01()) //5050
fmt.Println(Test02(100)) //5050
fmt.Println(Test03(1)) //5050
}
5.4 函數類型
在Go語言中,函數也是一種數據類型,我們可以通過type來定義它,它的類型就是所有擁有相同的參數,相同的返回值的一種類型。
type FuncType func(int, int) int //聲明一個函數類型, func后面沒有函數名
//函數中有一個參數類型為函數類型:f FuncType
func Calc(a, b int, f FuncType) (result int) {
result = f(a, b) //通過調用f()實現任務
return
}
func Add(a, b int) int {
return a + b
}
func Minus(a, b int) int {
return a - b
}
func main() {
//函數調用,第三個參數為函數名字,此函數的參數,返回值必須和FuncType類型一致
result := Calc(1, 1, Add)
fmt.Println(result) //2
var f FuncType = Minus
fmt.Println("result = ", f(10, 2)) //result = 8
}
5.5 匿名函數與閉包
所謂閉包就是一個函數“捕獲”了和它在同一作用域的其它常量和變量。這就意味着當閉包被調用的時候,不管在程序什么地方調用,閉包能夠使用這些常量或者變量。它不關心這些捕獲了的變量和常量是否已經超出了作用域,所以只有閉包還在使用它,這些變量就還會存在。
在Go語言里,所有的匿名函數(Go語言規范中稱之為函數字面量)都是閉包。匿名函數是指不需要定義函數名的一種函數實現方式,它並不是一個新概念,最早可以回溯到1958年的Lisp語言。
func main() {
i := 0
str := "mike"
//方式1
f1 := func() { //匿名函數,無參無返回值
//引用到函數外的變量
fmt.Printf("方式1:i = %d, str = %s\n", i, str)
}
f1() //函數調用
//方式1的另一種方式
type FuncType func() //聲明函數類型, 無參無返回值
var f2 FuncType = f1
f2() //函數調用
//方式2
var f3 FuncType = func() {
fmt.Printf("方式2:i = %d, str = %s\n", i, str)
}
f3() //函數調用
//方式3
func() { //匿名函數,無參無返回值
fmt.Printf("方式3:i = %d, str = %s\n", i, str)
}() //別忘了后面的(), ()的作用是,此處直接調用此匿名函數
//方式4, 匿名函數,有參有返回值
v := func(a, b int) (result int) {
result = a + b
return
}(1, 1) //別忘了后面的(1, 1), (1, 1)的作用是,此處直接調用此匿名函數, 並傳參
fmt.Println("v = ", v)
}
閉包捕獲外部變量特點:
func main() {
i := 10
str := "mike"
func() {
i = 100
str = "go"
//內部:i = 100, str = go
fmt.Printf("內部:i = %d, str = %s\n", i, str)
}() //別忘了后面的(), ()的作用是,此處直接調用此匿名函數
//外部:i = 100, str = go
fmt.Printf("外部:i = %d, str = %s\n", i, str)
}
函數返回值為匿名函數:
// squares返回一個匿名函數,func() int
// 該匿名函數每次被調用時都會返回下一個數的平方。
func squares() func() int {
var x int
return func() int {//匿名函數
x++ //捕獲外部變量
return x * x
}
}
func main() {
f := squares()
fmt.Println(f()) // "1"
fmt.Println(f()) // "4"
fmt.Println(f()) // "9"
fmt.Println(f()) // "16"
}
函數squares返回另一個類型為 func() int 的函數。對squares的一次調用會生成一個局部變量x並返回一個匿名函數。每次調用時匿名函數時,該函數都會先使x的值加1,再返回x的平方。第二次調用squares時,會生成第二個x變量,並返回一個新的匿名函數。新匿名函數操作的是第二個x變量。
通過這個例子,我們看到變量的生命周期不由它的作用域決定:squares返回后,變量x仍然隱式的存在於f中。
5.6 延遲調用defer
5.6.1 defer作用
關鍵字 defer ⽤於延遲一個函數或者方法(或者當前所創建的匿名函數)的執行。注意,defer語句只能出現在函數或方法的內部。
func main() {
fmt.Println("this is a test")
defer fmt.Println("this is a defer") //main結束前調用
/*
運行結果:
this is a test
this is a defer
*/
}
defer語句經常被用於處理成對的操作,如打開、關閉、連接、斷開連接、加鎖、釋放鎖。通過defer機制,不論函數邏輯多復雜,都能保證在任何執行路徑下,資源被釋放。釋放資源的defer應該直接跟在請求資源的語句后。
5.6.2 多個defer執行順序
如果一個函數中有多個defer語句,它們會以LIFO(后進先出)的順序執行。哪怕函數或某個延遲調用發生錯誤,這些調用依舊會被執⾏。
func test(x int) {
fmt.Println(100 / x)//x為0時,產生異常
}
func main() {
defer fmt.Println("aaaaaaaa")
defer fmt.Println("bbbbbbbb")
defer test(0)
defer fmt.Println("cccccccc")
/*
運行結果:
cccccccc
bbbbbbbb
aaaaaaaa
panic: runtime error: integer divide by zero
*/
}
5.6.3 defer和匿名函數結合使用
func main() {
a, b := 10, 20
defer func(x int) { // a以值傳遞方式傳給x
fmt.Println("defer:", x, b) // b 閉包引用
}(a)
a += 10
b += 100
fmt.Printf("a = %d, b = %d\n", a, b)
/*
運行結果:
a = 20, b = 120
defer: 10 120
*/
}
5.7 獲取命令行參數
package main
import (
"fmt"
"os" //os.Args所需的包
)
func main() {
args := os.Args //獲取用戶輸入的所有參數
//如果用戶沒有輸入,或參數個數不夠,則調用該函數提示用戶
if args == nil || len(args) < 2 {
fmt.Println("err: xxx ip port")
return
}
ip := args[1] //獲取輸入的第一個參數
port := args[2] //獲取輸入的第二個參數
fmt.Printf("ip = %s, port = %s\n", ip, port)
}
運行結果如下:
5.8 作用域
作用域為已聲明標識符所表示的常量、類型、變量、函數或包在源代碼中的作用范圍。
5.8.1 局部變量
在函數體內聲明的變量、參數和返回值變量就是局部變量,它們的作用域只在函數體內:
func test(a, b int) {
var c int
a, b, c = 1, 2, 3
fmt.Printf("a = %d, b = %d, c = %d\n", a, b, c)
}
func main() {
//a, b, c = 1, 2, 3 //err, a, b, c不屬於此作用域
{
var i int
i = 10
fmt.Printf("i = %d\n", i)
}
//i = 20 //err, i不屬於此作用域
if a := 3; a == 3 {
fmt.Println("a = ", a)
}
//a = 4 //err,a只能if內部使用
}
5.8.2 全局變量
在函數體外聲明的變量稱之為全局變量,全局變量可以在整個包甚至外部包(被導出后)使用。
var a int //全局變量的聲明
func test() {
fmt.Printf("test a = %d\n", a)
}
func main() {
a = 10
fmt.Printf("main a = %d\n", a) //main a = 10
test() //test a = 10
}
5.8.3 不同作用域同名變量
在不同作用域可以聲明同名的變量,其訪問原則為:在同一個作用域內,就近原則訪問最近的變量,如果此作用域沒有此變量聲明,則訪問全局變量,如果全局變量也沒有,則報錯。
var a int //全局變量的聲明
func test01(a float32) {
fmt.Printf("a type = %T\n", a) //a type = float32
}
func main() {
fmt.Printf("a type = %T\n", a) //a type = int, 說明使用全局變量的a
var a uint8 //局部變量聲明
{
var a float64 //局部變量聲明
fmt.Printf("a type = %T\n", a) //a type = float64
}
fmt.Printf("a type = %T\n", a) //a type = uint8
test01(3.14)
test02()
}
func test02() {
fmt.Printf("a type = %T\n", a) //a type = int
}
6. 工程管理
在實際的開發工作中,直接調用編譯器進行編譯和鏈接的場景是少而又少,因為在工程中不
會簡單到只有一個源代碼文件,且源文件之間會有相互的依賴關系。如果這樣一個文件一個文件逐步編譯,那不亞於一場災難。 Go語言的設計者作為行業老將,自然不會忽略這一點。早期Go語言使用makefile作為臨時方案,到了Go 1發布時引入了強大無比的Go命令行工具。
Go命令行工具的革命性之處在於徹底消除了工程文件的概念,完全用目錄結構和包名來推導工程結構和構建順序。針對只有一個源文件的情況討論工程管理看起來會比較多余,因為這可以直接用go run和go build搞定。下面我們將用一個更接近現實的虛擬項目來展示Go語言的基本工程管理方法。
6.1 工作區
6.1.1 工作區介紹
Go代碼必須放在工作區中。工作區其實就是一個對應於特定工程的目錄,它應包含3個子目錄:src目錄、pkg目錄和bin目錄。
l src目錄:用於以代碼包的形式組織並保存Go源碼文件。(比如:.go .c .h .s等)
l pkg目錄:用於存放經由go install命令構建安裝后的代碼包(包含Go庫源碼文件)的“.a”歸檔文件。
l bin目錄:與pkg目錄類似,在通過go install命令完成安裝后,保存由Go命令源碼文件生成的可執行文件。
目錄src用於包含所有的源代碼,是Go命令行工具一個強制的規則,而pkg和bin則無需手動創建,如果必要Go命令行工具在構建過程中會自動創建這些目錄。
需要特別注意的是,只有當環境變量GOPATH中只包含一個工作區的目錄路徑時,go install命令才會把命令源碼安裝到當前工作區的bin目錄下。若環境變量GOPATH中包含多個工作區的目錄路徑,像這樣執行go install命令就會失效,此時必須設置環境變量GOBIN。
6.1.2 GOPATH設置
為了能夠構建這個工程,需要先把所需工程的根目錄加入到環境變量GOPATH中。否則,即使處於同一工作目錄(工作區),代碼之間也無法通過絕對代碼包路徑完成調用。
在實際開發環境中,工作目錄往往有多個。這些工作目錄的目錄路徑都需要添加至GOPATH。當有多個目錄時,請注意分隔符,多個目錄的時候Windows是分號,Linux系統是冒號,當有多個GOPATH時,默認會將go get的內容放在第一個目錄下。
6.2 包
所有 Go 語言的程序都會組織成若干組文件,每組文件被稱為一個包。這樣每個包的代碼都可以作為很小的復用單元,被其他項目引用。
一個包的源代碼保存在一個或多個以.go為文件后綴名的源文件中,通常一個包所在目錄路徑的后綴是包的導入路徑。
6.2.1 自定義包
對於一個較大的應用程序,我們應該將它的功能性分隔成邏輯的單元,分別在不同的包里實現。我們創建的的自定義包最好放在GOPATH的src目錄下(或者GOPATH src的某個子目錄)。
在Go語言中,代碼包中的源碼文件名可以是任意的。但是,這些任意名稱的源碼文件都必須以包聲明語句作為文件中的第一行,每個包都對應一個獨立的名字空間:
package calc
包中成員以名稱⾸字母⼤⼩寫決定訪問權限:
l public: ⾸字母⼤寫,可被包外訪問
l private: ⾸字母⼩寫,僅包內成員可以訪問
注意:同一個目錄下不能定義不同的package。
6.2.2 main包
在 Go 語言里,命名為 main 的包具有特殊的含義。 Go 語言的編譯程序會試圖把這種名字的包編譯為二進制可執行文件。所有用 Go 語言編譯的可執行程序都必須有一個名叫 main 的包。一個可執行程序有且僅有一個 main 包。
當編譯器發現某個包的名字為 main 時,它一定也會發現名為 main()的函數,否則不會創建可執行文件。 main()函數是程序的入口,所以,如果沒有這個函數,程序就沒有辦法開始執行。程序編譯時,會使用聲明 main 包的代碼所在的目錄的目錄名作為二進制可執行文件的文件名。
6.2.3 main函數和init函數
Go里面有兩個保留的函數:init函數(能夠應用於所有的package)和main函數(只能應用於package main)。這兩個函數在定義時不能有任何的參數和返回值。雖然一個package里面可以寫任意多個init函數,但這無論是對於可讀性還是以后的可維護性來說,我們都強烈建議用戶在一個package中每個文件只寫一個init函數。
Go程序會自動調用init()和main(),所以你不需要在任何地方調用這兩個函數。每個package中的init函數都是可選的,但package main就必須包含一個main函數。
每個包可以包含任意多個 init 函數,這些函數都會在程序執行開始的時候被調用。所有被
編譯器發現的 init 函數都會安排在 main 函數之前執行。 init 函數用在設置包、初始化變量或者其他要在程序運行前優先完成的引導工作。
程序的初始化和執行都起始於main包。如果main包還導入了其它的包,那么就會在編譯時將它們依次導入。
有時一個包會被多個包同時導入,那么它只會被導入一次(例如很多包可能都會用到fmt包,但它只會被導入一次,因為沒有必要導入多次)。
當一個包被導入時,如果該包還導入了其它的包,那么會先將其它包導入進來,然后再對這些包中的包級常量和變量進行初始化,接着執行init函數(如果有的話),依次類推。等所有被導入的包都加載完畢了,就會開始對main包中的包級常量和變量進行初始化,然后執行main包中的init函數(如果存在的話),最后執行main函數。下圖詳細地解釋了整個執行過程:
示例代碼目錄結構:
main.go示例代碼如下:
// main.go
package main
import (
"fmt"
"test"
)
func main() {
fmt.Println("main.go main() is called")
test.Test()
}
test.go示例代碼如下:
//test.go
package test
import "fmt"
func init() {
fmt.Println("test.go init() is called")
}
func Test() {
fmt.Println("test.go Test() is called")
}
運行結果:
6.2.4 導入包
導入包需要使用關鍵字import,它會告訴編譯器你想引用該位置的包內的代碼。包的路徑可以是相對路徑,也可以是絕對路徑。
//方法1
import "calc"
import "fmt"
//方法2
import (
"calc"
"fmt"
)
標准庫中的包會在安裝 Go 的位置找到。 Go 開發者創建的包會在 GOPATH 環境變量指定的目錄里查找。GOPATH 指定的這些目錄就是開發者的個人工作空間。
如果編譯器查遍 GOPATH 也沒有找到要導入的包,那么在試圖對程序執行 run 或者 build
的時候就會出錯。
注意:如果導入包之后,未調用其中的函數或者類型將會報出編譯錯誤。
6.2.4.1 點操作
import (
//這個點操作的含義是這個包導入之后在你調用這個包的函數時,可以省略前綴的包名
. "fmt"
)
func main() {
Println("hello go")
}
6.2.4.2 別名操作
在導⼊時,可指定包成員訪問⽅式,⽐如對包重命名,以避免同名沖突:
import (
io "fmt" //fmt改為為io
)
func main() {
io.Println("hello go") //通過io別名調用
}
6.2.4.3 _操作
有時,用戶可能需要導入一個包,但是不需要引用這個包的標識符。在這種情況,可以使用空白標識符_來重命名這個導入:
import (
_ "fmt"
)
_操作其實是引入該包,而不直接使用包里面的函數,而是調用了該包里面的init函數。
6.3 測試案例
6.3.1 測試代碼
calc.go代碼如下:
package calc
func Add(a, b int) int { //加
return a + b
}
func Minus(a, b int) int { //減
return a - b
}
func Multiply(a, b int) int { //乘
return a * b
}
func Divide(a, b int) int { //除
return a / b
}
main.go代碼如下:
package main
import (
"calc"
"fmt"
)
func main() {
a := calc.Add(1, 2)
fmt.Println("a = ", a)
}
6.3.2 GOPATH設置
6.3.2.1 windows
6.3.2.2 linux
6.3.3 編譯運行程序
6.3.4 go install的使用
設置環境變量GOBIN:
在源碼目錄,敲go install:
7. 復合類型
7.1 分類
| 類型 |
名稱 |
長度 |
默認值 |
說明 |
| pointer |
指針 |
|
nil |
|
| array |
數組 |
|
0 |
|
| slice |
切片 |
|
nil |
引⽤類型 |
| map |
字典 |
|
nil |
引⽤類型 |
| struct |
結構體 |
|
|
|
7.2 指針
指針是一個代表着某個內存地址的值。這個內存地址往往是在內存中存儲的另一個變量的值的起始位置。Go語言對指針的支持介於Java語言和C/C++語言之間,它既沒有想Java語言那樣取消了代碼對指針的直接操作的能力,也避免了C/C++語言中由於對指針的濫用而造成的安全和可靠性問題。
7.2.1 基本操作
Go語言雖然保留了指針,但與其它編程語言不同的是:
l 默認值 nil,沒有 NULL 常量
l 操作符 "&" 取變量地址, "*" 通過指針訪問目標對象
l 不支持指針運算,不支持 "->" 運算符,直接⽤ "." 訪問目標成員
func main() {
var a int = 10 //聲明一個變量,同時初始化
fmt.Printf("&a = %p\n", &a) //操作符 "&" 取變量地址
var p *int = nil //聲明一個變量p, 類型為 *int, 指針類型
p = &a
fmt.Printf("p = %p\n", p)
fmt.Printf("a = %d, *p = %d\n", a, *p)
*p = 111 //*p操作指針所指向的內存,即為a
fmt.Printf("a = %d, *p = %d\n", a, *p)
}
7.2.2 new函數
表達式new(T)將創建一個T類型的匿名變量,所做的是為T類型的新值分配並清零一塊內存空間,然后將這塊內存空間的地址作為結果返回,而這個結果就是指向這個新的T類型值的指針值,返回的指針類型為*T。
func main() {
var p1 *int
p1 = new(int) //p1為*int 類型, 指向匿名的int變量
fmt.Println("*p1 = ", *p1) //*p1 = 0
p2 := new(int) //p2為*int 類型, 指向匿名的int變量
*p2 = 111
fmt.Println("*p2 = ", *p2) //*p1 = 111
}
我們只需使用new()函數,無需擔心其內存的生命周期或怎樣將其刪除,因為Go語言的內存管理系統會幫我們打理一切。
7.2.3 指針做函數參數
func swap01(a, b int) {
a, b = b, a
fmt.Printf("swap01 a = %d, b = %d\n", a, b)
}
func swap02(x, y *int) {
*x, *y = *y, *x
}
func main() {
a := 10
b := 20
//swap01(a, b) //值傳遞
swap02(&a, &b) //變量地址傳遞
fmt.Printf("a = %d, b = %d\n", a, b)
}
7.3 數組
7.3.1 概述
數組是指一系列同一類型數據的集合。數組中包含的每個數據被稱為數組元素(element),一個數組包含的元素個數被稱為數組的長度。
數組⻓度必須是常量,且是類型的組成部分。 [2]int 和 [3]int 是不同類型。
var n int = 10
var a [n]int //err, non-constant array bound n
var b [10]int //ok
7.3.2 操作數組
數組的每個元素可以通過索引下標來訪問,索引下標的范圍是從0開始到數組長度減1的位置。
var a [10]int
for i := 0; i < 10; i++ {
a[i] = i + 1
fmt.Printf("a[%d] = %d\n", i, a[i])
}
//range具有兩個返回值,第一個返回值是元素的數組下標,第二個返回值是元素的值
for i, v := range a {
fmt.Println("a[", i, "]=", v)
}
內置函數 len(長度) 和 cap(容量) 都返回數組⻓度 (元素數量):
a := [10]int{}
fmt.Println(len(a), cap(a))//10 10
初始化:
a := [3]int{1, 2} // 未初始化元素值為 0
b := [...]int{1, 2, 3} // 通過初始化值確定數組長度
c := [5]int{2: 100, 4: 200} // 通過索引號初始化元素,未初始化元素值為 0
fmt.Println(a, b, c) //[1 2 0] [1 2 3] [0 0 100 0 200]
//支持多維數組
d := [4][2]int{{10, 11}, {20, 21}, {30, 31}, {40, 41}}
e := [...][2]int{{10, 11}, {20, 21}, {30, 31}, {40, 41}} //第二維不能寫"..."
f := [4][2]int{1: {20, 21}, 3: {40, 41}}
g := [4][2]int{1: {0: 20}, 3: {1: 41}}
fmt.Println(d, e, f, g)
相同類型的數組之間可以使用 == 或 != 進行比較,但不可以使用 < 或 >,也可以相互賦值:
a := [3]int{1, 2, 3}
b := [3]int{1, 2, 3}
c := [3]int{1, 2}
fmt.Println(a == b, b == c) //true false
var d [3]int
d = a
fmt.Println(d) //[1 2 3]
7.3.3 在函數間傳遞數組
根據內存和性能來看,在函數間傳遞數組是一個開銷很大的操作。在函數之間傳遞變量時,總是以值的方式傳遞的。如果這個變量是一個數組,意味着整個數組,不管有多長,都會完整復制,並傳遞給函數。
func modify(array [5]int) {
array[0] = 10 // 試圖修改數組的第一個元素
//In modify(), array values: [10 2 3 4 5]
fmt.Println("In modify(), array values:", array)
}
func main() {
array := [5]int{1, 2, 3, 4, 5} // 定義並初始化一個數組
modify(array) // 傳遞給一個函數,並試圖在函數體內修改這個數組內容
//In main(), array values: [1 2 3 4 5]
fmt.Println("In main(), array values:", array)
}
數組指針做函數參數:
func modify(array *[5]int) {
(*array)[0] = 10
//In modify(), array values: [10 2 3 4 5]
fmt.Println("In modify(), array values:", *array)
}
func main() {
array := [5]int{1, 2, 3, 4, 5} // 定義並初始化一個數組
modify(&array) // 數組指針
//In main(), array values: [10 2 3 4 5]
fmt.Println("In main(), array values:", array)
}
7.4 slice
7.4.1 概述
數組的長度在定義之后無法再次修改;數組是值類型,每次傳遞都將產生一份副本。顯然這種數據結構無法完全滿足開發者的真實需求。Go語言提供了數組切片(slice)來彌補數組的不足。
切片並不是數組或數組指針,它通過內部指針和相關屬性引⽤數組⽚段,以實現變⻓⽅案。
slice並不是真正意義上的動態數組,而是一個引用類型。slice總是指向一個底層array,slice的聲明也可以像array一樣,只是不需要長度。
7.4.2 切片的創建和初始化
slice和數組的區別:聲明數組時,方括號內寫明了數組的長度或使用...自動計算長度,而聲明slice時,方括號內沒有任何字符。
var s1 []int //聲明切片和聲明array一樣,只是少了長度,此為空(nil)切片
s2 := []int{}
//make([]T, length, capacity) //capacity省略,則和length的值相同
var s3 []int = make([]int, 0)
s4 := make([]int, 0, 0)
s5 := []int{1, 2, 3} //創建切片並初始化
注意:make只能創建slice、map和channel,並且返回一個有初始值(非零)。
7.4.3 切片的操作
7.4.3.1 切片截取
| 操作 |
含義 |
| s[n] |
切片s中索引位置為n的項 |
| s[:] |
從切片s的索引位置0到len(s)-1處所獲得的切片 |
| s[low:] |
從切片s的索引位置low到len(s)-1處所獲得的切片 |
| s[:high] |
從切片s的索引位置0到high處所獲得的切片,len=high |
| s[low:high] |
從切片s的索引位置low到high處所獲得的切片,len=high-low |
| s[low:high:max] |
從切片s的索引位置low到high處所獲得的切片,len=high-low,cap=max-low |
| len(s) |
切片s的長度,總是<=cap(s) |
| cap(s) |
切片s的容量,總是>=len(s) |
示例說明:
array := []int{0, 1, 2, 3, 4, 5, 6, 7, 8, 9}
| 操作 |
結果 |
len |
cap |
說明 |
| array[:6:8] |
[0 1 2 3 4 5] |
6 |
8 |
省略 low |
| array[5:] |
[5 6 7 8 9] |
5 |
5 |
省略 high、 max |
| array[:3] |
[0 1 2] |
3 |
10 |
省略 high、 max |
| array[:] |
[0 1 2 3 4 5 6 7 8 9] |
10 |
10 |
全部省略 |
7.4.3.2 切片和底層數組關系
s := []int{0, 1, 2, 3, 4, 5, 6, 7, 8, 9}
s1 := s[2:5] //[2 3 4]
s1[2] = 100 //修改切片某個元素改變底層數組
fmt.Println(s1, s) //[2 3 100] [0 1 2 3 100 5 6 7 8 9]
s2 := s1[2:6] // 新切片依舊指向原底層數組 [100 5 6 7]
s2[3] = 200
fmt.Println(s2) //[100 5 6 200]
fmt.Println(s) //[0 1 2 3 100 5 6 200 8 9]
7.4.3.3 內建函數
1) append
append函數向 slice 尾部添加數據,返回新的 slice 對象:
var s1 []int //創建nil切換
//s1 := make([]int, 0)
s1 = append(s1, 1) //追加1個元素
s1 = append(s1, 2, 3) //追加2個元素
s1 = append(s1, 4, 5, 6) //追加3個元素
fmt.Println(s1) //[1 2 3 4 5 6]
s2 := make([]int, 5)
s2 = append(s2, 6)
fmt.Println(s2) //[0 0 0 0 0 6]
s3 := []int{1, 2, 3}
s3 = append(s3, 4, 5)
fmt.Println(s3)//[1 2 3 4 5]
append函數會智能地底層數組的容量增長,一旦超過原底層數組容量,通常以2倍容量重新分配底層數組,並復制原來的數據:
func main() {
s := make([]int, 0, 1)
c := cap(s)
for i := 0; i < 50; i++ {
s = append(s, i)
if n := cap(s); n > c {
fmt.Printf("cap: %d -> %d\n", c, n)
c = n
}
}
/*
cap: 1 -> 2
cap: 2 -> 4
cap: 4 -> 8
cap: 8 -> 16
cap: 16 -> 32
cap: 32 -> 64
*/
}
2) copy
函數 copy 在兩個 slice 間復制數據,復制⻓度以 len 小的為准,兩個 slice 可指向同⼀底層數組。
data := [...]int{0, 1, 2, 3, 4, 5, 6, 7, 8, 9}
s1 := data[8:] //{8, 9}
s2 := data[:5] //{0, 1, 2, 3, 4}
copy(s2, s1) // dst:s2, src:s1
fmt.Println(s2) //[8 9 2 3 4]
fmt.Println(data) //[8 9 2 3 4 5 6 7 8 9]
7.4.4 切片做函數參數
func test(s []int) { //切片做函數參數
s[0] = -1
fmt.Println("test : ")
for i, v := range s {
fmt.Printf("s[%d]=%d, ", i, v)
//s[0]=-1, s[1]=1, s[2]=2, s[3]=3, s[4]=4, s[5]=5, s[6]=6, s[7]=7, s[8]=8, s[9]=9,
}
fmt.Println("\n")
}
func main() {
slice := []int{0, 1, 2, 3, 4, 5, 6, 7, 8, 9}
test(slice)
fmt.Println("main : ")
for i, v := range slice {
fmt.Printf("slice[%d]=%d, ", i, v)
//slice[0]=-1, slice[1]=1, slice[2]=2, slice[3]=3, slice[4]=4, slice[5]=5, slice[6]=6, slice[7]=7, slice[8]=8, slice[9]=9,
}
fmt.Println("\n")
}
7.5 map
7.5.1 概述
Go語言中的map(映射、字典)是一種內置的數據結構,它是一個無序的key—value對的集合,比如以身份證號作為唯一鍵來標識一個人的信息。
map格式為:
map[keyType]valueType
在一個map里所有的鍵都是唯一的,而且必須是支持==和!=操作符的類型,切片、函數以及包含切片的結構類型這些類型由於具有引用語義,不能作為映射的鍵,使用這些類型會造成編譯錯誤:
dict := map[ []string ]int{} //err, invalid map key type []string
map值可以是任意類型,沒有限制。map里所有鍵的數據類型必須是相同的,值也必須如何,但鍵和值的數據類型可以不相同。
注意:map是無序的,我們無法決定它的返回順序,所以,每次打印結果的順利有可能不同。
7.5.2 創建和初始化
7.5.2.1 map的創建
var m1 map[int]string //只是聲明一個map,沒有初始化, 此為空(nil)map
fmt.Println(m1 == nil) //true
//m1[1] = "mike" //err, panic: assignment to entry in nil map
//m2, m3的創建方法是等價的
m2 := map[int]string{}
m3 := make(map[int]string)
fmt.Println(m2, m3) //map[] map[]
m4 := make(map[int]string, 10) //第2個參數指定容量
fmt.Println(m4) //map[]
7.5.2.2 初始化
//1、定義同時初始化
var m1 map[int]string = map[int]string{1: "mike", 2: "yoyo"}
fmt.Println(m1) //map[1:mike 2:yoyo]
//2、自動推導類型 :=
m2 := map[int]string{1: "mike", 2: "yoyo"}
fmt.Println(m2)
7.5.3 常用操作
7.5.3.1 賦值
m1 := map[int]string{1: "mike", 2: "yoyo"}
m1[1] = "xxx" //修改
m1[3] = "lily" //追加, go底層會自動為map分配空間
fmt.Println(m1) //map[1:xxx 2:yoyo 3:lily]
m2 := make(map[int]string, 10) //創建map
m2[0] = "aaa"
m2[1] = "bbb"
fmt.Println(m2) //map[0:aaa 1:bbb]
fmt.Println(m2[0], m2[1]) //aaa bbb
7.5.3.2 遍歷
m1 := map[int]string{1: "mike", 2: "yoyo"}
//迭代遍歷1,第一個返回值是key,第二個返回值是value
for k, v := range m1 {
fmt.Printf("%d ----> %s\n", k, v)
//1 ----> mike
//2 ----> yoyo
}
//迭代遍歷2,第一個返回值是key,第二個返回值是value(可省略)
for k := range m1 {
fmt.Printf("%d ----> %s\n", k, m1[k])
//1 ----> mike
//2 ----> yoyo
}
//判斷某個key所對應的value是否存在, 第一個返回值是value(如果存在的話)
value, ok := m1[1]
fmt.Println("value = ", value, ", ok = ", ok) //value = mike , ok = true
value2, ok2 := m1[3]
fmt.Println("value2 = ", value2, ", ok2 = ", ok2) //value2 = , ok2 = false
7.5.3.3 刪除
m1 := map[int]string{1: "mike", 2: "yoyo", 3: "lily"}
//迭代遍歷1,第一個返回值是key,第二個返回值是value
for k, v := range m1 {
fmt.Printf("%d ----> %s\n", k, v)
//1 ----> mike
//2 ----> yoyo
//3 ----> lily
}
delete(m1, 2) //刪除key值為3的map
for k, v := range m1 {
fmt.Printf("%d ----> %s\n", k, v)
//1 ----> mike
//3 ----> lily
}
7.5.4 map做函數參數
在函數間傳遞映射並不會制造出該映射的一個副本,不是值傳遞,而是引用傳遞:
func DeleteMap(m map[int]string, key int) {
delete(m, key) //刪除key值為3的map
for k, v := range m {
fmt.Printf("len(m)=%d, %d ----> %s\n", len(m), k, v)
//len(m)=2, 1 ----> mike
//len(m)=2, 3 ----> lily
}
}
func main() {
m := map[int]string{1: "mike", 2: "yoyo", 3: "lily"}
DeleteMap(m, 2) //刪除key值為3的map
for k, v := range m {
fmt.Printf("len(m)=%d, %d ----> %s\n", len(m), k, v)
//len(m)=2, 1 ----> mike
//len(m)=2, 3 ----> lily
}
}
7.6 結構體
7.6.1 結構體類型
有時我們需要將不同類型的數據組合成一個有機的整體,如:一個學生有學號/姓名/性別/年齡/地址等屬性。顯然單獨定義以上變量比較繁瑣,數據不便於管理。
結構體是一種聚合的數據類型,它是由一系列具有相同類型或不同類型的數據構成的數據集合。每個數據稱為結構體的成員。
7.6.2 結構體初始化
7.6.2.1 普通變量
type Student struct {
id int
name string
sex byte
age int
addr string
}
func main() {
//1、順序初始化,必須每個成員都初始化
var s1 Student = Student{1, "mike", 'm', 18, "sz"}
s2 := Student{2, "yoyo", 'f', 20, "sz"}
//s3 := Student{2, "tom", 'm', 20} //err, too few values in struct initializer
//2、指定初始化某個成員,沒有初始化的成員為零值
s4 := Student{id: 2, name: "lily"}
}
7.6.2.2 指針變量
type Student struct {
id int
name string
sex byte
age int
addr string
}
func main() {
var s5 *Student = &Student{3, "xiaoming", 'm', 16, "bj"}
s6 := &Student{4, "rocco", 'm', 3, "sh"}
}
7.6.3 結構體成員的使用
7.6.3.1 普通變量
//===============結構體變量為普通變量
//1、打印成員
var s1 Student = Student{1, "mike", 'm', 18, "sz"}
//結果:id = 1, name = mike, sex = m, age = 18, addr = sz
fmt.Printf("id = %d, name = %s, sex = %c, age = %d, addr = %s\n", s1.id, s1.name, s1.sex, s1.age, s1.addr)
//2、成員變量賦值
var s2 Student
s2.id = 2
s2.name = "yoyo"
s2.sex = 'f'
s2.age = 16
s2.addr = "guangzhou"
fmt.Println(s2) //{2 yoyo 102 16 guangzhou}
7.6.3.2 指針變量
//===============結構體變量為指針變量
//3、先分配空間,再賦值
s3 := new(Student)
s3.id = 3
s3.name = "xxx"
fmt.Println(s3) //&{3 xxx 0 0 }
//4、普通變量和指針變量類型打印
var s4 Student = Student{4, "yyy", 'm', 18, "sz"}
fmt.Printf("s4 = %v, &s4 = %v\n", s4, &s4) //s4 = {4 yyy 109 18 sz}, &s4 = &{4 yyy 109 18 sz}
var p *Student = &s4
//p.成員 和(*p).成員 操作是等價的
p.id = 5
(*p).name = "zzz"
fmt.Println(p, *p, s4) //&{5 zzz 109 18 sz} {5 zzz 109 18 sz} {5 zzz 109 18 sz}
7.6.4 結構體比較
如果結構體的全部成員都是可以比較的,那么結構體也是可以比較的,那樣的話兩個結構體將可以使用 == 或 != 運算符進行比較,但不支持 > 或 < 。
func main() {
s1 := Student{1, "mike", 'm', 18, "sz"}
s2 := Student{1, "mike", 'm', 18, "sz"}
fmt.Println("s1 == s2", s1 == s2) //s1 == s2 true
fmt.Println("s1 != s2", s1 != s2) //s1 != s2 false
}
7.6.5 結構體作為函數參數
7.6.5.1 值傳遞
func printStudentValue(tmp Student) {
tmp.id = 250
//printStudentValue tmp = {250 mike 109 18 sz}
fmt.Println("printStudentValue tmp = ", tmp)
}
func main() {
var s Student = Student{1, "mike", 'm', 18, "sz"}
printStudentValue(s) //值傳遞,形參的修改不會影響到實參
fmt.Println("main s = ", s) //main s = {1 mike 109 18 sz}
}
7.6.5.2 引用傳遞
func printStudentPointer(p *Student) {
p.id = 250
//printStudentPointer p = &{250 mike 109 18 sz}
fmt.Println("printStudentPointer p = ", p)
}
func main() {
var s Student = Student{1, "mike", 'm', 18, "sz"}
printStudentPointer(&s) //引用(地址)傳遞,形參的修改會影響到實參
fmt.Println("main s = ", s) //main s = {250 mike 109 18 sz}
}
7.6.6 可見性
Go語言對關鍵字的增加非常吝嗇,其中沒有private、 protected、 public這樣的關鍵字。
要使某個符號對其他包(package)可見(即可以訪問),需要將該符號定義為以大寫字母
開頭。
目錄結構:
test.go示例代碼如下:
//test.go
package test
//student01只能在本文件件引用,因為首字母小寫
type student01 struct {
Id int
Name string
}
//Student02可以在任意文件引用,因為首字母大寫
type Student02 struct {
Id int
name string
}
main.go示例代碼如下:
// main.go
package main
import (
"fmt"
"test" //導入test包
)
func main() {
//s1 := test.student01{1, "mike"} //err, cannot refer to unexported name test.student01
//err, implicit assignment of unexported field 'name' in test.Student02 literal
//s2 := test.Student02{2, "yoyo"}
//fmt.Println(s2)
var s3 test.Student02 //聲明變量
s3.Id = 1 //ok
//s3.name = "mike" //err, s3.name undefined (cannot refer to unexported field or method name)
fmt.Println(s3)
}
8. 面向對象編程
8.1 概述
對於面向對象編程的支持Go 語言設計得非常簡潔而優雅。因為, Go語言並沒有沿襲傳統面向對象編程中的諸多概念,比如繼承(不支持繼承,盡管匿名字段的內存布局和行為類似繼承,但它並不是繼承)、虛函數、構造函數和析構函數、隱藏的this指針等。
盡管Go語言中沒有封裝、繼承、多態這些概念,但同樣通過別的方式實現這些特性:
l 封裝:通過方法實現
l 繼承:通過匿名字段實現
l 多態:通過接口實現
8.2 匿名組合
8.2.1 匿名字段
一般情況下,定義結構體的時候是字段名與其類型一一對應,實際上Go支持只提供類型,而不寫字段名的方式,也就是匿名字段,也稱為嵌入字段。
當匿名字段也是一個結構體的時候,那么這個結構體所擁有的全部字段都被隱式地引入了當前定義的這個結構體。
//人
type Person struct {
name string
sex byte
age int
}
//學生
type Student struct {
Person // 匿名字段,那么默認Student就包含了Person的所有字段
id int
addr string
}
8.2.2 初始化
//人
type Person struct {
name string
sex byte
age int
}
//學生
type Student struct {
Person // 匿名字段,那么默認Student就包含了Person的所有字段
id int
addr string
}
func main() {
//順序初始化
s1 := Student{Person{"mike", 'm', 18}, 1, "sz"}
//s1 = {Person:{name:mike sex:109 age:18} id:1 addr:sz}
fmt.Printf("s1 = %+v\n", s1)
//s2 := Student{"mike", 'm', 18, 1, "sz"} //err
//部分成員初始化1
s3 := Student{Person: Person{"lily", 'f', 19}, id: 2}
//s3 = {Person:{name:lily sex:102 age:19} id:2 addr:}
fmt.Printf("s3 = %+v\n", s3)
//部分成員初始化2
s4 := Student{Person: Person{name: "tom"}, id: 3}
//s4 = {Person:{name:tom sex:0 age:0} id:3 addr:}
fmt.Printf("s4 = %+v\n", s4)
}
8.2.3 成員的操作
var s1 Student //變量聲明
//給成員賦值
s1.name = "mike" //等價於 s1.Person.name = "mike"
s1.sex = 'm'
s1.age = 18
s1.id = 1
s1.addr = "sz"
fmt.Println(s1) //{{mike 109 18} 1 sz}
var s2 Student //變量聲明
s2.Person = Person{"lily", 'f', 19}
s2.id = 2
s2.addr = "bj"
fmt.Println(s2) //{{lily 102 19} 2 bj}
8.2.4 同名字段
//人
type Person struct {
name string
sex byte
age int
}
//學生
type Student struct {
Person // 匿名字段,那么默認Student就包含了Person的所有字段
id int
addr string
name string //和Person中的name同名
}
func main() {
var s Student //變量聲明
//給Student的name,還是給Person賦值?
s.name = "mike"
//{Person:{name: sex:0 age:0} id:0 addr: name:mike}
fmt.Printf("%+v\n", s)
//默認只會給最外層的成員賦值
//給匿名同名成員賦值,需要顯示調用
s.Person.name = "yoyo"
//Person:{name:yoyo sex:0 age:0} id:0 addr: name:mike}
fmt.Printf("%+v\n", s)
}
8.2.5 其它匿名字段
8.2.5.1 非結構體類型
所有的內置類型和自定義類型都是可以作為匿名字段的:
type mystr string //自定義類型
type Person struct {
name string
sex byte
age int
}
type Student struct {
Person // 匿名字段,結構體類型
int // 匿名字段,內置類型
mystr // 匿名字段,自定義類型
}
func main() {
//初始化
s1 := Student{Person{"mike", 'm', 18}, 1, "bj"}
//{Person:{name:mike sex:109 age:18} int:1 mystr:bj}
fmt.Printf("%+v\n", s1)
//成員的操作,打印結果:mike, m, 18, 1, bj
fmt.Printf("%s, %c, %d, %d, %s\n", s1.name, s1.sex, s1.age, s1.int, s1.mystr)
}
8.2.5.2 結構體指針類型
type Person struct { //人
name string
sex byte
age int
}
type Student struct { //學生
*Person // 匿名字段,結構體指針類型
id int
addr string
}
func main() {
//初始化
s1 := Student{&Person{"mike", 'm', 18}, 1, "bj"}
//{Person:0xc0420023e0 id:1 addr:bj}
fmt.Printf("%+v\n", s1)
//mike, m, 18
fmt.Printf("%s, %c, %d\n", s1.name, s1.sex, s1.age)
//聲明變量
var s2 Student
s2.Person = new(Person) //分配空間
s2.name = "yoyo"
s2.sex = 'f'
s2.age = 20
s2.id = 2
s2.addr = "sz"
//yoyo 102 20 2 20
fmt.Println(s2.name, s2.sex, s2.age, s2.id, s2.age)
}
8.3 方法
8.3.1 概述
在面向對象編程中,一個對象其實也就是一個簡單的值或者一個變量,在這個對象中會包含一些函數,這種帶有接收者的函數,我們稱為方法(method)。 本質上,一個方法則是一個和特殊類型關聯的函數。
一個面向對象的程序會用方法來表達其屬性和對應的操作,這樣使用這個對象的用戶就不需要直接去操作對象,而是借助方法來做這些事情。
在Go語言中,可以給任意自定義類型(包括內置類型,但不包括指針類型)添加相應的方法。
⽅法總是綁定對象實例,並隱式將實例作為第⼀實參 (receiver),方法的語法如下:
func (receiver ReceiverType) funcName(parameters) (results)
l 參數 receiver 可任意命名。如⽅法中未曾使⽤,可省略參數名。
l 參數 receiver 類型可以是 T 或 *T。基類型 T 不能是接⼝或指針。
l 不支持重載方法,也就是說,不能定義名字相同但是不同參數的方法。
8.3.2 為類型添加方法
8.3.2.1 基礎類型作為接收者
type MyInt int //自定義類型,給int改名為MyInt
//在函數定義時,在其名字之前放上一個變量,即是一個方法
func (a MyInt) Add(b MyInt) MyInt { //面向對象
return a + b
}
//傳統方式的定義
func Add(a, b MyInt) MyInt { //面向過程
return a + b
}
func main() {
var a MyInt = 1
var b MyInt = 1
//調用func (a MyInt) Add(b MyInt)
fmt.Println("a.Add(b) = ", a.Add(b)) //a.Add(b) = 2
//調用func Add(a, b MyInt)
fmt.Println("Add(a, b) = ", Add(a, b)) //Add(a, b) = 2
}
通過上面的例子可以看出,面向對象只是換了一種語法形式來表達。方法是函數的語法糖,因為receiver其實就是方法所接收的第1個參數。
注意:雖然方法的名字一模一樣,但是如果接收者不一樣,那么方法就不一樣。
8.3.2.2 結構體作為接收者
方法里面可以訪問接收者的字段,調用方法通過點( . )訪問,就像struct里面訪問字段一樣:
type Person struct {
name string
sex byte
age int
}
func (p Person) PrintInfo() { //給Person添加方法
fmt.Println(p.name, p.sex, p.age)
}
func main() {
p := Person{"mike", 'm', 18} //初始化
p.PrintInfo() //調用func (p Person) PrintInfo()
}
8.3.3 值語義和引用語義
type Person struct {
name string
sex byte
age int
}
//指針作為接收者,引用語義
func (p *Person) SetInfoPointer() {
//給成員賦值
(*p).name = "yoyo"
p.sex = 'f'
p.age = 22
}
//值作為接收者,值語義
func (p Person) SetInfoValue() {
//給成員賦值
p.name = "yoyo"
p.sex = 'f'
p.age = 22
}
func main() {
//指針作為接收者,引用語義
p1 := Person{"mike", 'm', 18} //初始化
fmt.Println("函數調用前 = ", p1) //函數調用前 = {mike 109 18}
(&p1).SetInfoPointer()
fmt.Println("函數調用后 = ", p1) //函數調用后 = {yoyo 102 22}
fmt.Println("==========================")
p2 := Person{"mike", 'm', 18} //初始化
//值作為接收者,值語義
fmt.Println("函數調用前 = ", p2) //函數調用前 = {mike 109 18}
p2.SetInfoValue()
fmt.Println("函數調用后 = ", p2) //函數調用后 = {mike 109 18}
}
8.3.4 方法集
類型的方法集是指可以被該類型的值調用的所有方法的集合。
用實例實例 value 和 pointer 調用方法(含匿名字段)不受⽅法集約束,編譯器編總是查找全部方法,並自動轉換 receiver 實參。
8.3.4.1 類型 *T 方法集
一個指向自定義類型的值的指針,它的方法集由該類型定義的所有方法組成,無論這些方法接受的是一個值還是一個指針。
如果在指針上調用一個接受值的方法,Go語言會聰明地將該指針解引用,並將指針所指的底層值作為方法的接收者。
類型 *T ⽅法集包含全部 receiver T + *T ⽅法:
type Person struct {
name string
sex byte
age int
}
//指針作為接收者,引用語義
func (p *Person) SetInfoPointer() {
(*p).name = "yoyo"
p.sex = 'f'
p.age = 22
}
//值作為接收者,值語義
func (p Person) SetInfoValue() {
p.name = "xxx"
p.sex = 'm'
p.age = 33
}
func main() {
//p 為指針類型
var p *Person = &Person{"mike", 'm', 18}
p.SetInfoPointer() //func (p) SetInfoPointer()
p.SetInfoValue() //func (*p) SetInfoValue()
(*p).SetInfoValue() //func (*p) SetInfoValue()
}
8.3.4.2 類型 T 方法集
一個自定義類型值的方法集則由為該類型定義的接收者類型為值類型的方法組成,但是不包含那些接收者類型為指針的方法。
但這種限制通常並不像這里所說的那樣,因為如果我們只有一個值,仍然可以調用一個接收者為指針類型的方法,這可以借助於Go語言傳值的地址能力實現。
type Person struct {
name string
sex byte
age int
}
//指針作為接收者,引用語義
func (p *Person) SetInfoPointer() {
(*p).name = "yoyo"
p.sex = 'f'
p.age = 22
}
//值作為接收者,值語義
func (p Person) SetInfoValue() {
p.name = "xxx"
p.sex = 'm'
p.age = 33
}
func main() {
//p 為普通值類型
var p Person = Person{"mike", 'm', 18}
(&p).SetInfoPointer() //func (&p) SetInfoPointer()
p.SetInfoPointer() //func (&p) SetInfoPointer()
p.SetInfoValue() //func (p) SetInfoValue()
(&p).SetInfoValue() //func (*&p) SetInfoValue()
}
8.3.5 匿名字段
8.3.5.1 方法的繼承
如果匿名字段實現了一個方法,那么包含這個匿名字段的struct也能調用該方法。
type Person struct {
name string
sex byte
age int
}
//Person定義了方法
func (p *Person) PrintInfo() {
fmt.Printf("%s,%c,%d\n", p.name, p.sex, p.age)
}
type Student struct {
Person // 匿名字段,那么Student包含了Person的所有字段
id int
addr string
}
func main() {
p := Person{"mike", 'm', 18}
p.PrintInfo()
s := Student{Person{"yoyo", 'f', 20}, 2, "sz"}
s.PrintInfo()
}
8.3.5.2 方法的重寫
type Person struct {
name string
sex byte
age int
}
//Person定義了方法
func (p *Person) PrintInfo() {
fmt.Printf("Person: %s,%c,%d\n", p.name, p.sex, p.age)
}
type Student struct {
Person // 匿名字段,那么Student包含了Person的所有字段
id int
addr string
}
//Student定義了方法
func (s *Student) PrintInfo() {
fmt.Printf("Student:%s,%c,%d\n", s.name, s.sex, s.age)
}
func main() {
p := Person{"mike", 'm', 18}
p.PrintInfo() //Person: mike,m,18
s := Student{Person{"yoyo", 'f', 20}, 2, "sz"}
s.PrintInfo() //Student:yoyo,f,20
s.Person.PrintInfo() //Person: yoyo,f,20
}
8.3.6 表達式
類似於我們可以對函數進行賦值和傳遞一樣,方法也可以進行賦值和傳遞。
根據調用者不同,方法分為兩種表現形式:方法值和方法表達式。兩者都可像普通函數那樣賦值和傳參,區別在於方法值綁定實例,⽽方法表達式則須顯式傳參。
8.3.6.1 方法值
type Person struct {
name string
sex byte
age int
}
func (p *Person) PrintInfoPointer() {
fmt.Printf("%p, %v\n", p, p)
}
func (p Person) PrintInfoValue() {
fmt.Printf("%p, %v\n", &p, p)
}
func main() {
p := Person{"mike", 'm', 18}
p.PrintInfoPointer() //0xc0420023e0, &{mike 109 18}
pFunc1 := p.PrintInfoPointer //方法值,隱式傳遞 receiver
pFunc1() //0xc0420023e0, &{mike 109 18}
pFunc2 := p.PrintInfoValue
pFunc2() //0xc042048420, {mike 109 18}
}
8.3.6.2 方法表達式
type Person struct {
name string
sex byte
age int
}
func (p *Person) PrintInfoPointer() {
fmt.Printf("%p, %v\n", p, p)
}
func (p Person) PrintInfoValue() {
fmt.Printf("%p, %v\n", &p, p)
}
func main() {
p := Person{"mike", 'm', 18}
p.PrintInfoPointer() //0xc0420023e0, &{mike 109 18}
//方法表達式, 須顯式傳參
//func pFunc1(p *Person))
pFunc1 := (*Person).PrintInfoPointer
pFunc1(&p) //0xc0420023e0, &{mike 109 18}
pFunc2 := Person.PrintInfoValue
pFunc2(p) //0xc042002460, {mike 109 18}
}
8.4 接口
8.4.1 概述
在Go語言中,接口(interface)是一個自定義類型,接口類型具體描述了一系列方法的集合。
接口類型是一種抽象的類型,它不會暴露出它所代表的對象的內部值的結構和這個對象支持的基礎操作的集合,它們只會展示出它們自己的方法。因此接口類型不能將其實例化。
Go通過接口實現了鴨子類型(duck-typing):“當看到一只鳥走起來像鴨子、游泳起來像鴨子、叫起來也像鴨子,那么這只鳥就可以被稱為鴨子”。我們並不關心對象是什么類型,到底是不是鴨子,只關心行為。
8.4.2 接口的使用
8.4.2.1 接口定義
type Humaner interface {
SayHi()
}
l 接⼝命名習慣以 er 結尾
l 接口只有方法聲明,沒有實現,沒有數據字段
l 接口可以匿名嵌入其它接口,或嵌入到結構中
8.4.2.2 接口實現
接口是用來定義行為的類型。這些被定義的行為不由接口直接實現,而是通過方法由用戶定義的類型實現,一個實現了這些方法的具體類型是這個接口類型的實例。
如果用戶定義的類型實現了某個接口類型聲明的一組方法,那么這個用戶定義的類型的值就可以賦給這個接口類型的值。這個賦值會把用戶定義的類型的值存入接口類型的值。
type Humaner interface {
SayHi()
}
type Student struct { //學生
name string
score float64
}
//Student實現SayHi()方法
func (s *Student) SayHi() {
fmt.Printf("Student[%s, %f] say hi!!\n", s.name, s.score)
}
type Teacher struct { //老師
name string
group string
}
//Teacher實現SayHi()方法
func (t *Teacher) SayHi() {
fmt.Printf("Teacher[%s, %s] say hi!!\n", t.name, t.group)
}
type MyStr string
//MyStr實現SayHi()方法
func (str MyStr) SayHi() {
fmt.Printf("MyStr[%s] say hi!!\n", str)
}
//普通函數,參數為Humaner類型的變量i
func WhoSayHi(i Humaner) {
i.SayHi()
}
func main() {
s := &Student{"mike", 88.88}
t := &Teacher{"yoyo", "Go語言"}
var tmp MyStr = "測試"
s.SayHi() //Student[mike, 88.880000] say hi!!
t.SayHi() //Teacher[yoyo, Go語言] say hi!!
tmp.SayHi() //MyStr[測試] say hi!!
//多態,調用同一接口,不同表現
WhoSayHi(s) //Student[mike, 88.880000] say hi!!
WhoSayHi(t) //Teacher[yoyo, Go語言] say hi!!
WhoSayHi(tmp) //MyStr[測試] say hi!!
x := make([]Humaner, 3)
//這三個都是不同類型的元素,但是他們實現了interface同一個接口
x[0], x[1], x[2] = s, t, tmp
for _, value := range x {
value.SayHi()
}
/*
Student[mike, 88.880000] say hi!!
Teacher[yoyo, Go語言] say hi!!
MyStr[測試] say hi!!
*/
}
通過上面的代碼,你會發現接口就是一組抽象方法的集合,它必須由其他非接口類型實現,而不能自我實現。
8.4.3 接口組合
8.4.3.1 接口嵌入
如果一個interface1作為interface2的一個嵌入字段,那么interface2隱式的包含了interface1里面的方法。
type Humaner interface {
SayHi()
}
type Personer interface {
Humaner //這里想寫了SayHi()一樣
Sing(lyrics string)
}
type Student struct { //學生
name string
score float64
}
//Student實現SayHi()方法
func (s *Student) SayHi() {
fmt.Printf("Student[%s, %f] say hi!!\n", s.name, s.score)
}
//Student實現Sing()方法
func (s *Student) Sing(lyrics string) {
fmt.Printf("Student sing[%s]!!\n", lyrics)
}
func main() {
s := &Student{"mike", 88.88}
var i2 Personer
i2 = s
i2.SayHi() //Student[mike, 88.880000] say hi!!
i2.Sing("學生哥") //Student sing[學生哥]!!
}
8.4.3.2 接口轉換
超集接⼝對象可轉換為⼦集接⼝,反之出錯:
type Humaner interface {
SayHi()
}
type Personer interface {
Humaner //這里像寫了SayHi()一樣
Sing(lyrics string)
}
type Student struct { //學生
name string
score float64
}
//Student實現SayHi()方法
func (s *Student) SayHi() {
fmt.Printf("Student[%s, %f] say hi!!\n", s.name, s.score)
}
//Student實現Sing()方法
func (s *Student) Sing(lyrics string) {
fmt.Printf("Student sing[%s]!!\n", lyrics)
}
func main() {
//var i1 Humaner = &Student{"mike", 88.88}
//var i2 Personer = i1 //err
//Personer為超集,Humaner為子集
var i1 Personer = &Student{"mike", 88.88}
var i2 Humaner = i1
i2.SayHi() //Student[mike, 88.880000] say hi!!
}
8.4.4 空接口
空接口(interface{})不包含任何的方法,正因為如此,所有的類型都實現了空接口,因此空接口可以存儲任意類型的數值。它有點類似於C語言的void *類型。
var v1 interface{} = 1 // 將int類型賦值給interface{}
var v2 interface{} = "abc" // 將string類型賦值給interface{}
var v3 interface{} = &v2 // 將*interface{}類型賦值給interface{}
var v4 interface{} = struct{ X int }{1}
var v5 interface{} = &struct{ X int }{1}
當函數可以接受任意的對象實例時,我們會將其聲明為interface{},最典型的例子是標准庫fmt中PrintXXX系列的函數,例如:
func Printf(fmt string, args ...interface{})
func Println(args ...interface{})
8.4.5 類型查詢
我們知道interface的變量里面可以存儲任意類型的數值(該類型實現了interface)。那么我們怎么反向知道這個變量里面實際保存了的是哪個類型的對象呢?目前常用的有兩種方法:
l comma-ok斷言
l switch測試
8.4.5.1 comma-ok斷言
Go語言里面有一個語法,可以直接判斷是否是該類型的變量: value, ok = element.(T),這里value就是變量的值,ok是一個bool類型,element是interface變量,T是斷言的類型。
如果element里面確實存儲了T類型的數值,那么ok返回true,否則返回false。
示例代碼:
type Element interface{}
type Person struct {
name string
age int
}
func main() {
list := make([]Element, 3)
list[0] = 1 // an int
list[1] = "Hello" // a string
list[2] = Person{"mike", 18}
for index, element := range list {
if value, ok := element.(int); ok {
fmt.Printf("list[%d] is an int and its value is %d\n", index, value)
} else if value, ok := element.(string); ok {
fmt.Printf("list[%d] is a string and its value is %s\n", index, value)
} else if value, ok := element.(Person); ok {
fmt.Printf("list[%d] is a Person and its value is [%s, %d]\n", index, value.name, value.age)
} else {
fmt.Printf("list[%d] is of a different type\n", index)
}
}
/* 打印結果:
list[0] is an int and its value is 1
list[1] is a string and its value is Hello
list[2] is a Person and its value is [mike, 18]
*/
}
8.4.5.2 switch測試
type Element interface{}
type Person struct {
name string
age int
}
func main() {
list := make([]Element, 3)
list[0] = 1 //an int
list[1] = "Hello" //a string
list[2] = Person{"mike", 18}
for index, element := range list {
switch value := element.(type) {
case int:
fmt.Printf("list[%d] is an int and its value is %d\n", index, value)
case string:
fmt.Printf("list[%d] is a string and its value is %s\n", index, value)
case Person:
fmt.Printf("list[%d] is a Person and its value is [%s, %d]\n", index, value.name, value.age)
default:
fmt.Println("list[%d] is of a different type", index)
}
}
}
9. 異常處理
9.1 error接口
Go語言引入了一個關於錯誤處理的標准模式,即error接口,它是Go語言內建的接口類型,該接口的定義如下:
type error interface {
Error() string
}
Go語言的標准庫代碼包errors為用戶提供如下方法:
package errors
type errorString struct {
text string
}
func New(text string) error {
return &errorString{text}
}
func (e *errorString) Error() string {
return e.text
}
另一個可以生成error類型值的方法是調用fmt包中的Errorf函數:
package fmt
import "errors"
func Errorf(format string, args ...interface{}) error {
return errors.New(Sprintf(format, args...))
}
示例代碼:
import (
"errors"
"fmt"
)
func main() {
var err1 error = errors.New("a normal err1")
fmt.Println(err1) //a normal err1
var err2 error = fmt.Errorf("%s", "a normal err2")
fmt.Println(err2) //a normal err2
}
函數通常在最后的返回值中返回錯誤信息:
import (
"errors"
"fmt"
)
func Divide(a, b float64) (result float64, err error) {
if b == 0 {
result = 0.0
err = errors.New("runtime error: divide by zero")
return
}
result = a / b
err = nil
return
}
func main() {
r, err := Divide(10.0, 0)
if err != nil {
fmt.Println(err) //錯誤處理 runtime error: divide by zero
} else {
fmt.Println(r) // 使用返回值
}
}
9.2 panic
在通常情況下,向程序使用方報告錯誤狀態的方式可以是返回一個額外的error類型值。
但是,當遇到不可恢復的錯誤狀態的時候,如數組訪問越界、空指針引用等,這些運行時錯誤會引起painc異常。這時,上述錯誤處理方式顯然就不適合了。反過來講,在一般情況下,我們不應通過調用panic函數來報告普通的錯誤,而應該只把它作為報告致命錯誤的一種方式。當某些不應該發生的場景發生時,我們就應該調用panic。
一般而言,當panic異常發生時,程序會中斷運行,並立即執行在該goroutine(可以先理解成線程,在中被延遲的函數(defer 機制)。隨后,程序崩潰並輸出日志信息。日志信息包括panic value和函數調用的堆棧跟蹤信息。
不是所有的panic異常都來自運行時,直接調用內置的panic函數也會引發panic異常;panic函數接受任何值作為參數。
func panic(v interface{})
調用panic函數引發的panic異常:
func TestA() {
fmt.Println("func TestA()")
}
func TestB() {
panic("func TestB(): panic")
}
func TestC() {
fmt.Println("func TestC()")
}
func main() {
TestA()
TestB()//TestB()發生異常,中斷程序
TestC()
}
運行結果:
內置的panic函數引發的panic異常:
func TestA() {
fmt.Println("func TestA()")
}
func TestB(x int) {
var a [10]int
a[x] = 222 //x值為11時,數組越界
}
func TestC() {
fmt.Println("func TestC()")
}
func main() {
TestA()
TestB(11)//TestB()發生異常,中斷程序
TestC()
}
運行結果:
9.3 recover
運行時panic異常一旦被引發就會導致程序崩潰。這當然不是我們願意看到的,因為誰也不能保證程序不會發生任何運行時錯誤。
不過,Go語言為我們提供了專用於“攔截”運行時panic的內建函數——recover。它可以是當前的程序從運行時panic的狀態中恢復並重新獲得流程控制權。
func recover() interface{}
注意:recover只有在defer調用的函數中有效。
如果調用了內置函數recover,並且定義該defer語句的函數發生了panic異常,recover會使程序從panic中恢復,並返回panic value。導致panic異常的函數不會繼續運行,但能正常返回。在未發生panic時調用recover,recover會返回nil。
示例代碼:
func TestA() {
fmt.Println("func TestA()")
}
func TestB() (err error) {
defer func() { //在發生異常時,設置恢復
if x := recover(); x != nil {
//panic value被附加到錯誤信息中;
//並用err變量接收錯誤信息,返回給調用者。
err = fmt.Errorf("internal error: %v", x)
}
}()
panic("func TestB(): panic")
}
func TestC() {
fmt.Println("func TestC()")
}
func main() {
TestA()
err := TestB()
fmt.Println(err)
TestC()
/*
運行結果:
func TestA()
internal error: func TestB(): panic
func TestC()
*/
}
延遲調用中引發的錯誤,可被后續延遲調用捕獲,但僅最后⼀個錯誤可被捕獲:
func test() {
defer func() {
fmt.Println(recover())
}()
defer func() {
panic("defer panic")
}()
panic("test panic")
}
func main() {
test()
//運行結果:defer panic
}
10. 文本文件處理
10.1 字符串處理
字符串在開發中經常用到,包括用戶的輸入,數據庫讀取的數據等,我們經常需要對字符串進行分割、連接、轉換等操作,我們可以通過Go標准庫中的strings和strconv兩個包中的函數進行相應的操作。
10.1.1 字符串操作
下面這些函數來自於strings包,這里介紹一些我平常經常用到的函數,更詳細的請參考官方的文檔。
10.1.1.1 Contains
func Contains(s, substr string) bool
功能:字符串s中是否包含substr,返回bool值
示例代碼:
fmt.Println(strings.Contains("seafood", "foo"))
fmt.Println(strings.Contains("seafood", "bar"))
fmt.Println(strings.Contains("seafood", ""))
fmt.Println(strings.Contains("", ""))
//運行結果:
//true
//false
//true
//true
10.1.1.2 Join
func Join(a []string, sep string) string
功能:字符串鏈接,把slice a通過sep鏈接起來
示例代碼:
s := []string{"foo", "bar", "baz"}
fmt.Println(strings.Join(s, ", "))
//運行結果:foo, bar, baz
10.1.1.3 Index
func Index(s, sep string) int
功能:在字符串s中查找sep所在的位置,返回位置值,找不到返回-1
示例代碼:
fmt.Println(strings.Index("chicken", "ken"))
fmt.Println(strings.Index("chicken", "dmr"))
//運行結果:
// 4
// -1
10.1.1.4 Repeat
func Repeat(s string, count int) string
功能:重復s字符串count次,最后返回重復的字符串
示例代碼:
fmt.Println("ba" + strings.Repeat("na", 2))
//運行結果:banana
10.1.1.5 Replace
func Replace(s, old, new string, n int) string
功能:在s字符串中,把old字符串替換為new字符串,n表示替換的次數,小於0表示全部替換
示例代碼:
fmt.Println(strings.Replace("oink oink oink", "k", "ky", 2))
fmt.Println(strings.Replace("oink oink oink", "oink", "moo", -1))
//運行結果:
//oinky oinky oink
//moo moo moo
10.1.1.6 Split
func Split(s, sep string) []string
功能:把s字符串按照sep分割,返回slice
示例代碼:
fmt.Printf("%q\n", strings.Split("a,b,c", ","))
fmt.Printf("%q\n", strings.Split("a man a plan a canal panama", "a "))
fmt.Printf("%q\n", strings.Split(" xyz ", ""))
fmt.Printf("%q\n", strings.Split("", "Bernardo O'Higgins"))
//運行結果:
//["a" "b" "c"]
//["" "man " "plan " "canal panama"]
//[" " "x" "y" "z" " "]
//[""]
10.1.1.7 Trim
func Trim(s string, cutset string) string
功能:在s字符串的頭部和尾部去除cutset指定的字符串
示例代碼:
fmt.Printf("[%q]", strings.Trim(" !!! Achtung !!! ", "! "))
//運行結果:["Achtung"]
10.1.1.8 Fields
func Fields(s string) []string
功能:去除s字符串的空格符,並且按照空格分割返回slice
示例代碼:
fmt.Printf("Fields are: %q", strings.Fields(" foo bar baz "))
//運行結果:Fields are: ["foo" "bar" "baz"]
10.1.2 字符串轉換
字符串轉化的函數在strconv中,如下也只是列出一些常用的。
10.1.2.1 Append
Append 系列函數將整數等轉換為字符串后,添加到現有的字節數組中。
示例代碼:
str := make([]byte, 0, 100)
str = strconv.AppendInt(str, 4567, 10) //以10進制方式追加
str = strconv.AppendBool(str, false)
str = strconv.AppendQuote(str, "abcdefg")
str = strconv.AppendQuoteRune(str, '單')
fmt.Println(string(str)) //4567false"abcdefg"'單'
10.1.2.2 Format
Format 系列函數把其他類型的轉換為字符串。
示例代碼:
a := strconv.FormatBool(false)
b := strconv.FormatInt(1234, 10)
c := strconv.FormatUint(12345, 10)
d := strconv.Itoa(1023)
fmt.Println(a, b, c, d) //false 1234 12345 1023
10.1.2.3 Parse
Parse 系列函數把字符串轉換為其他類型。
示例代碼:
package main
import (
"fmt"
"strconv"
)
func checkError(e error) {
if e != nil {
fmt.Println(e)
}
}
func main() {
a, err := strconv.ParseBool("false")
checkError(err)
b, err := strconv.ParseFloat("123.23", 64)
checkError(err)
c, err := strconv.ParseInt("1234", 10, 64)
checkError(err)
d, err := strconv.ParseUint("12345", 10, 64)
checkError(err)
e, err := strconv.Atoi("1023")
checkError(err)
fmt.Println(a, b, c, d, e) //false 123.23 1234 12345 1023
}
10.2 正則表達式
正則表達式是一種進行模式匹配和文本操縱的復雜而又強大的工具。雖然正則表達式比純粹的文本匹配效率低,但是它卻更靈活。按照它的語法規則,隨需構造出的匹配模式就能夠從原始文本中篩選出幾乎任何你想要得到的字符組合。
Go語言通過regexp標准包為正則表達式提供了官方支持,如果你已經使用過其他編程語言提供的正則相關功能,那么你應該對Go語言版本的不會太陌生,但是它們之間也有一些小的差異,因為Go實現的是RE2標准,除了\C,詳細的語法描述參考:http://code.google.com/p/re2/wiki/Syntax
其實字符串處理我們可以使用strings包來進行搜索(Contains、Index)、替換(Replace)和解析(Split、Join)等操作,但是這些都是簡單的字符串操作,他們的搜索都是大小寫敏感,而且固定的字符串,如果我們需要匹配可變的那種就沒辦法實現了,當然如果strings包能解決你的問題,那么就盡量使用它來解決。因為他們足夠簡單、而且性能和可讀性都會比正則好。
示例代碼:
package main
import (
"fmt"
"regexp"
)
func main() {
context1 := "3.14 123123 .68 haha 1.0 abc 6.66 123."
//MustCompile解析並返回一個正則表達式。如果成功返回,該Regexp就可用於匹配文本。
//解析失敗時會產生panic
// \d 匹配數字[0-9],d+ 重復>=1次匹配d,越多越好(優先重復匹配d)
exp1 := regexp.MustCompile(`\d+\.\d+`)
//返回保管正則表達式所有不重疊的匹配結果的[]string切片。如果沒有匹配到,會返回nil。
//result1 := exp1.FindAllString(context1, -1) //[3.14 1.0 6.66]
result1 := exp1.FindAllStringSubmatch(context1, -1) //[[3.14] [1.0] [6.66]]
fmt.Printf("%v\n", result1)
fmt.Printf("\n------------------------------------\n\n")
context2 := `
<title>標題</title>
<div>你過來啊</div>
<div>hello mike</div>
<div>你大爺</div>
<body>呵呵</body>
`
//(.*?)被括起來的表達式作為分組
//匹配<div>xxx</div>模式的所有子串
exp2 := regexp.MustCompile(`<div>(.*?)</div>`)
result2 := exp2.FindAllStringSubmatch(context2, -1)
//[[<div>你過來啊</div> 你過來啊] [<div>hello mike</div> hello mike] [<div>你大爺</div> 你大爺]]
fmt.Printf("%v\n", result2)
fmt.Printf("\n------------------------------------\n\n")
context3 := `
<title>標題</title>
<div>你過來啊</div>
<div>hello
mike
go</div>
<div>你大爺</div>
<body>呵呵</body>
`
exp3 := regexp.MustCompile(`<div>(.*?)</div>`)
result3 := exp3.FindAllStringSubmatch(context3, -1)
//[[<div>你過來啊</div> 你過來啊] [<div>你大爺</div> 你大爺]]
fmt.Printf("%v\n", result3)
fmt.Printf("\n------------------------------------\n\n")
context4 := `
<title>標題</title>
<div>你過來啊</div>
<div>hello
mike
go</div>
<div>你大爺</div>
<body>呵呵</body>
`
exp4 := regexp.MustCompile(`<div>(?s:(.*?))</div>`)
result4 := exp4.FindAllStringSubmatch(context4, -1)
/*
[[<div>你過來啊</div> 你過來啊] [<div>hello
mike
go</div> hello
mike
go] [<div>你大爺</div> 你大爺]]
*/
fmt.Printf("%v\n", result4)
fmt.Printf("\n------------------------------------\n\n")
for _, text := range result4 {
fmt.Println(text[0]) //帶有div
fmt.Println(text[1]) //不帶帶有div
fmt.Println("================\n")
}
}
10.3 JSON處理
JSON (JavaScript Object Notation)是一種比XML更輕量級的數據交換格式,在易於人們閱讀和編寫的同時,也易於程序解析和生成。盡管JSON是JavaScript的一個子集,但JSON采用完全獨立於編程語言的文本格式,且表現為鍵/值對集合的文本描述形式(類似一些編程語言中的字典結構),這使它成為較為理想的、跨平台、跨語言的數據交換語言。
開發者可以用 JSON 傳輸簡單的字符串、數字、布爾值,也可以傳輸一個數組,或者一個更復雜的復合結構。在 Web 開發領域中, JSON被廣泛應用於 Web 服務端程序和客戶端之間的數據通信。
Go語言內建對JSON的支持。使用Go語言內置的encoding/json 標准庫,開發者可以輕松使用Go程序生成和解析JSON格式的數據。
JSON官方網站:http://www.json.org/
在線格式化:http://www.json.cn/
10.3.1 編碼JSON
10.3.1.1 通過結構體生成JSON
使用json.Marshal()函數可以對一組數據進行JSON格式的編碼。 json.Marshal()函數的聲明如下:
func Marshal(v interface{}) ([]byte, error)
還有一個格式化輸出:
// MarshalIndent 很像 Marshal,只是用縮進對輸出進行格式化
func MarshalIndent(v interface{}, prefix, indent string) ([]byte, error)
1) 編碼JSON
示例代碼:
package main
import (
"encoding/json"
"fmt"
)
type IT struct {
Company string
Subjects []string
IsOk bool
Price float64
}
func main() {
t1 := IT{"itcast", []string{"Go", "C++", "Python", "Test"}, true, 666.666}
//生成一段JSON格式的文本
//如果編碼成功, err 將賦於零值 nil,變量b 將會是一個進行JSON格式化之后的[]byte類型
//b, err := json.Marshal(t1)
//輸出結果:{"Company":"itcast","Subjects":["Go","C++","Python","Test"],"IsOk":true,"Price":666.666}
b, err := json.MarshalIndent(t1, "", " ")
/*
輸出結果:
{
"Company": "itcast",
"Subjects": [
"Go",
"C++",
"Python",
"Test"
],
"IsOk": true,
"Price": 666.666
}
*/
if err != nil {
fmt.Println("json err:", err)
}
fmt.Println(string(b))
}
2) struct tag
我們看到上面的輸出字段名的首字母都是大寫的,如果你想用小寫的首字母怎么辦呢?把結構體的字段名改成首字母小寫的?JSON輸出的時候必須注意,只有導出的字段(首字母是大寫)才會被輸出,如果修改字段名,那么就會發現什么都不會輸出,所以必須通過struct tag定義來實現。
針對JSON的輸出,我們在定義struct tag的時候需要注意的幾點是:
l 字段的tag是"-",那么這個字段不會輸出到JSON
l tag中帶有自定義名稱,那么這個自定義名稱會出現在JSON的字段名中
l tag中如果帶有"omitempty"選項,那么如果該字段值為空,就不會輸出到JSON串中
l 如果字段類型是bool, string, int, int64等,而tag中帶有",string"選項,那么這個字段在輸出到JSON的時候會把該字段對應的值轉換成JSON字符串
示例代碼:
type IT struct {
//Company不會導出到JSON中
Company string `json:"-"`
// Subjects 的值會進行二次JSON編碼
Subjects []string `json:"subjects"`
//轉換為字符串,再輸出
IsOk bool `json:",string"`
// 如果 Price 為空,則不輸出到JSON串中
Price float64 `json:"price, omitempty"`
}
func main() {
t1 := IT{Company: "itcast", Subjects: []string{"Go", "C++", "Python", "Test"}, IsOk: true}
b, err := json.Marshal(t1)
//json.MarshalIndent(t1, "", " ")
if err != nil {
fmt.Println("json err:", err)
}
fmt.Println(string(b))
//輸出結果:{"subjects":["Go","C++","Python","Test"],"IsOk":"true","price":0}
}
10.3.1.2 通過map生成JSON
// 創建一個保存鍵值對的映射
t1 := make(map[string]interface{})
t1["company"] = "itcast"
t1["subjects "] = []string{"Go", "C++", "Python", "Test"}
t1["isok"] = true
t1["price"] = 666.666
b, err := json.Marshal(t1)
//json.MarshalIndent(t1, "", " ")
if err != nil {
fmt.Println("json err:", err)
}
fmt.Println(string(b))
//輸出結果:{"company":"itcast","isok":true,"price":666.666,"subjects ":["Go","C++","Python","Test"]}
10.3.2 解碼JSON
可以使用json.Unmarshal()函數將JSON格式的文本解碼為Go里面預期的數據結構。
json.Unmarshal()函數的原型如下:
func Unmarshal(data []byte, v interface{}) error
該函數的第一個參數是輸入,即JSON格式的文本(比特序列),第二個參數表示目標輸出容器,用於存放解碼后的值。
10.3.2.1 解析到結構體
type IT struct {
Company string `json:"company"`
Subjects []string `json:"subjects"`
IsOk bool `json:"isok"`
Price float64 `json:"price"`
}
func main() {
b := []byte(`{
"company": "itcast",
"subjects": [
"Go",
"C++",
"Python",
"Test"
],
"isok": true,
"price": 666.666
}`)
var t IT
err := json.Unmarshal(b, &t)
if err != nil {
fmt.Println("json err:", err)
}
fmt.Println(t)
//運行結果:{itcast [Go C++ Python Test] true 666.666}
//只想要Subjects字段
type IT2 struct {
Subjects []string `json:"subjects"`
}
var t2 IT2
err = json.Unmarshal(b, &t2)
if err != nil {
fmt.Println("json err:", err)
}
fmt.Println(t2)
//運行結果:{[Go C++ Python Test]}
}
10.3.2.2 解析到interface
示例代碼:
func main() {
b := []byte(`{
"company": "itcast",
"subjects": [
"Go",
"C++",
"Python",
"Test"
],
"isok": true,
"price": 666.666
}`)
var t interface{}
err := json.Unmarshal(b, &t)
if err != nil {
fmt.Println("json err:", err)
}
fmt.Println(t)
//使用斷言判斷類型
m := t.(map[string]interface{})
for k, v := range m {
switch vv := v.(type) {
case string:
fmt.Println(k, "is string", vv)
case int:
fmt.Println(k, "is int", vv)
case float64:
fmt.Println(k, "is float64", vv)
case bool:
fmt.Println(k, "is bool", vv)
case []interface{}:
fmt.Println(k, "is an array:")
for i, u := range vv {
fmt.Println(i, u)
}
default:
fmt.Println(k, "is of a type I don't know how to handle")
}
}
}
運行結果:
10.4 文件操作
10.4.1 相關api介紹
10.4.1.1 建立與打開文件
新建文件可以通過如下兩個方法:
func Create(name string) (file *File, err Error)
根據提供的文件名創建新的文件,返回一個文件對象,默認權限是0666的文件,返回的文件對象是可讀寫的。
func NewFile(fd uintptr, name string) *File
根據文件描述符創建相應的文件,返回一個文件對象
通過如下兩個方法來打開文件:
func Open(name string) (file *File, err Error)
該方法打開一個名稱為name的文件,但是是只讀方式,內部實現其實調用了OpenFile。
func OpenFile(name string, flag int, perm uint32) (file *File, err Error)
打開名稱為name的文件,flag是打開的方式,只讀、讀寫等,perm是權限
10.4.1.2 寫文件
func (file *File) Write(b []byte) (n int, err Error)
寫入byte類型的信息到文件
func (file *File) WriteAt(b []byte, off int64) (n int, err Error)
在指定位置開始寫入byte類型的信息
func (file *File) WriteString(s string) (ret int, err Error)
寫入string信息到文件
10.4.1.3 讀文件
func (file *File) Read(b []byte) (n int, err Error)
讀取數據到b中
func (file *File) ReadAt(b []byte, off int64) (n int, err Error)
從off開始讀取數據到b中
10.4.1.4 刪除文件
func Remove(name string) Error
調用該函數就可以刪除文件名為name的文件
10.4.2 示例代碼
10.4.2.1 寫文件
package main
import (
"fmt"
"os"
)
func main() {
fout, err := os.Create("./xxx.txt") //新建文件
//fout, err := os.OpenFile("./xxx.txt", os.O_CREATE, 0666)
if err != nil {
fmt.Println(err)
return
}
defer fout.Close() //main函數結束前, 關閉文件
for i := 0; i < 5; i++ {
outstr := fmt.Sprintf("%s:%d\n", "Hello go", i)
fout.WriteString(outstr) //寫入string信息到文件
fout.Write([]byte("abcd\n")) //寫入byte類型的信息到文件
}
}
xxx.txt內容如下:
10.4.2.2 讀文件
func main() {
fin, err := os.Open("./xxx.txt") //打開文件
if err != nil {
fmt.Println(err)
}
defer fin.Close()
buf := make([]byte, 1024) //開辟1024個字節的slice作為緩沖
for {
n, _ := fin.Read(buf) //讀文件
if n == 0 { //0表示已經到文件結束
break
}
fmt.Println(string(buf)) //輸出讀取的內容
}
}
10.4.3 案例:拷貝文件
示例代碼:
package main
import (
"fmt"
"io"
"os"
)
func main() {
args := os.Args //獲取用戶輸入的所有參數
//如果用戶沒有輸入,或參數個數不夠,則調用該函數提示用戶
if args == nil || len(args) != 3 {
fmt.Println("useage : xxx srcFile dstFile")
return
}
srcPath := args[1] //獲取輸入的第一個參數
dstPath := args[2] //獲取輸入的第二個參數
fmt.Printf("srcPath = %s, dstPath = %s\n", srcPath, dstPath)
if srcPath == dstPath {
fmt.Println("源文件和目的文件名字不能相同")
return
}
srcFile, err1 := os.Open(srcPath) //打開源文件
if err1 != nil {
fmt.Println(err1)
return
}
dstFile, err2 := os.Create(dstPath) //創建目的文件
if err2 != nil {
fmt.Println(err2)
return
}
buf := make([]byte, 1024) //切片緩沖區
for {
//從源文件讀取內容,n為讀取文件內容的長度
n, err := srcFile.Read(buf)
if err != nil && err != io.EOF {
fmt.Println(err)
break
}
if n == 0 {
fmt.Println("文件處理完畢")
break
}
//切片截取
tmp := buf[:n]
//把讀取的內容寫入到目的文件
dstFile.Write(tmp)
}
//關閉文件
srcFile.Close()
dstFile.Close()
}
運行結果:
11. 並發編程
11.1 概述
11.1.1 並行和並發
並行(parallel):指在同一時刻,有多條指令在多個處理器上同時執行。
並發(concurrency):指在同一時刻只能有一條指令執行,但多個進程指令被快速的輪換執行,使得在宏觀上具有多個進程同時執行的效果,但在微觀上並不是同時執行的,只是把時間分成若干段,使多個進程快速交替的執行。
l 並行是兩個隊列同時使用兩台咖啡機
l 並發是兩個隊列交替使用一台咖啡機
11.1.2 Go語言並發優勢
有人把Go比作21世紀的C語言,第一是因為Go語言設計簡單,第二,21世紀最重要的就是並行程序設計,而Go從語言層面就支持了並行。同時,並發程序的內存管理有時候是非常復雜的,而Go語言提供了自動垃圾回收機制。
Go語言為並發編程而內置的上層API基於CSP(communicating sequential processes, 順序通信進程)模型。這就意味着顯式鎖都是可以避免的,因為Go語言通過相冊安全的通道發送和接受數據以實現同步,這大大地簡化了並發程序的編寫。
一般情況下,一個普通的桌面計算機跑十幾二十個線程就有點負載過大了,但是同樣這台機器卻可以輕松地讓成百上千甚至過萬個goroutine進行資源競爭。
11.2 goroutine
11.2.1 goroutine是什么
goroutine是Go並行設計的核心。goroutine說到底其實就是協程,但是它比線程更小,十幾個goroutine可能體現在底層就是五六個線程,Go語言內部幫你實現了這些goroutine之間的內存共享。執行goroutine只需極少的棧內存(大概是4~5KB),當然會根據相應的數據伸縮。也正因為如此,可同時運行成千上萬個並發任務。goroutine比thread更易用、更高效、更輕便。
11.2.2 創建goroutine
只需在函數調⽤語句前添加 go 關鍵字,就可創建並發執⾏單元。開發⼈員無需了解任何執⾏細節,調度器會自動將其安排到合適的系統線程上執行。
在並發編程里,我們通常想講一個過程切分成幾塊,然后讓每個goroutine各自負責一塊工作。當一個程序啟動時,其主函數即在一個單獨的goroutine中運行,我們叫它main goroutine。新的goroutine會用go語句來創建。
示例代碼:
package main
import (
"fmt"
"time"
)
func newTask() {
i := 0
for {
i++
fmt.Printf("new goroutine: i = %d\n", i)
time.Sleep(1 * time.Second) //延時1s
}
}
func main() {
//創建一個 goroutine,啟動另外一個任務
go newTask()
i := 0
//main goroutine 循環打印
for {
i++
fmt.Printf("main goroutine: i = %d\n", i)
time.Sleep(1 * time.Second) //延時1s
}
}
程序運行結果:
11.2.3 主goroutine先退出
主goroutine退出后,其它的工作goroutine也會自動退出:
func newTask() {
i := 0
for {
i++
fmt.Printf("new goroutine: i = %d\n", i)
time.Sleep(1 * time.Second) //延時1s
}
}
func main() {
//創建一個 goroutine,啟動另外一個任務
go newTask()
fmt.Println("main goroutine exit")
}
程序運行結果:
11.2.4 runtime包
11.2.4.1 Gosched
runtime.Gosched() 用於讓出CPU時間片,讓出當前goroutine的執行權限,調度器安排其他等待的任務運行,並在下次某個時候從該位置恢復執行。
這就像跑接力賽,A跑了一會碰到代碼runtime.Gosched() 就把接力棒交給B了,A歇着了,B繼續跑。
示例代碼:
func main() {
//創建一個goroutine
go func(s string) {
for i := 0; i < 2; i++ {
fmt.Println(s)
}
}("world")
for i := 0; i < 2; i++ {
runtime.Gosched() //import "runtime"
/*
屏蔽runtime.Gosched()運行結果如下:
hello
hello
沒有runtime.Gosched()運行結果如下:
world
world
hello
hello
*/
fmt.Println("hello")
}
}
11.2.4.2 Goexit
調用 runtime.Goexit() 將立即終止當前 goroutine 執⾏,調度器確保所有已注冊 defer延遲調用被執行。
示例代碼:
func main() {
go func() {
defer fmt.Println("A.defer")
func() {
defer fmt.Println("B.defer")
runtime.Goexit() // 終止當前 goroutine, import "runtime"
fmt.Println("B") // 不會執行
}()
fmt.Println("A") // 不會執行
}() //別忘了()
//死循環,目的不讓主goroutine結束
for {
}
}
程序運行結果:
11.2.4.3 GOMAXPROCS
調用 runtime.GOMAXPROCS() 用來設置可以並行計算的CPU核數的最大值,並返回之前的值。
示例代碼:
func main() {
//n := runtime.GOMAXPROCS(1) //打印結果:111111111111111111110000000000000000000011111...
n := runtime.GOMAXPROCS(2) //打印結果:010101010101010101011001100101011010010100110...
fmt.Printf("n = %d\n", n)
for {
go fmt.Print(0)
fmt.Print(1)
}
}
在第一次執行(runtime.GOMAXPROCS(1))時,最多同時只能有一個goroutine被執行。所以
會打印很多1。過了一段時間后,GO調度器會將其置為休眠,並喚醒另一個goroutine,這時候就開始打印很多0了,在打印的時候,goroutine是被調度到操作系統線程上的。
在第二次執行(runtime.GOMAXPROCS(2))時,我們使用了兩個CPU,所以兩個goroutine可以一起被執行,以同樣的頻率交替打印0和1。
11.3 channel
goroutine運行在相同的地址空間,因此訪問共享內存必須做好同步。goroutine 奉行通過通信來共享內存,而不是共享內存來通信。
引⽤類型 channel 是 CSP 模式的具體實現,用於多個 goroutine 通訊。其內部實現了同步,確保並發安全。
11.3.1 channel類型
和map類似,channel也一個對應make創建的底層數據結構的引用。
當我們復制一個channel或用於函數參數傳遞時,我們只是拷貝了一個channel引用,因此調用者何被調用者將引用同一個channel對象。和其它的引用類型一樣,channel的零值也是nil。
定義一個channel時,也需要定義發送到channel的值的類型。channel可以使用內置的make()函數來創建:
make(chan Type) //等價於make(chan Type, 0)
make(chan Type, capacity)
當 capacity= 0 時,channel 是無緩沖阻塞讀寫的,當capacity> 0 時,channel 有緩沖、是非阻塞的,直到寫滿 capacity個元素才阻塞寫入。
channel通過操作符<-來接收和發送數據,發送和接收數據語法:
channel <- value //發送value到channel
<-channel //接收並將其丟棄
x := <-channel //從channel中接收數據,並賦值給x
x, ok := <-channel //功能同上,同時檢查通道是否已關閉或者是否為空
默認情況下,channel接收和發送數據都是阻塞的,除非另一端已經准備好,這樣就使得goroutine同步變的更加的簡單,而不需要顯式的lock。
示例代碼:
func main() {
c := make(chan int)
go func() {
defer fmt.Println("子協程結束")
fmt.Println("子協程正在運行……")
c <- 666 //666發送到c
}()
num := <-c //從c中接收數據,並賦值給num
fmt.Println("num = ", num)
fmt.Println("main協程結束")
}
程序運行結果:
11.3.2 無緩沖的channel
無緩沖的通道(unbuffered channel)是指在接收前沒有能力保存任何值的通道。
這種類型的通道要求發送 goroutine 和接收 goroutine 同時准備好,才能完成發送和接收操作。如果兩個goroutine沒有同時准備好,通道會導致先執行發送或接收操作的 goroutine 阻塞等待。
這種對通道進行發送和接收的交互行為本身就是同步的。其中任意一個操作都無法離開另一個操作單獨存在。
下圖展示兩個 goroutine 如何利用無緩沖的通道來共享一個值:
l 在第 1 步,兩個 goroutine 都到達通道,但哪個都沒有開始執行發送或者接收。
l 在第 2 步,左側的 goroutine 將它的手伸進了通道,這模擬了向通道發送數據的行為。這時,這個 goroutine 會在通道中被鎖住,直到交換完成。
l 在第 3 步,右側的 goroutine 將它的手放入通道,這模擬了從通道里接收數據。這個 goroutine 一樣也會在通道中被鎖住,直到交換完成。
l 在第 4 步和第 5 步,進行交換,並最終,在第 6 步,兩個 goroutine 都將它們的手從通道里拿出來,這模擬了被鎖住的 goroutine 得到釋放。兩個 goroutine 現在都可以去做別的事情了。
無緩沖的channel創建格式:
make(chan Type) //等價於make(chan Type, 0)
如果沒有指定緩沖區容量,那么該通道就是同步的,因此會阻塞到發送者准備好發送和接收者准備好接收。
示例代碼:
func main() {
c := make(chan int, 0) //無緩沖的通道
//內置函數 len 返回未被讀取的緩沖元素數量, cap 返回緩沖區大小
fmt.Printf("len(c)=%d, cap(c)=%d\n", len(c), cap(c))
go func() {
defer fmt.Println("子協程結束")
for i := 0; i < 3; i++ {
c <- i
fmt.Printf("子協程正在運行[%d]: len(c)=%d, cap(c)=%d\n", i, len(c), cap(c))
}
}()
time.Sleep(2 * time.Second) //延時2s
for i := 0; i < 3; i++ {
num := <-c //從c中接收數據,並賦值給num
fmt.Println("num = ", num)
}
fmt.Println("main協程結束")
}
程序運行結果:
11.3.3 有緩沖的channel
有緩沖的通道(buffered channel)是一種在被接收前能存儲一個或者多個值的通道。
這種類型的通道並不強制要求 goroutine 之間必須同時完成發送和接收。通道會阻塞發送和接收動作的條件也會不同。只有在通道中沒有要接收的值時,接收動作才會阻塞。只有在通道沒有可用緩沖區容納被發送的值時,發送動作才會阻塞。
這導致有緩沖的通道和無緩沖的通道之間的一個很大的不同:無緩沖的通道保證進行發送和接收的 goroutine 會在同一時間進行數據交換;有緩沖的通道沒有這種保證。
示例圖如下:
l 在第 1 步,右側的 goroutine 正在從通道接收一個值。
l 在第 2 步,右側的這個 goroutine獨立完成了接收值的動作,而左側的 goroutine 正在發送一個新值到通道里。
l 在第 3 步,左側的goroutine 還在向通道發送新值,而右側的 goroutine 正在從通道接收另外一個值。這個步驟里的兩個操作既不是同步的,也不會互相阻塞。
l 最后,在第 4 步,所有的發送和接收都完成,而通道里還有幾個值,也有一些空間可以存更多的值。
有緩沖的channel創建格式:
make(chan Type, capacity)
如果給定了一個緩沖區容量,通道就是異步的。只要緩沖區有未使用空間用於發送數據,或還包含可以接收的數據,那么其通信就會無阻塞地進行。
示例代碼:
func main() {
c := make(chan int, 3) //帶緩沖的通道
//內置函數 len 返回未被讀取的緩沖元素數量, cap 返回緩沖區大小
fmt.Printf("len(c)=%d, cap(c)=%d\n", len(c), cap(c))
go func() {
defer fmt.Println("子協程結束")
for i := 0; i < 3; i++ {
c <- i
fmt.Printf("子協程正在運行[%d]: len(c)=%d, cap(c)=%d\n", i, len(c), cap(c))
}
}()
time.Sleep(2 * time.Second) //延時2s
for i := 0; i < 3; i++ {
num := <-c //從c中接收數據,並賦值給num
fmt.Println("num = ", num)
}
fmt.Println("main協程結束")
}
程序運行結果:
11.3.4 range和close
如果發送者知道,沒有更多的值需要發送到channel的話,那么讓接收者也能及時知道沒有多余的值可接收將是有用的,因為接收者可以停止不必要的接收等待。這可以通過內置的close函數來關閉channel實現。
示例代碼:
func main() {
c := make(chan int)
go func() {
for i := 0; i < 5; i++ {
c <- i
}
//把 close(c) 注釋掉,程序會一直阻塞在 if data, ok := <-c; ok 那一行
close(c)
}()
for {
//ok為true說明channel沒有關閉,為false說明管道已經關閉
if data, ok := <-c; ok {
fmt.Println(data)
} else {
break
}
}
fmt.Println("Finished")
}
程序運行結果:
注意點:
l channel不像文件一樣需要經常去關閉,只有當你確實沒有任何發送數據了,或者你想顯式的結束range循環之類的,才去關閉channel;
l 關閉channel后,無法向channel 再發送數據(引發 panic 錯誤后導致接收立即返回零值);
l 關閉channel后,可以繼續向channel接收數據;
l 對於nil channel,無論收發都會被阻塞。
可以使用 range 來迭代不斷操作channel:
func main() {
c := make(chan int)
go func() {
for i := 0; i < 5; i++ {
c <- i
}
//把 close(c) 注釋掉,程序會一直阻塞在 for data := range c 那一行
close(c)
}()
for data := range c {
fmt.Println(data)
}
fmt.Println("Finished")
}
11.3.5 單方向的channel
默認情況下,通道是雙向的,也就是,既可以往里面發送數據也可以同里面接收數據。
但是,我們經常見一個通道作為參數進行傳遞而值希望對方是單向使用的,要么只讓它發送數據,要么只讓它接收數據,這時候我們可以指定通道的方向。
單向channel變量的聲明非常簡單,如下:
var ch1 chan int // ch1是一個正常的channel,不是單向的
var ch2 chan<- float64 // ch2是單向channel,只用於寫float64數據
var ch3 <-chan int // ch3是單向channel,只用於讀取int數據
l chan<- 表示數據進入管道,要把數據寫進管道,對於調用者就是輸出。
l <-chan 表示數據從管道出來,對於調用者就是得到管道的數據,當然就是輸入。
可以將 channel 隱式轉換為單向隊列,只收或只發,不能將單向 channel 轉換為普通 channel:
c := make(chan int, 3)
var send chan<- int = c // send-only
var recv <-chan int = c // receive-only
send <- 1
//<-send //invalid operation: <-send (receive from send-only type chan<- int)
<-recv
//recv <- 2 //invalid operation: recv <- 2 (send to receive-only type <-chan int)
//不能將單向 channel 轉換為普通 channel
d1 := (chan int)(send) //cannot convert send (type chan<- int) to type chan int
d2 := (chan int)(recv) //cannot convert recv (type <-chan int) to type chan int
示例代碼:
// chan<- //只寫
func counter(out chan<- int) {
defer close(out)
for i := 0; i < 5; i++ {
out <- i //如果對方不讀 會阻塞
}
}
// <-chan //只讀
func printer(in <-chan int) {
for num := range in {
fmt.Println(num)
}
}
func main() {
c := make(chan int) // chan //讀寫
go counter(c) //生產者
printer(c) //消費者
fmt.Println("done")
}
11.3.6 定時器
11.3.6.1 Timer
Timer是一個定時器,代表未來的一個單一事件,你可以告訴timer你要等待多長時間,它提供一個channel,在將來的那個時間那個channel提供了一個時間值。
示例代碼:
import "fmt"
import "time"
func main() {
//創建定時器,2秒后,定時器就會向自己的C字節發送一個time.Time類型的元素值
timer1 := time.NewTimer(time.Second * 2)
t1 := time.Now() //當前時間
fmt.Printf("t1: %v\n", t1)
t2 := <-timer1.C
fmt.Printf("t2: %v\n", t2)
//如果只是想單純的等待的話,可以使用 time.Sleep 來實現
timer2 := time.NewTimer(time.Second * 2)
<-timer2.C
fmt.Println("2s后")
time.Sleep(time.Second * 2)
fmt.Println("再一次2s后")
<-time.After(time.Second * 2)
fmt.Println("再再一次2s后")
timer3 := time.NewTimer(time.Second)
go func() {
<-timer3.C
fmt.Println("Timer 3 expired")
}()
stop := timer3.Stop() //停止定時器
if stop {
fmt.Println("Timer 3 stopped")
}
fmt.Println("before")
timer4 := time.NewTimer(time.Second * 5) //原來設置3s
timer4.Reset(time.Second * 1) //重新設置時間
<-timer4.C
fmt.Println("after")
}
11.3.6.2 Ticker
Ticker是一個定時觸發的計時器,它會以一個間隔(interval)往channel發送一個事件(當前時間),而channel的接收者可以以固定的時間間隔從channel中讀取事件。
示例代碼:
func main() {
//創建定時器,每隔1秒后,定時器就會給channel發送一個事件(當前時間)
ticker := time.NewTicker(time.Second * 1)
i := 0
go func() {
for { //循環
<-ticker.C
i++
fmt.Println("i = ", i)
if i == 5 {
ticker.Stop() //停止定時器
}
}
}() //別忘了()
//死循環,特地不讓main goroutine結束
for {
}
}
11.4 select
11.4.1 select作用
Go里面提供了一個關鍵字select,通過select可以監聽channel上的數據流動。
select的用法與switch語言非常類似,由select開始一個新的選擇塊,每個選擇條件由case語句來描述。
與switch語句可以選擇任何可使用相等比較的條件相比, select有比較多的限制,其中最大的一條限制就是每個case語句里必須是一個IO操作,大致的結構如下:
select {
case <-chan1:
// 如果chan1成功讀到數據,則進行該case處理語句
case chan2 <- 1:
// 如果成功向chan2寫入數據,則進行該case處理語句
default:
// 如果上面都沒有成功,則進入default處理流程
}
在一個select語句中,Go語言會按順序從頭至尾評估每一個發送和接收的語句。
如果其中的任意一語句可以繼續執行(即沒有被阻塞),那么就從那些可以執行的語句中任意選擇一條來使用。
如果沒有任意一條語句可以執行(即所有的通道都被阻塞),那么有兩種可能的情況:
l 如果給出了default語句,那么就會執行default語句,同時程序的執行會從select語句后的語句中恢復。
l 如果沒有default語句,那么select語句將被阻塞,直到至少有一個通信可以進行下去。
示例代碼:
func fibonacci(c, quit chan int) {
x, y := 1, 1
for {
select {
case c <- x:
x, y = y, x+y
case <-quit:
fmt.Println("quit")
return
}
}
}
func main() {
c := make(chan int)
quit := make(chan int)
go func() {
for i := 0; i < 6; i++ {
fmt.Println(<-c)
}
quit <- 0
}()
fibonacci(c, quit)
}
運行結果如下:
11.4.2 超時
有時候會出現goroutine阻塞的情況,那么我們如何避免整個程序進入阻塞的情況呢?我們可以利用select來設置超時,通過如下的方式實現:
func main() {
c := make(chan int)
o := make(chan bool)
go func() {
for {
select {
case v := <-c:
fmt.Println(v)
case <-time.After(5 * time.Second):
fmt.Println("timeout")
o <- true
break
}
}
}()
//c <- 666 // 注釋掉,引發 timeout
<-o
}
12. 網絡編程
12.1 網絡概述
12.1.1 網絡協議
從應用的角度出發,協議可理解為“規則”,是數據傳輸和數據的解釋的規則。
假設,A、B雙方欲傳輸文件。規定:
l 第一次,傳輸文件名,接收方接收到文件名,應答OK給傳輸方;
l 第二次,發送文件的尺寸,接收方接收到該數據再次應答一個OK;
l 第三次,傳輸文件內容。同樣,接收方接收數據完成后應答OK表示文件內容接收成功。
由此,無論A、B之間傳遞何種文件,都是通過三次數據傳輸來完成。A、B之間形成了一個最簡單的數據傳輸規則。雙方都按此規則發送、接收數據。A、B之間達成的這個相互遵守的規則即為協議。
這種僅在A、B之間被遵守的協議稱之為原始協議。
當此協議被更多的人采用,不斷的增加、改進、維護、完善。最終形成一個穩定的、完整的文件傳輸協議,被廣泛應用於各種文件傳輸過程中。該協議就成為一個標准協議。最早的ftp協議就是由此衍生而來。
12.1.2 分層模型
12.1.2.1 網絡分層架構
為了減少協議設計的復雜性,大多數網絡模型均采用分層的方式來組織。每一層都有自己的功能,就像建築物一樣,每一層都靠下一層支持。每一層利用下一層提供的服務來為上一層提供服務,本層服務的實現細節對上層屏蔽。
越下面的層,越靠近硬件;越上面的層,越靠近用戶。至於每一層叫什么名字,其實並不重要(面試的時候,面試官可能會問每一層的名字)。只需要知道,互聯網分成若干層即可。
1) 物理層:主要定義物理設備標准,如網線的接口類型、光纖的接口類型、各種傳輸介質的傳輸速率等。它的主要作用是傳輸比特流(就是由1、0轉化為電流強弱來進行傳輸,到達目的地后再轉化為1、0,也就是我們常說的數模轉換與模數轉換)。這一層的數據叫做比特。
2) 數據鏈路層:定義了如何讓格式化數據以幀為單位進行傳輸,以及如何讓控制對物理介質的訪問。這一層通常還提供錯誤檢測和糾正,以確保數據的可靠傳輸。如:串口通信中使用到的115200、8、N、1
3) 網絡層:在位於不同地理位置的網絡中的兩個主機系統之間提供連接和路徑選擇。Internet的發展使得從世界各站點訪問信息的用戶數大大增加,而網絡層正是管理這種連接的層。
4) 傳輸層:定義了一些傳輸數據的協議和端口號(WWW端口80等),如:TCP(傳輸控制協議,傳輸效率低,可靠性強,用於傳輸可靠性要求高,數據量大的數據),UDP(用戶數據報協議,與TCP特性恰恰相反,用於傳輸可靠性要求不高,數據量小的數據,如QQ聊天數據就是通過這種方式傳輸的)。 主要是將從下層接收的數據進行分段和傳輸,到達目的地址后再進行重組。常常把這一層數據叫做段。
5) 會話層:通過傳輸層(端口號:傳輸端口與接收端口)建立數據傳輸的通路。主要在你的系統之間發起會話或者接受會話請求(設備之間需要互相認識可以是IP也可以是MAC或者是主機名)。
6) 表示層:可確保一個系統的應用層所發送的信息可以被另一個系統的應用層讀取。例如,PC程序與另一台計算機進行通信,其中一台計算機使用擴展二一十進制交換碼(EBCDIC),而另一台則使用美國信息交換標准碼(ASCII)來表示相同的字符。如有必要,表示層會通過使用一種通格式來實現多種數據格式之間的轉換。
7) 應用層:是最靠近用戶的OSI層。這一層為用戶的應用程序(例如電子郵件、文件傳輸和終端仿真)提供網絡服務。
12.1.2.2 層與協議
每一層都是為了完成一種功能,為了實現這些功能,就需要大家都遵守共同的規則。大家都遵守這規則,就叫做“協議”(protocol)。
網絡的每一層,都定義了很多協議。這些協議的總稱,叫“TCP/IP協議”。TCP/IP協議是一個大家族,不僅僅只有TCP和IP協議,它還包括其它的協議,如下圖:
12.1.2.3 每層協議的功能
1) 鏈路層
以太網規定,連入網絡的所有設備,都必須具有“網卡”接口。數據包必須是從一塊網卡,傳送到另一塊網卡。通過網卡能夠使不同的計算機之間連接,從而完成數據通信等功能。網卡的地址——MAC 地址,就是數據包的物理發送地址和物理接收地址。
2) 網絡層
網絡層的作用是引進一套新的地址,使得我們能夠區分不同的計算機是否屬於同一個子網絡。這套地址就叫做“網絡地址”,這是我們平時所說的IP地址。這個IP地址好比我們的手機號碼,通過手機號碼可以得到用戶所在的歸屬地。
網絡地址幫助我們確定計算機所在的子網絡,MAC 地址則將數據包送到該子網絡中的目標網卡。網絡層協議包含的主要信息是源IP和目的IP。
於是,“網絡層”出現以后,每台計算機有了兩種地址,一種是 MAC 地址,另一種是網絡地址。兩種地址之間沒有任何聯系,MAC 地址是綁定在網卡上的,網絡地址則是管理員分配的,它們只是隨機組合在一起。
網絡地址幫助我們確定計算機所在的子網絡,MAC 地址則將數據包送到該子網絡中的目標網卡。因此,從邏輯上可以推斷,必定是先處理網絡地址,然后再處理 MAC 地址。
3) 傳輸層
當我們一邊聊QQ,一邊聊微信,當一個數據包從互聯網上發來的時候,我們怎么知道,它是來自QQ的內容,還是來自微信的內容?
也就是說,我們還需要一個參數,表示這個數據包到底供哪個程序(進程)使用。這個參數就叫做“端口”(port),它其實是每一個使用網卡的程序的編號。每個數據包都發到主機的特定端口,所以不同的程序就能取到自己所需要的數據。
端口特點:
l 對於同一個端口,在不同系統中對應着不同的進程
l 對於同一個系統,一個端口只能被一個進程擁有
4) 應用層
應用程序收到“傳輸層”的數據,接下來就要進行解讀。由於互聯網是開放架構,數據來源五花八門,必須事先規定好格式,否則根本無法解讀。“應用層”的作用,就是規定應用程序的數據格式。
12.2 Socket編程
12.2.1 什么是Socket
Socket起源於Unix,而Unix基本哲學之一就是“一切皆文件”,都可以用“打開open –> 讀寫write/read –> 關閉close”模式來操作。Socket就是該模式的一個實現,網絡的Socket數據傳輸是一種特殊的I/O,Socket也是一種文件描述符。Socket也具有一個類似於打開文件的函數調用:Socket(),該函數返回一個整型的Socket描述符,隨后的連接建立、數據傳輸等操作都是通過該Socket實現的。
常用的Socket類型有兩種:流式Socket(SOCK_STREAM)和數據報式Socket(SOCK_DGRAM)。流式是一種面向連接的Socket,針對於面向連接的TCP服務應用;數據報式Socket是一種無連接的Socket,對應於無連接的UDP服務應用。
12.2.2 TCP的C/S架構
12.2.3 示例程序
12.2.3.1 服務器代碼
package main
import (
"fmt"
"log"
"net"
"strings"
)
func dealConn(conn net.Conn) {
defer conn.Close() //此函數結束時,關閉連接套接字
//conn.RemoteAddr().String():連接客服端的網絡地址
ipAddr := conn.RemoteAddr().String()
fmt.Println(ipAddr, "連接成功")
buf := make([]byte, 1024) //緩沖區,用於接收客戶端發送的數據
for {
//阻塞等待用戶發送的數據
n, err := conn.Read(buf) //n代碼接收數據的長度
if err != nil {
fmt.Println(err)
return
}
//切片截取,只截取有效數據
result := buf[:n]
fmt.Printf("接收到數據來自[%s]==>[%d]:%s\n", ipAddr, n, string(result))
if "exit" == string(result) { //如果對方發送"exit",退出此鏈接
fmt.Println(ipAddr, "退出連接")
return
}
//把接收到的數據轉換為大寫,再給客戶端發送
conn.Write([]byte(strings.ToUpper(string(result))))
}
}
func main() {
//創建、監聽socket
listenner, err := net.Listen("tcp", "127.0.0.1:8000")
if err != nil {
log.Fatal(err) //log.Fatal()會產生panic
}
defer listenner.Close()
for {
conn, err := listenner.Accept() //阻塞等待客戶端連接
if err != nil {
log.Println(err)
continue
}
go dealConn(conn)
}
}
12.2.3.2 客服端代碼
package main
import (
"fmt"
"log"
"net"
)
func main() {
//客戶端主動連接服務器
conn, err := net.Dial("tcp", "127.0.0.1:8000")
if err != nil {
log.Fatal(err) //log.Fatal()會產生panic
return
}
defer conn.Close() //關閉
buf := make([]byte, 1024) //緩沖區
for {
fmt.Printf("請輸入發送的內容:")
fmt.Scan(&buf)
fmt.Printf("發送的內容:%s\n", string(buf))
//發送數據
conn.Write(buf)
//阻塞等待服務器回復的數據
n, err := conn.Read(buf) //n代碼接收數據的長度
if err != nil {
fmt.Println(err)
return
}
//切片截取,只截取有效數據
result := buf[:n]
fmt.Printf("接收到數據[%d]:%s\n", n, string(result))
}
}
12.2.3.3 運行結果
12.3 HTTP編程
12.3.1 概述
12.3.1.1 Web工作方式
我們平時瀏覽網頁的時候,會打開瀏覽器,輸入網址后按下回車鍵,然后就會顯示出你想要瀏覽的內容。在這個看似簡單的用戶行為背后,到底隱藏了些什么呢?
對於普通的上網過程,系統其實是這樣做的:瀏覽器本身是一個客戶端,當你輸入URL的時候,首先瀏覽器會去請求DNS服務器,通過DNS獲取相應的域名對應的IP,然后通過IP地址找到IP對應的服務器后,要求建立TCP連接,等瀏覽器發送完HTTP Request(請求)包后,服務器接收到請求包之后才開始處理請求包,服務器調用自身服務,返回HTTP Response(響應)包;客戶端收到來自服務器的響應后開始渲染這個Response包里的主體(body),等收到全部的內容隨后斷開與該服務器之間的TCP連接。
一個Web服務器也被稱為HTTP服務器,它通過HTTP協議與客戶端通信。這個客戶端通常指的是Web瀏覽器(其實手機端客戶端內部也是瀏覽器實現的)。
Web服務器的工作原理可以簡單地歸納為:
l 客戶機通過TCP/IP協議建立到服務器的TCP連接
l 客戶端向服務器發送HTTP協議請求包,請求服務器里的資源文檔
l 服務器向客戶機發送HTTP協議應答包,如果請求的資源包含有動態語言的內容,那么服務器會調用動態語言的解釋引擎負責處理“動態內容”,並將處理得到的數據返回給客戶端
l 客戶機與服務器斷開。由客戶端解釋HTML文檔,在客戶端屏幕上渲染圖形結果
12.3.1.2 HTTP協議
超文本傳輸協議(HTTP,HyperText Transfer Protocol)是互聯網上應用最為廣泛的一種網絡協議,它詳細規定了瀏覽器和萬維網服務器之間互相通信的規則,通過因特網傳送萬維網文檔的數據傳送協議。
HTTP協議通常承載於TCP協議之上,有時也承載於TLS或SSL協議層之上,這個時候,就成了我們常說的HTTPS。如下圖所示:
12.3.1.3 地址(URL)
URL全稱為Unique Resource Location,用來表示網絡資源,可以理解為網絡文件路徑。
URL的格式如下:
http://host[":"port][abs_path]
http://192.168.31.1/html/index
URL的長度有限制,不同的服務器的限制值不太相同,但是不能無限長。
12.3.2 HTTP報文淺析
12.3.2.1 請求報文格式
1) 測試代碼
服務器測試代碼:
package main
import (
"fmt"
"log"
"net"
)
func main() {
//創建、監聽socket
listenner, err := net.Listen("tcp", "127.0.0.1:8000")
if err != nil {
log.Fatal(err) //log.Fatal()會產生panic
}
defer listenner.Close()
conn, err := listenner.Accept() //阻塞等待客戶端連接
if err != nil {
log.Println(err)
return
}
defer conn.Close() //此函數結束時,關閉連接套接字
//conn.RemoteAddr().String():連接客服端的網絡地址
ipAddr := conn.RemoteAddr().String()
fmt.Println(ipAddr, "連接成功")
buf := make([]byte, 4096) //緩沖區,用於接收客戶端發送的數據
//阻塞等待用戶發送的數據
n, err := conn.Read(buf) //n代碼接收數據的長度
if err != nil {
fmt.Println(err)
return
}
//切片截取,只截取有效數據
result := buf[:n]
fmt.Printf("接收到數據來自[%s]==>:\n%s\n", ipAddr, string(result))
}
瀏覽器輸入url地址:
服務器端運行打印結果如下:
2) 請求報文格式說明
HTTP 請求報文由請求行、請求頭部、空行、請求包體4個部分組成,如下圖所示:
1) 請求行
請求行由方法字段、URL 字段 和HTTP 協議版本字段 3 個部分組成,他們之間使用空格隔開。常用的 HTTP 請求方法有 GET、POST。
GET:
l 當客戶端要從服務器中讀取某個資源時,使用GET 方法。GET 方法要求服務器將URL 定位的資源放在響應報文的數據部分,回送給客戶端,即向服務器請求某個資源。
l 使用GET方法時,請求參數和對應的值附加在 URL 后面,利用一個問號(“?”)代表URL 的結尾與請求參數的開始,傳遞參數長度受限制,因此GET方法不適合用於上傳數據。
l 通過GET方法來獲取網頁時,參數會顯示在瀏覽器地址欄上,因此保密性很差。
POST:
l 當客戶端給服務器提供信息較多時可以使用POST 方法,POST 方法向服務器提交數據,比如完成表單數據的提交,將數據提交給服務器處理。
l GET 一般用於獲取/查詢資源信息,POST 會附帶用戶數據,一般用於更新資源信息。POST 方法將請求參數封裝在HTTP 請求數據中,而且長度沒有限制,因為POST攜帶的數據,在HTTP的請求正文中,以名稱/值的形式出現,可以傳輸大量數據。
2) 請求頭部
請求頭部為請求報文添加了一些附加信息,由“名/值”對組成,每行一對,名和值之間使用冒號分隔。
請求頭部通知服務器有關於客戶端請求的信息,典型的請求頭有:
| 請求頭 |
含義 |
| User-Agent |
請求的瀏覽器類型 |
| Accept |
客戶端可識別的響應內容類型列表,星號“ * ”用於按范圍將類型分組,用“ */* ”指示可接受全部類型,用“ type/* ”指示可接受 type 類型的所有子類型 |
| Accept-Language |
客戶端可接受的自然語言 |
| Accept-Encoding |
客戶端可接受的編碼壓縮格式 |
| Accept-Charset |
可接受的應答的字符集 |
| Host |
請求的主機名,允許多個域名同處一個IP 地址,即虛擬主機 |
| connection |
連接方式(close或keepalive) |
| Cookie |
存儲於客戶端擴展字段,向同一域名的服務端發送屬於該域的cookie |
3) 空行
最后一個請求頭之后是一個空行,發送回車符和換行符,通知服務器以下不再有請求頭。
4) 請求包體
請求包體不在GET方法中使用,而是POST方法中使用。
POST方法適用於需要客戶填寫表單的場合。與請求包體相關的最常使用的是包體類型Content-Type和包體長度Content-Length。
12.3.2.2 響應報文格式
1) 測試代碼
服務器示例代碼:
package main
import (
"fmt"
"net/http"
)
//服務端編寫的業務邏輯處理程序
func myHandler(w http.ResponseWriter, r *http.Request) {
fmt.Fprintln(w, "hello world")
}
func main() {
http.HandleFunc("/go", myHandler)
//在指定的地址進行監聽,開啟一個HTTP
http.ListenAndServe("127.0.0.1:8000", nil)
}
啟動服務器程序:
客戶端測試示例代碼:
package main
import (
"fmt"
"log"
"net"
)
func main() {
//客戶端主動連接服務器
conn, err := net.Dial("tcp", "127.0.0.1:8000")
if err != nil {
log.Fatal(err) //log.Fatal()會產生panic
return
}
defer conn.Close() //關閉
requestHeader := "GET /go HTTP/1.1\r\nAccept: image/gif, image/jpeg, image/pjpeg, application/x-ms-application, application/xaml+xml, application/x-ms-xbap, */*\r\nAccept-Language: zh-Hans-CN,zh-Hans;q=0.8,en-US;q=0.5,en;q=0.3\r\nUser-Agent: Mozilla/4.0 (compatible; MSIE 7.0; Windows NT 10.0; WOW64; Trident/7.0; .NET4.0C; .NET4.0E; .NET CLR 2.0.50727; .NET CLR 3.0.30729; .NET CLR 3.5.30729)\r\nAccept-Encoding: gzip, deflate\r\nHost: 127.0.0.1:8000\r\nConnection: Keep-Alive\r\n\r\n"
//先發送請求包
conn.Write([]byte(requestHeader))
buf := make([]byte, 4096) //緩沖區
//阻塞等待服務器回復的數據
n, err := conn.Read(buf) //n代碼接收數據的長度
if err != nil {
fmt.Println(err)
return
}
//切片截取,只截取有效數據
result := buf[:n]
fmt.Printf("接收到數據[%d]:\n%s\n", n, string(result))
}
啟動程序,測試http的成功響應報文:
啟動程序,測試http的失敗響應報文:
2) 響應報文格式說明
HTTP 響應報文由狀態行、響應頭部、空行、響應包體4個部分組成,如下圖所示:
1) 狀態行
狀態行由 HTTP 協議版本字段、狀態碼和狀態碼的描述文本3個部分組成,他們之間使用空格隔開。
狀態碼:
狀態碼由三位數字組成,第一位數字表示響應的類型,常用的狀態碼有五大類如下所示:
| 狀態碼 |
含義 |
| 1xx |
表示服務器已接收了客戶端請求,客戶端可繼續發送請求 |
| 2xx |
表示服務器已成功接收到請求並進行處理 |
| 3xx |
表示服務器要求客戶端重定向 |
| 4xx |
表示客戶端的請求有非法內容 |
| 5xx |
表示服務器未能正常處理客戶端的請求而出現意外錯誤 |
常見的狀態碼舉例:
| 狀態碼 |
含義 |
| 200 OK |
客戶端請求成功 |
| 400 Bad Request |
請求報文有語法錯誤 |
| 401 Unauthorized |
未授權 |
| 403 Forbidden |
服務器拒絕服務 |
| 404 Not Found |
請求的資源不存在 |
| 500 Internal Server Error |
服務器內部錯誤 |
| 503 Server Unavailable |
服務器臨時不能處理客戶端請求(稍后可能可以) |
2) 響應頭部
響應頭可能包括:
| 響應頭 |
含義 |
| Location |
Location響應報頭域用於重定向接受者到一個新的位置 |
| Server |
Server 響應報頭域包含了服務器用來處理請求的軟件信息及其版本 |
| Vary |
指示不可緩存的請求頭列表 |
| Connection |
連接方式 |
3) 空行
最后一個響應頭部之后是一個空行,發送回車符和換行符,通知服務器以下不再有響應頭部。
4) 響應包體
服務器返回給客戶端的文本信息。
12.3.3 HTTP編程
Go語言標准庫內建提供了net/http包,涵蓋了HTTP客戶端和服務端的具體實現。使用
net/http包,我們可以很方便地編寫HTTP客戶端或服務端的程序。
12.3.3.1 HTTP服務端
示例代碼:
package main
import (
"fmt"
"net/http"
)
//服務端編寫的業務邏輯處理程序
//hander函數: 具有func(w http.ResponseWriter, r *http.Requests)簽名的函數
func myHandler(w http.ResponseWriter, r *http.Request) {
fmt.Println(r.RemoteAddr, "連接成功") //r.RemoteAddr遠程網絡地址
fmt.Println("method = ", r.Method) //請求方法
fmt.Println("url = ", r.URL.Path)
fmt.Println("header = ", r.Header)
fmt.Println("body = ", r.Body)
w.Write([]byte("hello go")) //給客戶端回復數據
}
func main() {
http.HandleFunc("/go", myHandler)
//該方法用於在指定的 TCP 網絡地址 addr 進行監聽,然后調用服務端處理程序來處理傳入的連接請求。
//該方法有兩個參數:第一個參數 addr 即監聽地址;第二個參數表示服務端處理程序,通常為空
//第二個參數為空意味着服務端調用 http.DefaultServeMux 進行處理
http.ListenAndServe("127.0.0.1:8000", nil)
}
瀏覽器輸入url地址:
服務器運行結果:
12.3.3.2 HTTP客戶端
package main
import (
"fmt"
"io"
"log"
"net/http"
)
func main() {
//get方式請求一個資源
//resp, err := http.Get("http://www.baidu.com")
//resp, err := http.Get("http://www.neihan8.com/article/index.html")
resp, err := http.Get("http://127.0.0.1:8000/go")
if err != nil {
log.Println(err)
return
}
defer resp.Body.Close() //關閉
fmt.Println("header = ", resp.Header)
fmt.Printf("resp status %s\nstatusCode %d\n", resp.Status, resp.StatusCode)
fmt.Printf("body type = %T\n", resp.Body)
buf := make([]byte, 2048) //切片緩沖區
var tmp string
for {
n, err := resp.Body.Read(buf) //讀取body包內容
if err != nil && err != io.EOF {
fmt.Println(err)
return
}
if n == 0 {
fmt.Println("讀取內容結束")
break
}
tmp += string(buf[:n]) //累加讀取的內容
}
fmt.Println("buf = ", string(tmp))
}
