Go!
文章目錄
- Go!
- 變量定義
- 基本數據類型
- 流程控制
- 數組
- 切片
- sort包
- 複合數據類型
- 函數
- time包及日期函數
- 指針(童年陰影)
- 結構體(害怕)
- go mod以及go包
- 接口
- goroutine協程
- channel管道
- channel類型
- 創建channel
- channel操作
- goroutine結合channel
- gorutine結合channel實現統計素數
- 單向管道
- select多路複用
- goruntine recover解決協程中出現的panic
- go併發安全和鎖
- 反射
- 文件 目錄操作
變量定義
變量
var
str := "aaa" //自動推斷字符串類型
num := 10 //int類型
var(
n1 = 10
n2 = 20
n3 = 30
)
常量
const
const NAME := "kk"
const(
N1 = 0
N2 = 1
N3 = 3
)
//iota 計數器
const (
a = iota //0
b //1
c //2
_ //跳過
d //4
)
const (
a = iota //0
b = 100 //100
c = iota //2
d //3
)
const (
a, b = iota + 1, iota + 2 //1,2
c, d //2,3
e, f //3,4
)
基本數據類型
int
強制轉換
var a1 int32 = 10
var a2 int64 = 20
fmt.Println(int64(a1) + a2) //30
num := 12
fmt.Printf("num=%v\n", num) //%v 原樣輸出
fmt.Printf("num=%d\n", num) //%d 表示10進制輸出
fmt.Printf("num=%b\n", num) //%b表示二進制輸出
fmt.Printf("num=%o\n", num) //%o八進制輸出
fmt.Printf("num=%x\n", num) //%x表示16進制輸出
float
使用科學計數法表示浮點數據
var f2 float32 = 3.14e2 //表示f2等於3.14*10的2次方
fmt.Printf ("%v--%T",f2,f2)
//314--float32
bool
var flag = true
string
var str = "string"
常用方法
引入strings包
方法 | 介紹 |
---|---|
len(str) | 長度 |
+或fmt.Sprintf | 拼接字符串 |
strings.Split | 分割 |
strings.contains | 判斷是否包含 |
strings.HasPrefix,strings.HasSuffix | 前綴/後綴判斷 |
strings.Index(),strings.LastIndex() | 子串出現的位置 |
strings.join(a[] string, sep string) | join操作 |
byte
字節嘛
rune
這個和char差不多
用增強型for循環遍歷就不會把中文搞開了
s := "你好golang"
for _,v := range s{
fmt.Printf("%v(%c)",v,v)
}
//20320(你) 22909(好) 32( ) 103(g) 111(0) 108(1) 97(a) 110(n) 103(g)
修改字符串
strconv
把其他類型轉換成string類型,也可以反向轉換
i := 20
str1 := strconv.FormatInt(int64(i),10) //兩個參數,要轉換的變量(要求int64),進制
流程控制
for
for i := 0;i<10;i++{
fmt.Print("*")
}
for range
增強型for循環
for k,v := range str{
fmt.Printf("%v---%c",k,v)
}
//str是個字符串數組,k爲字符的下標,v爲字符
//不想打印下標也可以用_省略
for _,v := range str{
fmt.Printf("%c",v)
}
switch
var n = 8
switch n {
case 1,3,5,7,9 :
fmt.Println("奇數")
break
case 2,4,6,8,10:
fmt.Println("偶數")
break
}
注意:
- go中的switch case分支語句可以有多個值
- 每一個case可以不用寫break,不會穿透,不過還是寫上比較好
- 使用
fallthrough
可以進行手動穿透,只能穿透一層
goto
var n = 30
if n > 24{
fmt.Println("成年人")
goto label
}
fmt.Println("aaa")
fmt.Println("bbb")
label:
fmt.Println("ccc")
fmt.Println("ddd")
說明,當走到goto之後,程序會直接跳到對應的label,label中間的就不會被執行了
數組
//1 初始化0值
var nums = [3]int
var strs = [4]string
//2 初始化
var arr = [4]int{0,1,2,3}
//3 自行推斷數組長度
var arr1 = [...]string{"php","java","golang"}
fmt.Println(arr) //[0,1,2,3]
**值類型:**改變變量副本的值,不會改變變量本身的值
**引用類型:**改變變量副本的值,會改變變量本身的值
切片
就是把聲明數組時把長度去掉
var name []int //聲明瞭切片以後,切片的默認值就是nil
基於數組定義切片
a := [5]int{1,2,3,4,5} //定義了一個長度爲5的數組
b := a[:] //獲取數組裏面的所有值
c := a[1:4] //指定獲取數組中的內容組成切片,左閉右開 1,2,3
d := a[2:] //3,4,5
e :=a[:3] //1,2,3
基於切片再切片
a := []string{"北京","上海","廣州","深圳","成都","重慶"}
b := a[1:] //差不多
關於切片的長度和容量
切片擁有自己的長度和容量,我們可以通過使用內置的len()函數求長度,使用內置的cap()函數求切片的容量。
- 長度:切片的長度就是它所包含的元素個數
- 容量:切片的容量是從它的第一個元素開始數,到其底層數組元素末尾的個數。
s := []int{1,2,3,4,5,6}
fmt.Printf("長度%d 容量%d\n", len(s), cap(s)) //長度6容量6
a := s[2:]
fmt.Printf("長度%d 容量%d\n", len(a), cap(a)) //長度4容量4
b := s[1:3]
fmt.Printf("長度%d 容量%d\n", len(b), cap(b)) //長度2容量5 底層數組末尾,所以是5
本質
本質就是對底層數組的封裝,包含三個信息,底層數組的指針,切片長度len,和切片容量cap
make()創建一個切片
var sliceA = make([]int,4,8) //make(切片類型,len,cap) 有默認值,打印一下是[0 0 0 0]
append()
擴容
//golang中沒法通過下標的方式給切片擴容
//golang中給切片擴容的話要用到append()方法
var sliceA []int
sliceA = append( sliceA,12)
sliceA = append(sliceA,24)
//一次傳入多個值
sliceA = append(sliceA,1,2,3,4)
合併
sliceA := []string{"php", "java"}
sliceB := []string{"nodejs", "go"}
sliceA = append(sliceA, sliceB...) //切片合併 [php java nodejs go]
切片的擴容策略
有三種擴容策略,上源碼
newcap := old.cap
doublecap := newcap + newcap
if cap > doublecap {
newcap = cap
} else {
if old.len < 1024 {
newcap = doublecap
} else {
// Check 0 < newcap to detect overflow
// and prevent an infinite loop.
for 0 < newcap && newcap < cap {
newcap += newcap / 4
}
// Set newcap to the requested cap when
// the newcap calculation overflowed.
if newcap <= 0 {
newcap = cap
}
}
}
copy()
sliceA := []int{1, 2, 3, 45}
sliceB := make([]int, 4, 4)
copy(sliceB, sliceA) //直接進行賦值是淺拷貝,引用傳遞,使用copy就是深拷貝,值傳遞了
sliceB[0] = 111 //此時對切片進行改變並不會影響原來切片的值
fmt.Print1n(sliceA)
fmt.Println(sliceB )
從切片中刪除元素
Go語言中並沒有刪除切片元素的專用方法,我們可以使用切片本身的特性來刪除元素
a := []int{30,31 ,32 ,33 ,34,35,36,37} //想要刪除索引爲2的元素
a = append(a[1:2],a[3:]...) //使用append進行操作,appen合併的時候最後一個元素要加...
sort包
升序
sort.Ints() //整型排序
sort.Float64s() //浮點型排序
sort.Strings() //字符串排序
降序
sort.Sort(sort.Reverse(sort.IntSlice(intList))) //整型降序
sort.Sort(sort.Reverse(sort.Float64Slice(floatList))) //浮點型降序
sort.Sort(sort.Reverse(sort.StringSlice(floatList))) //字符串降序
複合數據類型
map
k-v
使用make創建一個map
make(map[KeyType]ValueType,[cap])
cap表示map的初始容量,該參數不是必須的
var userinfo = make(map[string]string)
userinfo["name"] = "張三"
在聲明map的時候直接填充數據
var userinfo = map[string]string{
"username": "張三",
"age": "20",
"gender":"男",
}
循環遍歷map
for k,v := range userinfo{
fmt.Printf("%v:%v",k,v)
}
map的crud
//創建,修改map數據
userinfo := make(map[string]string)
userinfo["name"] = "張三"
userinfo["age"] = "20"
fmt.Println(userinfo) //map[name:張三]
//獲取map的數據
username := userinfo["name"]
fmt.Println(username) //張三
//查看map中是否包含key
v,ok := userinfo["name"]
fmt.Println(v,ok) //張三 true
//v,ok := userinfo["xxx"]
//fmt.Println(v,ok) //(空) false
//刪除map中的kv
delete(userinfo,"name")
fmt.Println(userinfo) //map[age:20]
創建元素爲map類型的切片
//我們想在切片裏面放一系列用戶的信息,這時候我們就可以定義一個元素爲map類型的切片
userinfo := make([]map[string]string,3,3)
fmt.Println(userinfo) //map[] map不初始化的默認值nil
if userinfo[0] == nil {
userinfo[0] = make(map[string]string)
userinfo[0]["name"] = "張三"
userinfo[0]["age"] = "20"
userinfo[0]["height"] = "180cm"
userinfo[0]["gender"] = "男"
}
if userinfo[1] == nil {
userinfo[1] = make(map[string]string)
userinfo[1]["name"] = "李四"
userinfo[1]["age"] = "21"
userinfo[1]["height"] = "179cm"
userinfo[1]["gender"] = "女"
}
for _,v := range userinfo{
fmt.Println(v)
}
/**
[map[] map[] map[]]
map[age:20 gender:男 height:180cm name:張三]
map[age:21 gender:女 height:179cm name:李四]
map[]
*/
將切片作爲map的value
//如果我們想在map對象中存放一系列的屬性的時候,我們就可以把map類型的值定義成切片
userinfo := make(map[string][]string)
userinfo["hobby"] = []string{
"喫飯","睡覺","rip",
}
userinfo["work"] = []string{
"需求","設計","實現",
}
fmt.Println(userinfo)
/**
map[hobby:[喫飯 睡覺 rip] work:[需求 設計 實現]]
*/
map也是引用類型
小練習
//寫一個程序,統計一個字符串中每個單詞出現的次數。比如: "how do you do"中how=1 do=2 you=1
str := "how do you do"
strs := strings.Split(str," ")
count := make(map[string]int)
for _,v := range strs{
count[v]++
}
fmt.Println(count)
函數
函數定義
func 函數名(參數)(返回值){
函數體
}
兩數相加
func twoSum(x int, y int) int {
return x + y
}
func main() {
fmt.Println(twoSum(11,2))
}
如果入參的類型是一樣的,可以省略,直接寫最後
func twoSum(x, y int) int {
return x + y
}
函數的可變參數
理解爲一個切片
func change(x ...int) {
fmt.Printf("%v----%T",x,x)
}
func main(){
change(1,2,3,4,5,9,2) //[1 2 3 4 5 9 2]----[]int
}
//代表參數中,第一個參數賦給x,剩下的都賦給y
func change(x int, y ...int) {
fmt.Printf(x,y)
sum := X
for v := range y{
sum += v
}
return sum
}
多個返回值
func calc(x, y int) (int,int) {
sum := x+y
sub := x-y
return sum, sub
}
//給返回值命名,這樣就不用在函數體中聲明返回值了
func calc(x, y int) (sum, sub int) {
sum = x+y
sub = x-y
return sum, sub
}
封裝一個降序排序函數
func sortIntDesc(since []int) {
sort.Sort(sort.Reverse(sort.IntSlice(since)))
}
函數類型與變量
定義函數類型
type calc func(int, int) int //定義一個爲calc的函數類型
func add(x, y int) int {
return x + y
}
func sub(x, y int) int {
return x - y
}
func calculation(x, y int,op calc) int {
return op(x,y)
}
func main() {
var c calc
c = add
fmt.Println(calculation(2,3,c)) //5
}
/**
也可以傳一個匿名函數
fmt.Println(calculation(2,3, func(x int, y int) int {
return x * y
}))
*/
讓函數返回函數
type calc func(int, int) int //定義一個爲calc的函數類型
func add(x, y int) int {
return x + y
}
func sub(x, y int) int {
return x - y
}
func mul(x, y int) int {
return x * y
}
func div(x, y int) int {
return x / y
}
func myfun(o string) calc {
switch o {
case "+":
return add
case "-":
return sub
case "*":
return mul
case "/":
return div
default:
return nil
}
}
func main() {
c := myfun("*")
fmt.Println(c(2,3)) //6
}
匿名函數和閉包
匿名函數沒有名字
func(參數)(返回值){
函數體
}
func main(){
//匿名函數
func() {
fmt.Println("test..")
}() //在這裏我們加一個() 表示執行這個匿名函數本身 匿名自執行函數
//匿名自執行函數接收參數
func(x, y int) {
fmt.Println(x +y)
}(10, 20)
}
閉包
全局變量特點:
- 常駐內存
- 可能污染全局
局部變量的特點:
- 不常駐內存
- 不污染全局
閉包
- 可以讓一個變量常駐內存
- 可以讓一個變量不污染全局
閉包是指有權訪問另一個函數作用域中的變量的函數
創建閉包的常見的方式就是在一個函數內部創建另一個函數,通過另一個函數訪問這個函數的局部變量
注意:由於閉包裏作用域返回的局部變量資源不會被立刻銷燬回收,所以可能會佔用更多的內存。過度使用閉包會導致性能下降,建議在非常有必要的時候才使用閉包。
func adder() func() int {
var i = 10
return func() int {
return i + 1
}
}
func adder1() func(y int) int {
var i = 10
return func(y int) int {
i += y
return i
}
}
func main() {
var fn = adder() //執行方法
fmt.Println(fn()) //11
fmt.Println(fn()) //11
fmt.Println(fn()) //11
var fn1 = adder1() //執行方法
fmt.Println(fn1(10)) //20
fmt.Println(fn1(10)) //30
fmt.Println(fn1(10)) //40
}
defer
Go語言中的defer語句會將其後面跟隨的語句進行延遲處理。在defer歸屬的函數即將返回時,將延遲處理的語句按defer定義的逆序進行執行,也就是說,先被defer的語句最後被執行,最後被defer的語句,最先被執行。
defer
把語句像棧一樣執行,先進後出
func main() {
fmt.Println("開始")
fmt.Println(1)
fmt.Println(2)
fmt.Println(3)
fmt.Println("結束")
}
/**
開始 1 2 3 結束
*/
func main() {
fmt.Println("開始")
defer fmt.Println(1)
defer fmt.Println(2)
defer fmt.Println(3)
fmt.Println("結束")
}
/**
開始 3 2 1 結束
*/
defer在命名返回值和匿名返回函數中表現不一樣
func f1() {
fmt.Println("開始")
defer func() {
fmt.Println("aaaa")
}() //注意:此處必須是匿名自執行方法
fmt.Println("結束")
}
func main() {
f1()
}
/**
開始
結束
aaaa
*/
//匿名返回值 執行結果是0
func f2() int {
var a int
defer func() {
a++
}()
return a
}
func main() {
fmt.Println(f2()) //0
}
//命名返回值 執行結果是1
func f2() (a int) {
defer func() {
a++
}()
return a
}
func main() {
fmt.Println(f3()) //1
}
defer執行時機
在Go語言的函數中return語句在底層並不是原子操作,它分爲給返回值賦值和RET指令兩步。而defer語句執行的時機就在返回值賦值操作後,RET 指令執行前。具體如下圖所示:
panic + recover
Go語言中目前是沒有異常機制,但是使用panic/recover 模式來處理錯誤。
panic可以在任何地方引發,但recover只有在defer調用的函數中有效。
func fn1() {
fmt.Println("fn1")
}
func fn2() {
panic("拋出一個異常")
}
func main() {
fn1()
fn2()
fmt.Println("結束")
}
/**
程序遇到panic會直接結束運行並拋出異常
*/
使用recover來接收異常
go裏面沒有try catch,所以使用panic和recover來進行異常處理
func fn1() {
fmt.Println("fn1")
}
func fn2() {
defer func() {
err := recover()
if err != nil {
fmt.Println("err:",err)
}
}()
panic("拋出一個異常")
}
func main() {
fn1()
fn2()
fmt.Println("結束")
}
處理異常的例子
func fn3(a,b int) int {
defer func() {
err := recover()
if err != nil {
fmt.Println("error:",err)
}
}()
return a/b
}
func main() {
fmt.Println(fn3(3, 0))
fmt.Println("結束")
}
/**
error: runtime error: integer divide by zero
0
結束
*/
說白了就跟try catch一樣,只不過需要在defer中用recover捕獲異常,注意defer的一定是一個自執行函數
使用recover捕獲異常之後,後續的代碼是可以繼續執行的
例子:模擬讀取文件失敗
func readFile(fileName string) error {
if fileName == "main.go" {
return nil
}else {
return errors.New("讀取文件失敗")
}
}
func myFun() {
defer func() {
err := recover()
if err != nil {
fmt.Println("error:",err,"\n給管理員發郵件")
}
}()
err := readFile("xxx.go")
if err != nil {
panic(err)
}
}
func main() {
myFun()
}
/**
error: 讀取文件失敗
給管理員發郵件
*/
time包及日期函數
最拉誇的方式
func main() {
now := time.Now()
fmt.Println(now) //2020-06-06 14:29:14.8076067 +0800 CST m=+0.011968301
year := now.Year()
month := now.Month()
day := now.Day()
hour := now.Hour()
minute := now.Minute()
second := now.Second()
//year, month, day := now.Date()
fmt.Printf("%d-%02d-%02d %02d:%02d:%02d",year, month, day,hour,minute,second) //2020-06-06 14:38:23
}
格式化
需要注意的是Go語言中格式化時間模板不是常見的Y-m-d H:M:S
而是使用Go的誕生時間2006年1月2號15點04分(記憶口訣爲2006 1 2 3 4)
- 2006 年
- 01 月
- 02 日
- 03 時 12小時制 15 24小時制
- 04 分
- 05 秒
func main() {
now := time.Now()
fmt.Println(now) //2020-06-06 14:44:39.9233244 +0800 CST m=+0.007976001
nowFormat1 := now.Format("2006-01-02 03:04:05")
fmt.Println(nowFormat1) //2020-06-06 02:42:50
nowFormat2 := now.Format("2006-01-02 15:04:05")
fmt.Println(nowFormat2) //2020-06-06 14:44:39
}
獲取當前時間戳
時間戳是自1970年1月1日(08:00:00GMT)至當前時間的總毫秒數。它也被稱爲Unix時間戳(UnixTimestamp) 。
func main() {
now := time.Now()
timeStamp := now.Unix()
fmt.Println(timeStamp) //1591426031
}
將時間戳轉化爲日期格式
func main() {
now := time.Now()
timeStamp := now.Unix()
fmt.Println(timeStamp) //1591426031
//將時間戳轉化爲日期格式
fmt.Println(time.Unix(timeStamp, 0).Format("2006-01-02 15:04:05")) //2020-06-06 14:51:14
}
將日期轉換爲時間戳
順便複習一下recover和panic
func main() {
str := "2020-06-06 14:51:14"
tmp := "2006-01-02 15:04:05"
defer func() {
err := recover()
if err != nil {
fmt.Println("時間轉換錯誤", err)
//str := "2020/06-06 14:51:14"
//時間轉換錯誤 parsing time "2020/06-06 14:51:14" as "2006-01-02 15:04:05": cannot parse "/06-06 14:51:14" as "-"
}
}()
location, err := time.ParseInLocation(tmp, str, time.Local)
if err != nil {
panic(err)
} else {
unix := location.Unix()
fmt.Println("時間戳是", unix) //時間戳是 1591426274
}
}
時間間隔類型常量
1、time包中定義的時間間隔類型的常量如下:
const (
Nanosecond Duration = 1
Microsecond = 1000 * Nanosecond
Millisecond = 1000 * Microsecond
Second = 1000 * Millisecond
Minute = 60 * Second
Hour = 60 * Minute
)
func main(){
// fmt.Println(time.Millisecond) //1ms
// fmt.Println(time.Second) //1s
}
時間操作函數
Add
時間+時間間隔
func (t Time) Add(d Duration) Time
Sub 時間差
Equal 比較時間,會考慮時區
定時器
使用time.NewTicker(時間間隔)來設置定時器
func main(){
ticker := time.NewTicker(time.Second)
n := 0
for t := range ticker.C{
n++
if n > 5 { //一共執行5次任務
ticker.Stop() //停止計時器
return
}
fmt.Println(t)
fmt.Println("offer")
}
}
使用time.Sleep()休眠
func main(){
for i := 5 ;i > 0 ;i-- {
fmt.Println("offer")
time.Sleep(time.Second) //休眠1秒,相當於TimeUnit.SECOND.sleep(1)
}
}
指針(童年陰影)
指針也是一個變量,但它是一種特殊的變量,它存儲的數據不是一個普通的值,而是另一個變量的內存地址。
func main() {
a := 10
p := &a
fmt.Printf("a: %v,%T,%p\n",a,a,&a)
fmt.Printf("p: %v,%T,%p\n",p,p,&p)
fmt.Printf("%v",*p) //*p : 取出這個地址對應的值,即a的值
}
/**
a: 10,int,0xc00000a0a8
p: 0xc00000a0a8,*int,0xc000006028
10
*/
指針地址和指針類型
每個變量在運行時都擁有一個地址,這個地址代表變量在內存中的位置。Go語言中使用&字符放在變量前面對變量進行取地址操作。Go 語言中的值類型(int、 float、 bool、 string、array、struct) 都有對應的指針類型,如: *int、 *int64、 *string 等。
簡單例子
func fun1(x int) {
x = 20 //值傳遞
}
func fun2(x *int) {
*x = 40 //引用傳遞
}
func main() {
a := 10
fun1(a)
fmt.Println(a) //10
fun2(&a)
fmt.Println(a) //40
}
指針也是引用類型
這樣寫會報錯
var a *int
*a = 100
fmt.Println(*a)
使用new關鍵字創建初始化指針變量
new在實際開發中基本用不到**(shit! , java程序員直呼外行)**
func main() {
a := new(int) //a是一個指針變量 類型是*int的指針類型 指針變量對應的值是0
fmt.Printf("%v--%T--%v",a,a,*a) //0xc00000a0a8--*int--0
}
make函數分配內存
主要用於切片,map,channel
的內存創建
make和new的區別
- 二者都是用來做內存分配的。
- make只用於slice、map以及channel的初始化,返回的還是這三個引用類型本身
- 而new用於類型的內存分配,並且內存對應的值爲類型零值,返回的是指向類型的指針。
結構體(害怕)
type自定義類型與類型別名的區別
func main() {
var a myInt = 10
fmt.Printf("%v,%T\n",a,a) //10,main.myInt
var b myFloat = 12.3
fmt.Printf("%v,%T\n",b,b) //12.3,float64
}
結構體的定義
使用type和struct關鍵字來定義結構體,具體代碼格式如下
type 類型名 struct {
字段名 字段類型
字段名 字段類型
}
其中:
- ==類型名:==表示自定義結構體的名稱,在同一個包內不能重複。
- ==字段名:==表示結構體字段名。結構體中的字段名必須唯一。
- ==字段類型:==表示結構體字段的具體類型。
我們定義一個Person結構體
注意,結構體名字首字母可以大寫也可以小寫,小寫代表他是私有的,字段也是一樣的
type Person struct {
name string
age int
gender string
}
結構體實例化
第一種方法 var
(小聲BB:相當於java裏面的setter)
結構體類型
var 結構體實例 結構體類型
func main(){
var p1 Person
p1.name = "張三"
p1.age = 20
p1.gender = "男"
fmt.Printf("%v--%T\n",p1,p1) //{張三 20 男}--main.Person
fmt.Printf("%#v--%T",p1,p1) //main.Person{name:"張三", age:20, gender:"男"}--main.Person
}
%#v
代表詳細信息(相當於toString)
第二種方式 new
(java菜逼直呼內行)
指針類型
func main(){
var p2 = new(Person)
p2.name = "李四"
p2.age = 20
p2.gender = "男"
fmt.Printf("%#v--%T",p2,p2) //&main.Person{name:"李四", age:20, gender:"男"}--*main.Person
}
從打印的結果中我們可以看出p2是一個結構體指針。
注意:在Golang中支持對結構體指針直接使用.
來訪問結構體的成員。p2.pname = "張三”
其實在底層是(*p2).name = "張三”
第三種方式 直接結構體地址賦值
(無參構造器?)
指針類型
func main(){
p3 := &Person{}
p3.name = "花玲"
p3.age = 18
p3.gender = "女"
fmt.Printf("%#v--%T",p3,p3) //&main.Person{name:"花玲", age:18, gender:"女"}--*main.Person
}
第四種方式 鍵值對賦值
(有參構造器?)
結構體類型
func main(){
p4 := Person{
name: "花玲",
age: 18,
gender: "女",
}
fmt.Printf("%#v--%T",p4,p4) //main.Person{name:"花玲", age:18, gender:"女"}--main.Person
}
第五種方式 指針加賦值
指針類型
func main(){
p5 := &Person{
name: "花玲",
age: 18,
gender: "女",
}
fmt.Printf("%#v--%T", p5, p5) //&main.Person{name:"花玲", age:18, gender:"女"}--*main.Person
}
注意,初始化的時候可以只給部分字段賦值,剩下的就是默認值,0值或空值
第六種方式 不加字段名,但是要一一對應
指針類型
func main(){
p6 := &Person{
"花玲",
18,
"女",
}
fmt.Printf("%#v--%T", p6, p6) //&main.Person{name:"花玲", age:18, gender:"女"}--*main.Person
}
結構體方法和接收者
在go語言中,沒有類的概念但是可以給類型(結構體,自定義類型)定義方法。
所謂方法就是定義了接收者的函數。
接收者的概念就類似於其他語言中的this或者self。
方法定義的格式如下
func (接收者變量 接收者類型) 方法名(參數列表) (返回參數) {
函數體
}
其中:
- **接收者變量:**接收者中的參數變量名在命名時,官方建議使用接收者類型名的第一個小寫字母,而不是self、 this 之類的命名。例如,Person 類型的接收者變量應該命名爲p,Connector類型的接收者變量應該命名爲c等。
- **接收者類型:**接收者類型和參數類似,可以是指針類型和非指針類型。
定義一個Person結構體,定義一個方法打印person的信息
type Person2 struct {
name string
age int
gender string
height int
}
func (p Person2) PrintInfo() {
fmt.Printf("姓名:%v 年齡:%v", p.name, p.age)
}
func main() {
p1 := Person2{
name: "花玲",
age: 18,
gender: "女",
height: 165,
}
p1.PrintInfo() //姓名:花玲 年齡:18
}
定義一個方法,修改對象信息
注意,想要修改屬性,接收者類型必須指定爲結構體指針
當然,不修改結構體中的屬性的話,不用指定爲結構體指針
func (p Person2) PrintInfo() {
fmt.Printf("姓名:%v 年齡:%v\n", p.name, p.age)
}
//這裏
func (p *Person2) SetName(name string) {
p.name = name
}
func main() {
p1 := Person2{
name: "花玲",
age: 18,
gender: "女",
height: 165,
}
p1.PrintInfo() //姓名:花玲 年齡:18
p1.SetName("老婆")
p1.PrintInfo() //姓名:老婆 年齡:18
}
注意:結構體實例是獨立的,不會相互影響
結構體嵌套和繼承
結構體的匿名字段
就是不給字段取名字,直接定義類型
type person struct {
string
int
}
結構體嵌套
聽起來好高端,就是DI
type User struct {
username string
password string
address Address //表示User結構體裏面嵌套了Address結構體
}
type Address struct {
name string
phone string
city string
}
func main() {
var u User
u.username = "小明"
u.password = "123456"
u.address = Address{
name: "小明",
phone: "13996459090",
city: "重慶",
}
/**
* 也可以這樣
* u.address.name = "小明"
* u.address.phone = "13996459090"
* u.address.city = "重慶"
*/
fmt.Println(u) //{小明 123456 {小明 13996459090 重慶}}
}
當然,你可以寫匿名的嵌套結構體,比如u.city = "重慶"
,由於User結構體本身並沒有city這個字段,所以賦值的時候他會去user裏面的嵌套結構體裏找這個字段並給他賦值
關於嵌套結構體的字段名衝突
- 如果結構體中有和嵌套結構體中相同的某一字段,那麼賦值的時候會給結構體本身的字段賦值
- 如果有兩個嵌套結構體,他們有相同的字段,那麼賦值的時候會報錯,指定是哪一個嵌套結構體就行了
u.Address.Addtime = "2006-01-01"
結構體的繼承
通過匿名嵌套結構體實現繼承
就這??雖然拉胯,不過確實是實現了繼承
type Animal struct {
name string
}
func (a Animal) run() {
fmt.Printf("%v在運動\n",a.name)
}
type Dog struct {
age int
Animal
}
func (d Dog) shut() {
fmt.Printf("%v在叫\n",d.name)
}
func main() {
var dog Dog
dog.age = 2
dog.name = "旺財"
dog.run() //旺財在運動
dog.shut() //旺財在叫
}
結構體序列化與反序列化Json
注意,想要將字段轉換成json的話,字段必須是公有的(即首字母大寫)
GolangJSON序列化是指把結構體數據轉化成JSON格式的字符串,Golang JSON的反序列化是指把JSON數據轉化成Golang中的結構體對象
序列化
通過json.Marshal()
序列化
type Student struct {
Id int
Gender string
Name string
Sno string
}
func main() {
s1 := Student{
Id: 12,
Gender: "男",
Name: "張三",
Sno: "s001",
}
fmt.Println(s1) //{12 男 張三 s001}
marshal, _ := json.Marshal(s1)
jsonStr := string(marshal)
fmt.Println(jsonStr) //{"Id":12,"Gender":"男","Name":"張三","Sno":"s001"}
}
反序列化
type Student struct {
Id int
Gender string
Name string
Sno string
}
func main() {
var str = `{"Id":12,"Gender":"男","Name":"張三","Sno":"s001"}` //注意此處是`` 而不是''
var s1 Student
err := json.Unmarshal([]byte(str),&s1)
if err != nil {
fmt.Println(err)
}
fmt.Printf("%#v",s1) //main.Student{Id:12, Gender:"男", Name:"張三", Sno:"s001"}
}
結構體標籤Tag
我想在json序列化之後將json中的字段首字母改爲小寫
type Student struct {
Id int `json:"id"`
Gender string `json:"gender"`
Name string `json:"name"`
Sno string `json:"xxx"`
}
func main() {
s1 := Student{
Id: 12,
Gender: "男",
Name: "張三",
Sno: "s001",
}
fmt.Println(s1) //{12 男 張三 s001}
marshal, _ := json.Marshal(s1)
jsonStr := string(marshal)
fmt.Println(jsonStr)
//{"Id":12,"Gender":"男","Name":"張三","Sno":"s001"} 原來的
//{"id":12,"gender":"男","name":"張三","xxx":"s001"} 加了Tag
}
嵌套結構體的序列化與反序列化
序列化
type Student struct {
Id int
Gender string
Name string
}
type Class struct {
Title string
Students []Student
}
func main() {
class := Class{
Title: "6班",
Students: make([]Student, 0,200),
}
for i := 0; i < 10; i++ {
s := Student{
Id: i,
Gender: "女",
Name: fmt.Sprintf("學生_%v",i),
}
class.Students = append(class.Students,s)
}
fmt.Println(class)
marshal, _ := json.Marshal(class)
jsonStr := string(marshal)
fmt.Println(jsonStr)
}
/**
{6班 [{0 女 學生_0} {1 女 學生_1} {2 女 學生_2} {3 女 學生_3} {4 女 學生_4} {5 女 學生_5} {6 女 學生_6} {7 女 學生_7} {8 女 學生_8} {9 女 學生_9}]}
{"Title":"6班",
"Students":[{"Id":0,"Gender":"女","Name":"學生_0"},{"Id":1,"Gender":"女","Name":"學生_1"},{"Id":2,"Gender":"女","Name":"學生_2"},{"Id":3,"Gender":"女","Name":"學生_3"},{"Id":4,"Gender":"女","Name":"學生_4"},{"Id":5,"Gender":"女","Name":"學生_5"},{"Id":6,"Gender":"女","Name":"學生_6"},{"Id":7,"Gender":"女","Name":"學生_7"},{"Id":8,"Gender":"女","Name":"學生_8"},{"Id":9,"Gender":"女","Name":"學生_9"}]}
*/
使用json解析工具可以看到,我們是解析成功的
反序列化
type Student struct {
Id int
Gender string
Name string
}
type Class struct {
Title string
Students []Student
}
func main(){
jsonStr := `{"Title":"6班","Students":[{"Id":0,"Gender":"女","Name":"學生_0"},{"Id":1,"Gender":"女","Name":"學生_1"},{"Id":2,"Gender":"女","Name":"學生_2"},{"Id":3,"Gender":"女","Name":"學生_3"},{"Id":4,"Gender":"女","Name":"學生_4"},{"Id":5,"Gender":"女","Name":"學生_5"},{"Id":6,"Gender":"女","Name":"學生_6"},{"Id":7,"Gender":"女","Name":"學生_7"},{"Id":8,"Gender":"女","Name":"學生_8"},{"Id":9,"Gender":"女","Name":"學生_9"}]}`
var class Class
err := json.Unmarshal([]byte(jsonStr), &class)
if err != nil {
fmt.Println(err)
}else {
fmt.Println(class)
}
}
//{6班 [{0 女 學生_0} {1 女 學生_1} {2 女 學生_2} {3 女 學生_3} {4 女 學生_4} {5 女 學生_5} {6 女 學生_6} {7 女 學生_7} {8 女 學生_8} {9 女 學生_9}]}
go mod以及go包
- 包( package)是多個Go源碼的集合,是一種高級的代碼複用方案,Go 語言爲我們提供了很多內置包,如fmt、strconv、 strings、 sort、 errors、 time、 encoding/json、 OS、io 等。
- Golang中的包可以分爲三種: 1.系統內置包 2.自定義包 3.第三方包
- 系統內置包: Golang語言給我們提供的內置包,引入後可以直接使用,如fmt、strconv、strings、sort、errors、 time、encoding/json、 OS、io 等。
- 自定義包:開發者自己寫的包
- 第三方包:屬於自定義包的一種,需要下載安裝到本地後纔可以使用,如前面給大家介紹的"github.com/shopspring/decimal"包解決float精度丟失問題。
go包管理工具go mod
在Golang1.11版本之前如果我們要自定義包的話必須把項目放在GOPATH目錄。Go1.11版本之後無需手動配置環境變量,使用go mod管理項目,也不需要非得把項目放到GOPATH指定目錄下,你可以在你磁盤的任何位置新建一個項目, Go1.13以後可以徹底不要GOPATH了。
go mod init 初始化項目
實際項目開發中我們首先要在我們項目目錄中用go mod
命令生成一個go.mod文件
管理我們項目的依賴。
比如我們的golang項目文件要放在了某個文件夾
,這個時候我們需要在這個文件夾裏面
使用go mod
命令生成一個go.mod文件
go mod簡介及參數
某些同學用golan的時候import會爆紅,但是又能正常使用包裏面的方法
看看你的go moudles有沒有開啓
另外,通過命令修改go的module開啓,以及國內代理設置
go env # 查看go的環境設置
設置命令
go env -w GO111MODULE=on
go env -w GOPROXY=https://goproxy.cn,direct
包名很長,給包取一個別名
import (
"fmt"
T "gomod/calc"
)
func main() {
sum := T.Add(10,2)
fmt.Println(sum)
}
init()函數
在Go語言程序執行時導入包語句會自動觸發包內部init()函數的調用。需要注意的是:init()函數沒有參數也沒有返回值。init()函 數在程序運行時自動被調用執行,不能在代碼中主動調用它。
在運行時,被最後導入的包會最先初始化並調用其init()函數,如下圖示:
import (
"fmt"
T "gomod/calc"
)
func init() { //系統自動執行,不需要手動調用
fmt.Println("init........")
}
func main() {
sum := T.Add(10,2)
fmt.Println(sum)
}
/**
init........
12
*/
go中使用第三方包
我們可以在https://pkg.go.dev/ 查找看常見的golang第三方包
找到我們需要下載安裝的第三方包的地址
比如前面給大家演示的解決float精度損失的包decimal https://github.com1/shopspring/decimal
安裝這個包
第一種方法
go get 包名稱
(全局)
go get github.com/shopspring/decimal
第二種方法
go mod download
(全局)
第三種方式
go mod vender
將依賴複製到當前項目的vender下(本項目)
引入這個包
可以在第三方包的文檔裏面看到該怎麼引入
一般是import go get後的地址
哎反正你import包就完事兒了,golan會幫你下載的
接口
接口是什麼在這裏不做闡述,java開發的同學應該很清楚,接口就是一系列動作的規範
接口的定義
- 在Golang中接口(interface) 是一種類型,一種抽象的類型。接口(interface) 是一-組函數method的集合,Golang 中的接口不能包含任何變量。
- 在Golang中接口中的所有方法都沒有方法體,接口定義了一個對象的行爲規範,只定義規範不實現。接口體現了程序設計的多態和高內聚低耦合的思想
- Golang中的接口也是一種數據類型,不需要顯示實現。只需要-一個變量含有接口類型中的所有方法,那麼這個變量就實現了這個接口。
type 接口名 interface{
方法名1( 參數列表1 ) 返回值列表1
方法名2( 參數列表2 ) 返回值列表2
...
}
- **接口名:**使用type將接口定義爲自定義的類型名。Go語言的接口在命名時,一般會在單詞後面添加er,如有寫操作的接口叫Writer,字符串功能的接口叫Stringer等。接口名最好要能突出該接口的類型含義。
- **方法名:**當方法名首字母是大寫且這個接口類型名首字母也是大寫時,這個方法可以被接口所在的包(package) 之外的代碼訪問。
- 參數列表,返回值列表: 參數列表和返回值列表中的參數變量名可以省略
如果接口裏面有方法,必須通過結構體或者自定義類型實現接口
哎,就是實現類 implements,懂?
哎,乏味
type Usber interface {
start()
stop()
}
type Phone struct {
Name string
}
func (p Phone) start() {
fmt.Println(p.Name,"啓動")
}
func (p Phone) stop() {
fmt.Println(p.Name,"關機")
}
func main() {
var p1 Usber = Phone{Name: "華爲"} //就和java一樣,接口接收實現類
p1.start() //華爲 啓動
}
當然,衆所周知,接口裏面沒有定義的方法,是不能調用的
比如你的實現類中有自己的方法,通過接口是不能調用到的,但是爲什麼要這樣做呢?接口都是行爲規範了.
比如java中的List接口中沒有定義LinkedList的相關方法,所以如果你用List去接,是調用不到的,你要用LinkedList去接.
這就變成了直接使用實現類(結構體).
再來看一個例子
java開發的同學都知道,其實應該把各個接口和實現類分開放到不同的go中,但是這裏方便展示就放一起了
這裏打的電腦的work方法接收的參數是一個接口類型,實際上是體現了多態,這裏不多贅述
type Usber interface {
start()
work()
stop()
}
//電腦
type Computer struct {
}
func (c Computer) work(usb Usber) {
usb.start()
usb.work()
usb.stop()
}
//手機
type Phone struct {
}
func (p Phone) start() {
fmt.Println("手機啓動")
}
func (p Phone) stop() {
fmt.Println("手機關機")
}
func (p Phone) work(){
fmt.Println("手機工作")
}
//相機
type Camera struct {
}
func (c Camera) start() {
fmt.Println("相機啓動")
}
func (c Camera) stop() {
fmt.Println("相機關機")
}
func (c Camera) work(){
fmt.Println("相機工作")
}
func main() {
var computer = Computer{}
var phone Usber = Phone{}
var camera Usber = Camera{}
computer.work(phone)
computer.work(camera)
}
/**
手機啓動
手機工作
手機關機
相機啓動
相機工作
相機關機
*/
空接口
Serializable?其實有差別
空接口代表沒有任何約束,可以接收任意類型
空接口作爲函數的參數進行
//空接口測試
func show(a interface{}) {
fmt.Printf("值:%v,類型:%T",a,a)
}
func main() {
str := "這是一個測試"
show(str) //值:這是一個測試,類型:string
}
map的值實現空接口
這樣就可以往這個map中傳任意類型了,就好比Map<String,Object>
func main(){
map1 := make(map[string]interface{})
map1["1"] = "string類型"
map1["2"] = 2
map1["3"] = []string{
"切片類型1","切片類型2",
}
show(map1)
}
/**
值:map[1:string類型 2:2 3:[切片類型1 切片類型2]],類型:map[string]interface {}
*/
切片值實現空接口
同理,Object類型的list
func main(){
arr := make([]interface{},3,3)
arr[0] = "string"
arr[1] = 1
arr[2] = 2.84
show(arr)
}
/**
值:[string 1 2.84],類型:[]interface {}
*/
類型斷言
相當於instanceof
一個接口的值(簡稱接口值)是由一個具體類型和具體類型的值兩部分組成的。這兩部分分別稱爲接口的動態類型和動態值。
如果我們想要判斷空接口中值的類型,那麼這個時候就可以使用類型斷言,其語法格式:
x.(T)
- x:表示類型爲interface{}的變量
- T:表示斷言x可能是的類型。
該語法返回兩個參數,第- 個參數是x轉化爲T類型後的變量,第二個值是一個布爾值,若爲true則斷言成功,false則失敗
func main(){
var a interface{} = "你好"
v,ok := a.(string)
if ok {
fmt.Println("a是一個string類型",v)
}else {
fmt.Println("斷言失敗")
}
}
/**
a是一個string類型 你好
*/
注意:類型.(type)
只能結合switch
語句使用
定義一個方法,可以傳入任意數據類型,然後根據不同的類型實現不同的功能
func justifyType(x interface{}) {
switch v := x.(type) {
case string:
fmt.Printf("x is a string, value is %v\n", v)
case int:
fmt.Printf("x is a int is %v\n", v)
case bool:
fmt.Printf("x is a bool is %v\n", v)
default:
fmt.Println("unsupport type! ")
}
}
再比如
func (c Computer) work(usb Usber) {
//根據傳入接口的不同類型執行不同的邏輯
switch usb.(type) {
case Phone:
usb.start()
case Camera:
usb.stop()
}
}
func main() {
var computer = Computer{}
var phone Usber = Phone{}
var camera Usber = Camera{}
computer.work(phone)
computer.work(camera)
}
/**
手機啓動
相機關機
*/
結構體值接收者和指針接收者實現接口的區別
值接收者
如果結構體中的方法是值接收者,那麼實例化後的結構體值類型和結構體指針類型都可以賦值給接口變量
type Usber interface {
start()
work()
stop()
}
//手機
type Phone struct {
}
func (p Phone) start() { //值接收者
fmt.Println("手機啓動")
}
func (p Phone) stop() {
fmt.Println("手機關機")
}
func (p Phone) work(){
fmt.Println("手機工作")
}
func main() {
//結構體值接收者,實例化後的結構體值類型和結構體指針類型都可以賦值給接口變量
var p1 Usber = Phone{}
var p2 Usber = &Phone{}
p1.start()
p2.stop()
}
指針接收者
結構體指針接收者,實例化後只有結構體指針類型纔可以賦值給接口變量
type Usber interface {
start()
work()
stop()
}
//手機
type Phone struct {
}
func (p *Phone) start() { //值接收者
fmt.Println("手機啓動")
}
func (p *Phone) stop() {
fmt.Println("手機關機")
}
func (p *Phone) work(){
fmt.Println("手機工作")
}
func main() {
//結構體指針接收者,實例化後只有結構體指針類型纔可以賦值給接口變量
var p1 Usber = Phone{} //報錯 Phone does not implement Usber (start method has pointer receiver)
var p2 Usber = &Phone{}
p1.start()
p2.stop()
}
接口方法有返回值的情況
哎其實差不多,不過因爲要修改結構體屬性值,所以注意方法要使用指針類型
type Animal interface {
SetName(string)
GetName() string
}
type Dog struct {
Name string
}
func (d *Dog) SetName(name string) {
d.Name = name
}
func (d Dog) GetName() (name string) {
return d.Name
}
type Cat struct{
Name string
}
func (c *Cat) SetName(name string) {
c.Name = name
}
func (c Cat) GetName() string {
return c.Name
}
func main() {
var dog Animal = &Dog{Name: "wangcai"}
var cat Animal = &Cat{Name: "xiaohei"}
fmt.Println(dog.GetName())
dog.SetName("旺財")
fmt.Println(dog.GetName())
fmt.Println(cat.GetName())
cat.SetName("小黑")
fmt.Println(cat.GetName())
}
/**
wangcai
旺財
xiaohei
小黑
*/
一個結構體實現多個接口,接口嵌套
多態?
使用接口嵌套達到結構體實現多個接口的目的
當然,儘量遵守單一職責設計原則,一個接口儘量不耦合其它接口,另外定義一個接口來組合已有的接口
type Animal interface {
SetName(string)
GetName() string
}
type Action interface {
run()
sleep()
}
//定義一個接口,要求實現該接口的結構體實現包含接口的所有方法
type MovingAnimal interface {
Animal
Action
}
type Dog struct {
Name string
}
func (d *Dog) SetName(name string) {
d.Name = name
}
func (d Dog) GetName() (name string) {
return d.Name
}
func (d Dog) run() {
fmt.Println(d.Name,"在奔跑")
}
func (d Dog) sleep() {
fmt.Println(d.Name,"在睡覺")
}
func main() {
var dog MovingAnimal = &Dog{Name: "wangcai"}
fmt.Println(dog.GetName())
dog.SetName("旺財")
fmt.Println(dog.GetName())
dog.run()
dog.sleep()
}
/**
wangcai
旺財
旺財 在奔跑
旺財 在睡覺
*/
空接口和類型斷言的使用細節
空接口類型是不能進行索引或者說拿到結構體中的值的,通過類型斷言就可以做到,具體看下面的示例
type Address struct {
Name string
Phone int
}
//Golang中空接口和類型斷言使用細節
func main() {
var userinfo = make(map[string]interface{})
userinfo["username"] = "花玲"
userinfo["age"] = 18
userinfo["hobby"] = []string{"喫飯", "睡覺", "我"}
fmt.Println(userinfo["username"]) //花玲
fmt.Println(userinfo["age"]) //18
//fmt.Println(userinfo["hobby"][1]) //type interface {} does not support indexing
address := Address{
Name: "ky",
Phone: 123456789,
}
userinfo["address"] = address
//fmt.Println(userinfo["age"].Name) //type interface {} is interface with no methods
//解決方案,類型斷言
hobby,_ := userinfo["hobby"].([]string)
fmt.Println(hobby[2]) //我
addr,_ := userinfo["address"].(Address)
fmt.Println(addr.Name,addr.Phone) //ky 123456789
}
goroutine協程
爲什麼要使用goroutine
**需求: **要統計1-10000006的數字中那些是素數,並打印這些素數?
素數: 就是除了1和它本身不能被其他數整除的數
實現方法:
- 傳統方法,通過一個for循環判斷各個數是不是素數
- 使用併發或者並行的方式,將統計素數的任務分配給多個goroutine去完成,這個時候就用到了goroutine
- goroutine 結合channel
Golang中的協程(goroutine) 以及主線程
golang中的主線程: ( 可以理解爲線程/也可以理解爲進程),在一個Golang程序的主線程上可以起多個協程。Golang 中多協程可以實現並行或者併發。
協程: 可以理解爲用戶級線程,這是對內核透明的,也就是系統並不知道有協程的存在,是完全由用戶自己的程序進行調度的。Golang的一大特色就是從語言層面原生支持協程,在函數或者方法前面加go 關鍵字就可創建一個協程。可以說Golang中的協程就是goroutine。
Golang中的多協程有點類似其他語言中的多線程。
多協程和多線程: Golang中每個goroutine (協程)默認佔用內存遠比Java、C的線程少。
OS線程(操作系統線程)一般都有固定的棧內存(通常爲2MB左右) ,一個goroutine (協程)佔用內存非常小,只有2KB左右,多協程goroutine切換調度開銷方面遠比線程要少。
這也是爲什麼越來越多的大公司使用Golang的原因之一。
Goroutine的使用以及sync.WaitGroup
並行執行需求:
- 在主線程(可以理解成進程)中,開啓一個goroutine,該協程每隔50毫秒秒輸出"你好golang"
- 在主線程中也每隔50毫秒輸出"你好golang",輸出10次後,退出程序,要求主線程和goroutine同時執行。
func test() {
for i := 0; i < 10; i++ {
fmt.Println("test() hello golang")
time.Sleep(time.Millisecond * 50)
}
}
func main() {
go test() //開啓一個協程
for i := 0; i < 10; i++ {
fmt.Println("main() hello golang")
time.Sleep(time.Millisecond * 50)
}
}
問題?
如果主線程中任務的執行速度比協程中的任務執行速度快,會出現什麼問題?
當主線程執行結束,協程無論是否執行完畢,都會停止執行.
使用sync.WaitGroup
這是個什麼東西?
java開發的同學應該知道,其實這就是個CountdownLatch,等我娓娓道來
var wg sync.WaitGroup
func test() {
for i := 0; i < 10; i++ {
fmt.Println("test() hello golang--",i)
time.Sleep(time.Millisecond * 100)
}
wg.Done() //計數器減一
}
func main() {
wg.Add(1) //計數器加一
go test() //開啓一個協程
for i := 0; i < 10; i++ {
fmt.Println("main() hello golang--",i)
time.Sleep(time.Millisecond * 50)
}
wg.Wait()
fmt.Println("主線程執行結束")
}
其中
wg.Add(1) 相當於CowndownLatch的初始化爲1
wg.Done() 相當於CowndownLatch的countDown()方法
wg.wait() 相當於CowndownLatch的await()
這樣說好理解了嗎?
當然,可以開啓多個協程任務
這裏我們開啓了5個協程去執行test方法,每個test方法裏打印10條語句
var wg sync.WaitGroup
func test(x int) {
defer wg.Done()
for i := 1; i <= 10; i++ {
fmt.Println("協程--",x,"--",i)
}
}
func main() {
for i := 1; i <= 5 ; i++ {
go test(i)
wg.Add(1)
}
wg.Wait()
}
設置golang並行運行時候佔用的cpu數量
Go運行時的調度器使用GOMAXPROCS參數來確定需要使用多少個OS線程來同時執行Go代碼。默認值是機器上的CPU核心數。例如在一個8核心的機器上,調度器會把Go代碼同時調度到8個OS線程上。
Go語言中可以通過runtime.GOMAXPROCS()函數設置當前程序併發時佔用的CPU邏輯核心數。
Go1.5版本之前,默認使用的是單核心執行。Go1.5 版本之後,默認使用全部的CPU邏輯核心數。
一個需求
統計1-120000內的素數,for循環實現
func main() {
start := time.Now().Unix()
for i := 2; i <= 120000; i++ {
flag:=true
for j := 2; j <= i/2 ; j++ {
if i%j == 0 {
flag = false
break
}
}
if flag {
//fmt.Print(i," ")
}
}
end := time.Now().Unix()
fmt.Println(end-start) // j<i/2 耗時3毫秒 j<i 耗時5毫秒
}
開啓多個協程
func cal(n int) {
start := (n-1)*30000 + 1
end := n * 30000
defer wg.Done()
for i := start; i <= end; i++ {
if i == 1 {
continue
}
flag := true
for j := 2; j <= i/2; j++ {
if i%j == 0 {
flag = false
break
}
}
if flag {
fmt.Print(i,"是素數\n")
}
}
}
func main() {
//統計1-120000內的素數,協程實現
start := time.Now().Unix()
for i := 1; i <= 4; i++ {
wg.Add(1)
go cal(i)
}
wg.Wait()
end := time.Now().Unix()
fmt.Println(end - start) //2毫秒
}
channel管道
-
管道是Go語言在語言級別上提供的goroutine間的通訊方式,我們可以使用channel在多個goroutine之間傳遞消息。如果說goroutine是Go程序併發的執行體,channel就是它們之間的連接。channel是可以讓一- 個goroutine發送特定值到另-個 goroutine 的通信機制。
-
Go語言的併發模型是CSP ( Communicating Sequential Processes),提倡通過通信共享內存而不是通過共享內存而實現通信。
-
Go語言中的管道(channel) 是一種特殊的類型。管道像一個傳送帶或者隊列,總是遵循先入先出(First In First Out)的規則,保證收發數據的順序。每一個管道都是一個具體類型的導管,也就是聲明channel的時候需要爲其指定元素類型。
channel類型
channel是一種類型, 一種引用類型。聲明管道類型的格式如下:
var 變量 chan 元素類型
舉幾個例子:
var ch1 chan int //聲明一個傳遞整型的管道
var ch2 chan bool //聲明一個傳遞布爾型的管道
var ch3 chan []int //聲明一個傳遞int切片的管道
創建channel
聲明的管道後需要使用make函數初始化之後才能使用。
make(chan 元素類型,容量)
//創建一個能存儲10個int 類型數據的管道
ch1 := make(chan int, 10)
//創建一一個能存儲4個bool類上數據的管道
ch2 := make(chan bool, 4)
//創建一個 能存儲3個[]int 切片類型數據的管道
ch3 := make(chan []int, 3)
channel操作
管道有``發送(send) 、接收(receive) 和關閉(close) 三種操作。 發送和接收都使用
<-`符號的
現在我們先使用以下語句定義一個管道:
ch := make(chan int, 3)
1.發送(將數據放在管道內)
將一個值放進管道
ch <- 10 //把10發送到ch中
2.接收(從管道內取值)
x := <- ch //從ch中取值並賦值給變量x
3.關閉管道
close(ch)
管道是引用數據類型
func main() {
//1.創建channel
ch := make(chan int, 3)
//2.給管道里面發送數據
ch <- 3
ch <- 4
ch <- 5
//3.獲取管道內容
a := <-ch
fmt.Println(a) //3
<-ch //從管道里取值 //4
c := <-ch
fmt.Println(c) //5
ch <- 6
ch <- 7
//4.管道的容量,和長度
fmt.Printf("值:%v,容量:%v,長度:%v",ch,cap(ch),len(ch)) //值:0xc00007e000,容量:3,長度2
//6.管道阻塞
ch1:=make(chan int,1)
ch1 <- 2
ch1 <- 3 //all goroutines are asleep - deadlock!
}
關於管道阻塞
- 管道滿了,添加(deadlock)
- 管道空了,取值(deadlock)
在沒有使用協程的情況下,如果我們的管道數據已經全部取出,再取就會報告deadlock
管道的循環遍歷
循環遍歷管道數據
使用for range遍歷通道,當通道被關閉的時候就會退出for range, 如果沒有關閉管道就會報個錯誤
func main() {
var ch1 = make(chan int, 10)
for i := 0; i < 10; i++ {
ch1 <- i
}
close(ch1)
//for range循環遍歷管道的值 管道沒有key
for v := range ch1 {
fmt.Println(v) //如果不關閉管道會報錯fatal error: all goroutines are asleep - deadlock!
}
}
但是使用for循環不關閉管道不會有這個問題
我覺得細細想一下,也很科學,range的時候會去取"下一個值",但是for就不會,所以不會deadlock
func main(){
ch2 := make(chan int,10)
for i := 0; i < 10; i++ {
ch2 <- i
}
for i := 0; i < 10; i++ {
fmt.Println(<-ch2)
}
}
總結
- 使用forr時,記得關閉管道.
- 如果不知道什麼時候關閉管道就使用fori
goroutine結合channel
**需求1:**定義兩個方法,一個方法給管道里面寫數據,一個給管道里面讀取數據。要求同步進行。
- 開啓一個fn1的的協程給向管道inChan中寫入100條數據
- 開啓一個fn2的協程讀取inChan中寫入的數據
- 注意: fn1和fn2同時操作一一個管道
- 主線程必須等待操作完成後纔可以退出
var wg sync.WaitGroup
//寫數據
func fn1(ch chan int) {
for i := 1; i <= 10; i++ {
ch <- i
fmt.Printf("[寫入]數據%v成功\n", i)
time.Sleep(time.Millisecond * 50)
}
close(ch)
wg.Done()
}
//讀數據
func fn2(ch chan int) {
for v := range ch {
fmt.Printf("[讀取]數據%v成功\n", v)
time.Sleep(time.Millisecond * 50)
}
wg.Done()
}
func main() {
var ch = make(chan int, 10)
wg.Add(1)
go fn1(ch)
wg.Add(1)
go fn2(ch)
wg.Wait()
fmt.Println("退出........")
}
/**
[讀取]數據1成功
[寫入]數據1成功
[寫入]數據2成功
[讀取]數據2成功
[寫入]數據3成功
[讀取]數據3成功
[寫入]數據4成功
[讀取]數據4成功
[寫入]數據5成功
[讀取]數據5成功
[寫入]數據6成功
[讀取]數據6成功
[寫入]數據7成功
[讀取]數據7成功
[寫入]數據8成功
[讀取]數據8成功
[寫入]數據9成功
[讀取]數據9成功
[寫入]數據10成功
[讀取]數據10成功
退出........
*/
說明
管道是安全的,也許你寫入數據很慢,讀取數據很快,他讀不到數據會阻塞,管道是安全的
gorutine結合channel實現統計素數
3個channel,太秀了,上代碼
var wg sync.WaitGroup
//存放初始數據
func putNum(inChannel chan int) {
for i := 2; i < 120000; i++ {
inChannel <- i
}
close(inChannel)
wg.Done()
}
//統計素數
func primeNum(inChannel, primeChannel chan int, exitChan chan bool) {
for num := range inChannel {
flag := true
for j := 2; j <= num/2; j++ {
if num%j == 0 {
flag = false
break
}
}
if flag {
primeChannel <- num //num是素數
}
}
//給exitChan裏面放入一條數據
exitChan <- true
//close(primeChannel) //如果一個channel關閉了就沒法給這個channel發送數據了,所以我們不在這裏close
wg.Done()
}
//打印素數
func printPrime(primeChannel chan int) {
for prime := range primeChannel {
fmt.Println("得到一個素數", prime)
}
wg.Done()
}
func main() {
start := time.Now().Unix()
inChannel := make(chan int, 1000)
primeChannel := make(chan int, 1000)
exitChan := make(chan bool, 16) //標識primeChan close
//存放數字的協程
wg.Add(1)
go putNum(inChannel)
//統計素數的協程
for i := 0; i < 16; i++ {
wg.Add(1)
go primeNum(inChannel, primeChannel, exitChan)
}
//打印素數的協程
wg.Add(1)
go printPrime(primeChannel)
//判斷exitChan是否存滿值
wg.Add(1)
go func() {
for i := 0; i < 16; i++ {
<-exitChan //如果執行的速度比其他的協程快,這裏會阻塞
}
close(primeChannel)
wg.Done()
}()
wg.Wait()
end := time.Now().Unix()
fmt.Println("執行完畢........", end-start,"ms")
}
運行結果
單向管道
有的時候我們會將管道作爲參數在多個任務函數間傳遞,很多時候我們在不同的任務函數中使用管道都會對其進行限制,比如限制管道在函數中只能發送或只能接收。
默認情況下,管道是雙向的
func main(){
ch := make(chan int,5)
ch<-1
ch<-2
<-ch
<-ch
}
聲明爲只讀管道
func main() {
ch := make(chan <- int,5) //聲明爲只寫
ch<-1
ch<-2
<-ch //receive from send-only type chan<- int
}
聲明爲只讀管道
func main() {
ch := make(<- chan int,5) //聲明爲只讀
ch<-1 //send to receive-only type <-chan int
ch<-2
<-ch
}
應用:一個管道兩個協程,一個只讀,一個只寫
var wg sync.WaitGroup
func fn1(ch chan<- int) {
for i := 0; i < 10; i++ {
ch<-i
fmt.Printf("[寫入]數據%v成功\n",i)
time.Sleep(time.Millisecond*50)
}
close(ch)
wg.Done()
}
func fn2(ch <-chan int) {
for v := range ch {
fmt.Printf("[讀取]數據%v成功\n",v)
time.Sleep(time.Microsecond*50)
}
wg.Done()
}
func main() {
var ch = make(chan int,10)
wg.Add(1)
go fn1(ch)
wg.Add(1)
go fn2(ch)
wg.Wait()
fmt.Println("執行結束......")
}
/**
....
[寫入]數據7成功
[讀取]數據7成功
[寫入]數據8成功
[讀取]數據8成功
[寫入]數據9成功
[讀取]數據9成功
執行結束......
*/
select多路複用
以上這種方式雖然可以實現從多個管道接收值的需求,但是運行性能會差很多。
爲了應對這種場景,Go內置了select關鍵字
,可以同時響應多個管道的操作。
select
的使用類似於switch語句
,它有一系列case分支
和一個默認的分支
。每個case會對應一個管道的通信(接收或發送)過程。select 會一直等待
,直到某個case的通信操作完成時,就會執行case分支對應的語句。具體格式如下:
select{
case <-ch1:
...
case data := <-ch2:
...
case ch3<-data:
...
default:
默認操作
}
select多路複用是結合for循環實現的
使用select多路複用的時候不需要關閉channel
func main(){
//select多路複用
for {
select {
case v:=<-inChan:
fmt.Printf("從inChan中讀取數據-%v\n",v)
time.Sleep(time.Millisecond*50)
case v:=<-stringChan:
fmt.Printf("從stringChan中讀取數據-%v\n",v)
time.Sleep(time.Millisecond*50)
default:
fmt.Printf("數據獲取完畢")
return //注意退出
}
}
}
goruntine recover解決協程中出現的panic
和之前我們處理異常的方式一樣
func sayHello() {
for i := 0; i < 10; i++ {
time.Sleep(time.Millisecond * 50)
fmt.Println("hello world")
}
}
func test() {
//處理異常
defer func() {
err := recover()
if err!=nil {
fmt.Println("錯誤:",err)
}
}()
//一個map
var myMap map[int]string //這裏沒有分配內存,所以必定拋異常,如果不處理程序就終止
myMap[0] = "golan"
}
func main() {
go sayHello()
go test()
time.Sleep(time.Second)
fmt.Println("執行完畢....")
}
go併發安全和鎖
互斥鎖
互斥鎖是傳統併發編程中對共享資源進行訪問控制的主要手段,它由標準庫sync中的Mutex結構體類型表示。sync.Mutex 類型只有兩個公開的指針方法,Lock 和Unlock。Lock 鎖定當前的共享資源,Unlock 進行解鎖
同一時間只能有一個協程對資源進行訪問
並行改串行
var count = 0
var wg sync.WaitGroup
var mutex sync.Mutex
func test() {
defer wg.Done()
mutex.Lock() //加鎖
count++
fmt.Println("count : ",count)
time.Sleep(time.Millisecond)
mutex.Unlock() //解鎖
}
func main() {
for i := 0; i < 20; i++ {
wg.Add(1)
go test()
}
wg.Wait()
fmt.Println("執行結束....")
}
讀寫鎖
互斥鎖的本質是當一個goroutine訪問的時候,其他goroutine都不能訪問。這樣在資源同步,避免競爭的同時也降低了程序的併發性能。程序由原來的並行執行變成了串行執行。
其實,當我們對一個不會變化的數據只做“讀"操作的話,是不存在資源競爭的問題的。因爲數據是不變的,不管怎麼讀取,多少goroutine 同時讀取,都是可以的。
所以問題不是出在“讀”上,主要是修改,也就是“寫”。修改的數據要同步,這樣其他goroutine纔可以感知到。所以真正的互斥應該是讀取和修改、修改和修改之間,讀和讀是沒有互斥操作的必要的。
因此,衍生出另外一-種鎖,叫做讀寫鎖。
讀寫鎖可以讓多個讀操作併發,同時讀取,但是對於寫操作是完全互斥的。也就是說,當一個goroutine進行寫操作的時候,其他goroutine既不能進行讀操作,也不能進行寫操作。
GO中的讀寫鎖由結構體類型sync.RWMutex
表示。此類型的方法集合中包含兩對方法:
一組是對寫操作的鎖定和解鎖,簡稱“寫鎖定”和“寫解鎖:
func (*RWMutex)Lock()
func (*RWMutex)Unlock()
另一組表示對讀操作的鎖定和解鎖,簡稱爲“讀鎖定”與“讀解鎖" :
func (*RWMutex)RLock()
func (*RWMutex)RUnlock()
讀寫鎖示例
var wg sync.WaitGroup
var rwMutex sync.RWMutex
//寫方法
func write() {
defer wg.Done()
rwMutex.Lock()
fmt.Println("執行寫操作")
time.Sleep(time.Second * 2)
rwMutex.Unlock()
}
//讀方法
func read() {
defer wg.Done()
rwMutex.RLock()
fmt.Println("----執行讀操作")
time.Sleep(time.Second * 2)
rwMutex.RUnlock()
}
func main() {
//開啓10個協程執行讀操作
for i := 0; i < 10; i++ {
wg.Add(1)
go read()
}
for i := 0; i < 10; i++ {
wg.Add(1)
go write()
}
wg.Wait()
fmt.Println("執行結束....")
}
反射
有時我們需要寫一個函數, 這個函數有能力統一處理 各種值類型,而這些類型可能無法共享同一個接口,也可能佈局未知,也有可能這個類型在我們設計函數時還不存在,這個時候我們就可以用到反射。
- 空接口可以存儲任意類型的變量,那我們如何知道這個空接口保存數據的類型是什麼?值是什麼呢?
- 可以使用類型斷言
- 可以使用反射實現,也就是在程序運行時動態的獲取一個變量的類型信息和值信息。
- 把結構體序列化成json字符串,自定義結構體Tab標籤的時候就用到了反射
- ORM框架就用到了反射技術
反射是框架的靈魂, java開發的同學就應該知道,spring等框架底層大量用到了反射
**ORM:**對象關係映射( Object Relational Mapping,簡稱ORM)是通過使用描述對象和數據庫之間映射的元數據,將面嚮對象語言程序中的對象自動持久化到關係數據庫中。
反射的基本介紹
反射是指在程序運行期間對程序本身進行訪問和修改的能力。正常情況程序在編譯時,變量被轉換爲內存地址,變量名不會被編譯器寫入到可執行部分。在運行程序時,程序無法獲取自身的信息。支持反射的語言可以在程序編譯期將變量的反射信息,如字段名稱、類型信息、結構體信息等整合到可執行文件中,並給程序提供接口訪問反射信息,這樣就可以在程序運行期獲取類型的反射信息,並且有能力修改它們。
go可以實現的功能
- 反射可以在程序運行期間動態的獲取變量的各種信息,比如變量的類型類別
- 如果是結構體,通過反射還可以獲取結構體本身的信息,比如結構體的字段、結構體的方法。
- 通過反射,可以修改變量的值,可以調用關聯的方法
go語言中的變量是分爲兩部分的
**類型信息:**預先定義好的元信息
**值信息:**程序運行過程中可動態變化
在Go語言的反射機制中,任何接口值都由是一個具體類型和具體類型的值兩部分組成的。
在Go語言中反射的相關功能由內置的reflect包
提供,任意接口值在反射中都可以理解爲由reflect.Type
和reflect.Value
兩部分組成,並且reflect包提供了reflect.TypeOf
和reflect.ValueOf
兩個重要函數來獲取任意對象的Value和Type
。
reflect.TypeOf()獲取任意值的類型對象
在Go語言中,使用reflect.TypeOf()函數可以接受任意interface{}參數,可以獲得任意值的類型對象(reflect.Type) ,程序通過類型對象可以訪問任意值的類型信息。
type myInt int
type Person struct {
Name string
Age int
}
//反射獲取任意變量的類型
func reflectFn(x interface{}) {
typeOf := reflect.TypeOf(x)
fmt.Println(typeOf)
}
func main() {
a := 10
b := 12.3
c := true
d := "你好"
reflectFn(a) //int
reflectFn(b) //float64
reflectFn(c) //bool
reflectFn(d) //string
var num myInt = 2
var person = Person{
Name: "花玲",
Age: 18,
}
reflectFn(num) //main.myInt
reflectFn(person) //main.Person
var h = 24
reflectFn(&h) //*int
}
type Name和type Kind
在反射中關於類型還劃分爲兩種:類型(Type)
和種類(Kind)
。因爲在Go語言中我們可以使用type關鍵字構造很多自定義類型,而種類(Kind) 就是指底層的類型,但在反射中,當需要區分指針、結構體等大品種的類型時,就會用到種類(Kind) 。
舉 個例子,我們定義了兩個指針類型和兩個結構體類型,通過反射查看它們的類型和種類。
Go語言的反射中像數組、切片、Map、指針等類型的變量,它們的.Name()都是返回空。
type myInt int
type Person struct {
Name string
Age int
}
//反射獲取任意變量的類型
func reflectFn(x interface{}) {
typeOf := reflect.TypeOf(x)
fmt.Printf("類型:%v,Name:%v,Kind:%v\n",typeOf,typeOf.Name(),typeOf.Kind())
}
func main() {
a := 10
b := 12.3
c := true
d := "你好"
reflectFn(a) //類型:int,Name:int,Kind:int
reflectFn(b) //類型:float64,Name:float64,Kind:float64
reflectFn(c) //類型:bool,Name:bool,Kind:bool
reflectFn(d) //類型:string,Name:string,Kind:string
var num myInt = 2
var person = Person{
Name: "花玲",
Age: 18,
}
reflectFn(num) //類型:main.myInt,Name:myInt,Kind:int
reflectFn(person) //類型:main.Person,Name:Person,Kind:struct
var h = 24
reflectFn(&h) //類型:*int,Name:,Kind:ptr
}
Name:類型名稱 Kind():底層類型
reflect.ValueOf()
reflect.valueOf()返回的是reflect.Value類型
,其中包含了原始值的值信息
。reflect.Value 與原始值之間可以互相轉換
。
reflect.Value類型提供的獲取原始值的方法如下:
//反射獲取任意變量的值
func reflectValue(x interface{}) {
//fmt.Println(x)
//num := x.(int)
//sum := num + 10
//fmt.Println(sum) //23
//反射獲取變量的原始值
v:=reflect.ValueOf(x)
sum := v.Int() + 10
fmt.Println(sum) //23
}
func main() {
var a = 13
reflectValue(a)
}
//反射獲取任意變量的值
func reflectValue(x interface{}) {
v := reflect.ValueOf(x)
kind := v.Kind()
switch kind {
case reflect.Int:
fmt.Printf("int類型的原始值%v\n",v.Int())
case reflect.Float32:
fmt.Printf("float32類型的原始值:%v\n",v.Float())
case reflect.String:
fmt.Printf("string類型的原始值:%v\n",v.String())
default:
fmt.Printf("還沒有判斷該類型\n")
}
}
func main() {
var a int64 = 13
var b float32 = 12.3
var c string = "你好golang"
reflectValue(a) //還沒有判斷該類型
reflectValue(b) //float32類型的原始值:12.300000190734863 精度損失
reflectValue(c) //string類型的原始值:你好golang
}
利用reflect.ValueOf()修改原始值
func reflectSetValue(x interface{}) {
//*x = 120 //錯誤寫法
//v := x.(*int64) //類型斷言可以
//*v = 120
v:=reflect.ValueOf(x)
//kind := v.Kind() //ptr
//k := v.Elem().Kind() //int64
switch v.Elem().Kind() {
case reflect.Int64:
v.Elem().SetInt(123)
case reflect.String:
v.Elem().SetString("go你好")
default:
fmt.Println("還未識別該類型")
}
}
func main() {
var a int64 = 100
reflectSetValue(&a)
fmt.Println(a)
var b string = "你好go"
reflectSetValue(&b)
fmt.Println(b)
}
結構體反射
與結構體相關的方法
任意值通過reflect.TypeOf()
獲得反射對象信息後,如果它的類型是結構體,可以通過反射值對象(reflect.Type)
的NumField()
和Field()
方法獲得結構體成員的詳細信息。
reflect.Type中與獲取結構體成員相關的的方法如下表所示。
StructField類型
type StructField struct {
//參見http://golang.org/ref/spec#Uniqueness_of_identifiers
Name string // Name是字段的名字
PkgPath string //PkgPath是非導出字段的包路徑,對導出字段該字段爲”
Type Type //字段的類型
Tag StructTag //字段的標籤
Offset uintptr //字段在結構體中的字節偏移量
Index []int //用於Type.FieldByIndex時的索引切片
Anonymous bool //是否匿名字段
}
如何獲取屬性
//定義結構體
//Student結構體
type Student struct {
Name string `json:"name1" form:"username"`
Age int `json:"age"`
Score int `json:"score"`
}
func (s Student) GetInfo() string {
str := fmt.Sprintf("姓名:%v,年齡:%v,成績:%v", s.Name, s.Age, s.Score)
return str
}
func (s *Student) SetInfo(name string, age, score int) {
s.Name = name
s.Age = age
s.Score = score
}
func (s Student) print() {
fmt.Println("這就是個打印方法....")
}
//打印字段
func PrintStructField(s interface{}) {
//判斷參數是不是結構體類型
t := reflect.TypeOf(s)
v := reflect.ValueOf(s)
if t.Kind() != reflect.Struct && t.Elem().Kind() != reflect.Struct {
fmt.Println("傳入的參數不是一個結構體")
return
}
//通過類型變量裏面的Field可以獲取結構體的字段
field0 := t.Field(0)
fmt.Printf("%#v \n", field0)
fmt.Println("字段名稱: ", field0.Name) //字段名稱: Name
fmt.Println("字段類型: ", field0.Type) //字段類型: string
fmt.Println("字段tag: ", field0.Tag.Get("json")) //字段tag: name1
fmt.Println("字段tag: ", field0.Tag.Get("form")) //字段tag: username
fmt.Println("=============")
//通過類型變量裏面的FieldByName可以獲取結構體的字段
field1, ok := t.FieldByName("Age")
if ok {
fmt.Println("字段名稱: ", field1.Name)
fmt.Println("字段類型: ", field1.Type)
fmt.Println("字段tag: ", field1.Tag.Get("json"))
}
fmt.Println("=============")
//通過類型變量裏面的NumField獲取到該結構體有幾個字段
var fieldCount = t.NumField()
fmt.Println("結構體有", fieldCount, "個屬性") //結構體有 3 個屬性
fmt.Println("=============")
//通過值變量獲取結構體屬性對應的值
fmt.Println(v.FieldByName("Name"))
fmt.Println(v.FieldByName("Age"))
fmt.Println("=============")
//通過for循環獲取所有的屬性
for i := 0; i < t.NumField(); i++ {
field := t.Field(i)
value := v.Field(i)
fmt.Printf("屬性名稱:%v,屬性值:%v,屬性類型:%v,屬性Tag:%v\n",field.Name,value,field.Type,field.Tag.Get("json"))
}
}
func main() {
s1 := Student{
Name: "花玲",
Age: 18,
Score: 100,
}
PrintStructField(s1)
}
字段
//打印字段
func PrintStructField(s interface{}) {
//判斷參數是不是結構體類型
t := reflect.TypeOf(s)
v := reflect.ValueOf(s)
if t.Kind() != reflect.Struct && t.Elem().Kind() != reflect.Struct {
fmt.Println("傳入的參數不是一個結構體")
return
}
//通過類型變量裏面的Field可以獲取結構體的字段
field0 := t.Field(0) //獲取第0個屬性,此時是Name
fmt.Printf("%#v \n", field0) //reflect.StructField{Name:"Name", PkgPath:"", Type:(*reflect.rtype)(0x4aef40), Tag:"json:\"name\"", Offset:0x0, Index:[]int{0}, Anonymous:false}
fmt.Println("字段名稱: ", field0.Name) //字段名稱: Name
fmt.Println("字段類型: ", field0.Type) //字段類型: string
fmt.Println("字段tag: ", field0.Tag.Get("json")) //字段tag: name1
fmt.Println("字段tag: ", field0.Tag.Get("form")) //字段tag: username
fmt.Println("=============")
//通過類型變量裏面的FieldByName可以獲取結構體的字段
field1, ok := t.FieldByName("Age")
if ok {
fmt.Println("字段名稱: ", field1.Name)
fmt.Println("字段類型: ", field1.Type)
fmt.Println("字段tag: ", field1.Tag.Get("json"))
}
fmt.Println("=============")
//通過類型變量裏面的NumField獲取到該結構體有幾個字段
var fieldCount = t.NumField()
fmt.Println("結構體有", fieldCount, "個屬性") //結構體有 3 個屬性
fmt.Println("=============")
//通過值變量獲取結構體屬性對應的值
fmt.Println(v.FieldByName("Name"))
fmt.Println(v.FieldByName("Age"))
fmt.Println("=============")
//通過for循環獲取所有的屬性
for i := 0; i < t.NumField(); i++ {
field := t.Field(i)
value := v.Field(i)
fmt.Printf("屬性名稱:%v,屬性值:%v,屬性類型:%v,屬性Tag:%v\n", field.Name, value, field.Type, field.Tag.Get("json"))
}
}
執行方法
func PrintStructFn(s interface{}) {
//判斷參數是不是結構體類型
t := reflect.TypeOf(s)
v := reflect.ValueOf(s)
if t.Kind() != reflect.Struct && t.Elem().Kind() != reflect.Struct {
fmt.Println("傳入的參數不是一個結構體")
return
}
//通過類型變量裏面的Method以獲取結構體的方法
method0 := t.Method(0) //這個0和方法的順序無關,是根據ASCII碼有關
fmt.Println(method0.Name) //GetInfo
fmt.Println(method0.Type) //func(main.Student) string
fmt.Println("-------------------")
//通過類型變量獲取這個結構體有多少個方法
fmt.Println("方法個數:", t.NumMethod())
method1, ok := t.MethodByName("Print")
if ok {
fmt.Println(method1.Name) //Print
fmt.Println(method1.Type) //func(main.Student)
}
fmt.Println("-------------------")
//通過<值變量>執行方法(注意 需要使用值變量,並且要注意參數) v.Method(0).Call(nil)或者v.MethodBy
v.Method(1).Call(nil) //這就是個打印方法....
//執行方法傳入參數(注意需要使用<值變量>,並且要注意參數,接收的參數是[]reflect.Value的切片)
fmt.Println(v.MethodByName("GetInfo").Call(nil)) //[姓名:花玲,年齡:18,成績:100]
var params = []reflect.Value{
reflect.ValueOf("kk"),
reflect.ValueOf(18),
reflect.ValueOf(100),
}
v.MethodByName("SetInfo").Call(params)
fmt.Println(v.MethodByName("GetInfo").Call(nil)) //[姓名:kk,年齡:18,成績:100]
}
通過反射修改結構體屬性
func reflectChangeStruct(s interface{}) {
t := reflect.TypeOf(s)
v := reflect.ValueOf(s)
if t.Kind() != reflect.Ptr {
fmt.Println("傳入的不是指針類型")
return
} else if t.Elem().Kind() != reflect.Struct {
fmt.Println("傳入的不是結構體指針類型")
return
}
//修改結構體屬性的值
name :=v.Elem().FieldByName("Name")
name.SetString("kk")
age := v.Elem().FieldByName("Age")
age.SetInt(21)
}
主函數
func main() {
s1 := Student{
Name: "花玲",
Age: 18,
Score: 100,
}
//PrintStructField(s1)
//PrintStructFn(&s1)
reflectChangeStruct(&s1)
fmt.Printf("%#v\n",s1) //main.Student{Name:"kk", Age:21, Score:100}
}
不要亂用反射!
反射是一個強大並富有表現力的工具,能讓我們寫出更靈活的代碼。但是反射不應該被濫用,原因有以下三個。
- 基於反射的代碼是極其脆弱的,反射中的類型錯誤會在真正運行的時候纔會引發panic,那很可能是在代碼寫完的很長時間之後。
- 大量使用反射的代碼通常難以理解。
文件 目錄操作
讀取文件
只讀方式打開文件file,err := os .Open()
func main() {
//只讀方式打開文件
file, err := os.Open("p:/a.txt")
defer file.Close() //必須關閉
if err != nil {
fmt.Println(err)
return
}
//讀取文件內容
fileSlice := make([]byte,128) //每次讀取128字節
var strSlice []byte
for {
n, err := file.Read(fileSlice)
if err == io.EOF { //err==io.EOF表示讀取完畢
fmt.Println("讀取完畢")
break
}
if err != nil {
fmt.Println("讀取失敗")
return
}
fmt.Printf("讀取到了%v個字節\n",n)
strSlice = append(strSlice, fileSlice[:n]...) //最後可能讀取到的塊沒有128字節了,爲了避免切片填充,限制切片範圍
}
fmt.Println(string(strSlice)) //讀出來的是byte切片,我們需要轉換成string再來打印
}
讀取文件(方法2) bufio讀取文件
java開發的同學應該不難看出這個很像是字節流
func main() {
file, err := os.Open("p:/a.txt")
defer file.Close()
if err != nil {
fmt.Println(err)
return
}
//讀取文件
reader := bufio.NewReader(file)
var out string
for {
str, err := reader.ReadString('\n')
if err == io.EOF {
fmt.Println("讀取結束")
out+=str //這裏也需要拼接,不然會讀不全
break
}
if err != nil {
fmt.Println(err)
return
}
out+=str
}
fmt.Println(out)
}
讀取文件(方法3) ioutil讀取文件
最簡單方式,如果文件比較小,可以用這種方式來讀取
func main() {
fileByte, err := ioutil.ReadFile("p:/a.txt")
if err != nil {
fmt.Println(err)
return
}
fmt.Println(string(fileByte))
}
寫入文件
方法1 os.OpenFile()
模式 | 含義 |
---|---|
os.O_ WRONLY | 只寫 |
os.O_ CREATE | 創建文件 |
os.O_ RDONLY | 只讀 |
os.O_ RDWR | 讀寫 |
os.O_ TRUNC | 清空 |
os.O_ APPEND | 追加 |
perm:文件權限,一個八進制數。r(讀) 04,w (寫) 02,x (執行) 01。一般來說傳0666就行
os.OpenFile()需要傳入三個參數,文件路徑
,模式
,文件權限
func main() {
file, err := os.OpenFile("p:/a.txt", os.O_CREATE|os.O_RDWR|os.O_APPEND, 0666)
defer file.Close()
if err != nil {
fmt.Println(err)
return
}
//寫入文件
for i := 0; i < 10; i++ {
file.WriteString(strconv.Itoa(i)+"花玲花玲花玲花玲花玲花玲\r\n")
}
var str = "直接寫入的字符串數據byte"
file.Write([]byte(str)) //這樣子也可以
}
寫入文件(方法2) bufio 寫入文件
記得flush,java開發的同學應該都知道
func main() {
file, err := os.OpenFile("p:/a.txt", os.O_CREATE|os.O_RDWR|os.O_TRUNC, 0666)
defer file.Close()
if err != nil {
fmt.Println(err)
return
}
writer := bufio.NewWriter(file)
for i := 0; i < 10; i++ {
writer.WriteString("你好\r\n") //將數據先寫入緩存
}
writer.Flush() //將緩存中的內容寫入文件
}
寫入文件(方法3)ioutil寫入文件
但是這個,沒有append模式,測試寫東西的話可以用一下,寫日誌還是用上面的吧
func main() {
str := "你好offer"
err := ioutil.WriteFile("p:/a.txt",[]byte(str),0666)
if err != nil {
fmt.Println(err)
return
}
}
目錄操作
複製文件 ioutil
func copyFile(srcFileName, dstFileName string) (err error) {
bytestr, err := ioutil.ReadFile(srcFileName)
if err != nil {
return err
}
err1 := ioutil.WriteFile(dstFileName, bytestr, 006)
if err1 != nil {
return err1
}
return nil
}
func main() {
src := "p:/a.txt"
dst := "p:/b.txt"
err := copyFile(src, dst)
if err != nil {
fmt.Println(err)
return
}
fmt.Println("複製成功")
}
複製文件 文件流
func copyFile(srcFileName, dstFileName string) (err error) {
sFile, err1 := os.Open(srcFileName)
defer sFile.Close() //不關閉可能造成內存泄漏
dFile, err2 := os.OpenFile(dstFileName, os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0666)
defer dFile.Close()
if err1 != nil {
return err1
}
if err2 != nil {
return err2
}
var fileSlice = make([]byte, 1024*10)
for true {
//讀取數據
n1, err := sFile.Read(fileSlice)
if err == io.EOF {
break
}
if err != nil {
return err
}
//寫入數據
if _, err4 := dFile.Write(fileSlice[:n1]); err4 != nil {
return err4
}
}
return nil
}
func main() {
src := "p:/a.txt"
dst := "p:/b.txt"
err := copyFile(src, dst)
if err != nil {
fmt.Println(err)
return
}
fmt.Println("複製成功")
}
創建目錄
func main() {
os.Mkdir("p:/a", 0666) //創建單個目錄
os.MkdirAll("p:/a/b/c",0666) //創建多級目錄
}
刪除文件
func main() {
err := os.Remove("p:/aaa.txt") //可以刪除一個文件也可以刪除文件夾
if err != nil {
fmt.Println(err)
}
err1 := os.RemoveAll("p:/a.txt") //級聯刪除
if err1!=nil {
fmt.Println(err1)
}
}
重命名
func main() {
err := os.Rename("p:/a.txt", "p:/b.txt")
if err != nil {
fmt.Println(err)
}
fmt.Println("重命名成功")
}