主頁 > 區塊鏈 > 肝了 10 萬字 ,Go 語言保姆級編程教程2021最新版(建議收藏)

肝了 10 萬字 ,Go 語言保姆級編程教程2021最新版(建議收藏)

2021-06-15 07:46:23 區塊鏈

友情提示:編輯器顯示 12 萬 多字,先點贊,關注,收藏,一鍵三連支持,再學習,

本文對比 C 語言進行學習 Go 語言,如果你有 C 語言基礎,學習 Go 語言會容易很多,

Go 語言保姆級教程目錄

    • 什么是Go語言
    • Go語言優勢
    • Go語言發展史
    • Go作者
    • Go語言現狀
    • Go語言應用場景
    • 如何學習Go語言
    • 源檔案對比
    • 代碼管理對比
    • 關鍵字對比
    • 資料型別對比
    • 常量變數對比
    • 注釋對比
    • 運算子對比
    • 流程控制陳述句對比
    • 函式和方法對比
    • 編程思想對比
    • 其它新增特性
    • Go語言SDK安裝和配置
    • 安裝Go語言開發工具
    • Goland安裝
    • Go語言程式組成
    • Go語言程式主函式定義格式
    • Go語言HelloWorld
    • Go語言HelloWorld和C語言HelloWorld異同
    • Go語言注釋
    • Go語言編碼風格
    • 關鍵字
    • C語言關鍵字和Go語言關鍵字對比
    • 識別符號
    • Go語言資料型別
    • Go語言變數
    • Go語言變數定義注意點
    • 區域變數和全域變數
    • 資料型別轉換
    • 數值型別和字串型別之間轉換
    • Go語言常量
    • 輸入函式
    • go命令列操作指令
    • 通過os包獲取命令列引數
    • 通過flag包獲取命令列引數
    • os包和flag包獲取命令列引數對比
    • 算數運算子
    • 關系算符
    • 邏輯運算子
    • 位運算子
    • 賦值運算子
    • 其它運算子
    • 運算子優先級
    • Go語言流程控制基本概念
    • 選擇結構if
    • 選擇結構switch
    • 回圈結構for
    • 四大跳轉
    • 函式
    • 和C語言函式差異
    • 值傳遞和參考傳遞
    • 匿名函式
    • 閉包
    • 延遲呼叫
    • init函式
    • 陣列
    • 一維陣列
    • 二維陣列
    • 切片
    • map(字典、映射)
    • 結構體
    • 普通指標
    • 指向陣列指標
    • 指向切片的指標
    • 指向字典指標
    • 指向結構體指標
    • 指標作為函式引數和回傳值
    • 方法
    • 介面
    • 面向物件基本概念
    • 面向物件思想
    • 面向物件和面向程序區別
    • 面向物件的特點
    • 類與物件的關系
    • 如何設計一個類
    • 如何分析一個類
    • 如何定義一個類
    • 如何通過類創建一個物件
    • 不同包中變數、函式、方法、型別公私有問題
    • 面向物件三大特性
    • 例外處理
    • 列印例外資訊
    • 中斷程式
    • 恢復程式
    • 字串相關方法
    • 正則運算式
    • 時間和日期函式
    • Go語言中呼叫C語言函式
    • C語言中呼叫Go語言函式(很少使用)
    • 檔案的打開和關閉
    • 檔案讀取
    • 檔案創建和寫入
    • 判斷檔案是否存在
    • 練習
    • 并發編程基本概念
    • 什么是串行?
    • 什么是并行?
    • 什么是并發?
    • 什么是程式?
    • 什么是行程?
    • 什么是執行緒?
    • 什么是協程?
    • Go并發
    • 多執行緒同步問題
    • 生產者消費者問題
    • 生產者和消費者資源競爭問題
    • 管道(Channel)
    • select選擇結構
    • 定時器補充

這篇文章可以說是一本書了,排版,碼字耗費了很長的時間,10W+字 Go 語言從入門保姆級教程2021年版,覺得有價值記得一鍵三連支持,

什么是Go語言

  • Go語言(Golang)是Google公司2009年推出的一門"高級編程言語", 目的是為了解決:
    • "現有主流編程語言"明顯落后于硬體發展速度的問題
    • 不能合理利用多核CPU的優勢提升軟體系統性能的問題
    • 軟體復雜度越來越高, ***維護成本也越來越高***的問題
    • 企業開發中不得不在***快速開發和性能之間艱難抉擇***的問題

科普小知識:
1.靜態語言:
1.1一般都需要通過編譯器(compiler)將源代碼翻譯成機器碼,之后才能執行,程式被編譯之后無論是程式中的資料型別還是程式的結構都不可以被改變
1.2靜態語言的性能和安全性都非常好, 例如C和C++、Go, 但是C和C++的缺點是開發速度慢, 維護成本高
2.動態語言
2.1一般不需要通過編譯器將源代碼翻譯成機器碼,在運行程式的時候才逐行翻譯,程式在運行的程序中可以動態修改程式中的資料型別和程式的結構
2.2動態語言開發速度快,維護成本低,例如Ruby和Python, 但是Ruby和Python的性能和安全性又略低

  • Go語言專門針對多核CPU進行了優化, 能夠充分使用硬體多核CPU的優勢, 使得通過Go語言撰寫的軟體系統性能能夠得到很大提升
  • Go語言撰寫的程式,既可以媲美C或C++代碼的運行速度, 也可以媲美Ruby或Python開發的效率
  • 所以Go語言很好的解決了"現有主流編程語言"存在的問題, 被譽"現代化的編程語言"

Go語言優勢

  • 簡單易學
    • Go語言的作者都有C的基因,Go自然而然也有了C的基因,但是Go的語法比C還簡單, 并且幾乎支持大多數你在其他語言見過的特性:封裝、繼承、多型、反射等
  • 豐富的標準庫
    • Go目前已經內置了大量的庫,特別是網路庫非常強大
    • 前面說了作者是C的作者,所以Go里面也可以直接包含c代碼,利用現有的豐富的C庫
  • 跨平臺編譯和部署
    • Go代碼可直接編譯成機器碼,不依賴其他庫,部署就是扔一個檔案上去就完事了. 并且Go代碼還可以做到跨平臺編譯(例如: window系統編譯linux的應用)
  • 內置強大的工具
    • Go語言里面內置了很多工具鏈,最好的應該是gofmt工具,自動化格式化代碼,能夠讓團隊review變得如此的簡單,代碼格式一模一樣,想不一樣都很困難
  • 性能優勢: Go 極其地快,其性能與 C 或 C++相似,在我們的使用中,Go 一般比 Python 要快 30 倍左右
    • 語言層面支持并發,這個就是Go最大的特色,天生的支持并發,可以充分的利用多核,很容易的使用并發
    • 內置runtime,支持垃圾回收
  • … …

Go語言的吉祥物是地鼠
地鼠的特點是速度快、成群結隊、頭腦簡單
而Go語言的特點正好也是編程速度快、并發性好、簡單易學


Go語言發展史

2007年,谷歌工程師Rob Pike, Ken Thompson和Robert Griesemer開始設計一門全新的語言,這是Go語言的最初原型,
2009年11月10日,Go語言以開放源代碼的方式向全球發布,
2011年3月16日,Go語言的第一個穩定(stable)版本r56發布,
2012年3月28日,Go語言的第一個正式版本Go1發布,
2013年4月04日,Go語言的第一個Go 1.1beta1測驗版發布,
2013年4月08日,Go語言的第二個Go 1.1beta2測驗版發布,
2013年5月02日,Go語言Go 1.1RC1版發布,
2013年5月07日,Go語言Go 1.1RC2版發布,
2013年5月09日,Go語言Go 1.1RC3版發布, 
2013年5月13日,Go語言Go 1.1正式版發布,
2013年9月20日,Go語言Go 1.2RC1版發布,
2013年12月1日,Go語言Go 1.2正式版發布,
2014年6月18日,Go語言Go 1.3版發布,
2014年12月10日,Go語言Go 1.4版發布,
2015年8月19日,Go語言Go 1.5版發布,本次更新中移除了”最后殘余的C代碼”,
2016年2月17日,Go語言Go 1.6版發布,
2016年8月15日,Go語言Go 1.7版發布,
2017年2月17日,Go語言Go 1.8版發布,
2017年8月24日,Go語言Go 1.9版發布,
2018年2月16日,Go語言Go 1.10版發布,

Go作者

  • Go語言是UNIX作者、C語言作者、谷歌V8引擎作者攜手打造的, 由谷歌公司2009年推出的一門高級編程言語,

跟著谷歌走吃喝啥都有


Go語言現狀

  • 多次獲得TIOBE年度最佳語言

  • 2018年Go語言一度超過Java, 進入編程語言排行榜前三名.

  • 從公司角度:

    • 許多大廠都已經擁抱 Go 語言,包括阿里巴巴、京東、今日頭條、小米、滴滴、七牛云、360等明星公司, 也包括知乎、輕松籌、快手、探探、美圖、獵豹移動等等,同時,創業公司也很喜歡 Go 語言,主要因為其入門快、程式庫多、運行迅速,很適合快速構建互聯網軟體產品,
  • 從業務維度:

    • Go 程式可以在裝有 Windows、Linux、FreeBSD 等作業系統的服務器上運行,并用于提供基礎軟體支撐、API 服務、Web 服務、網頁服務等等,
    • 在云計算、微服務、大資料、區塊鏈、物聯網等領域,Go 語言早已蓬勃發展. 除了語法簡單, 性能優越以外, K8S底層架構在云計算的領導地位(K8S就是Go開發的), 也讓這些各大公司不得不擁抱Go語言,
    • 區塊鏈的崛起更進一步帶動了Go工程師的需求,市面上大部磁區塊鏈明星專案都是用Go開發的, 足以說明Go在分布式系統中的地位,這也就是為什么今年開始,大批金融公司開始招聘Go工程師的重要原因,
  • 從薪資角度來看

    • 應屆生普遍在4~8K, 1年左右普遍在10K左右, 2年~3年普遍在20K左右

Go語言應用場景

  • 網路編程,這一塊目前應用最廣,包括Web應用、API應用、下載應用、記憶體資料庫等
  • 云平臺開發,目前國外很多云平臺在采用Go開
  • 服務器編程, 以前你如果使用Java或者C++做的那些事情,都可以用Go來做
  • 分布式系統,資料庫代理器等
  • 它可以做從底層到前端的任何作業

如何學習Go語言

  • Go語言被稱之為現代化的C語言, 所以無論是從語法特性, 還是作者本身, Go語言都與C語言有著莫大的關系, 所以學習本套課程之前如果你有C語言的基礎, 那么將會事半功倍
  • 對于初學者而言, 學習編程的捷徑只有一條, 那就是多動手

竹子用了4年的時間, 僅僅長了3cm, 從第五年開始, 以每天30cm的速度瘋狂地生長, 僅僅用了六周的時間就長到了15米, 其實,在前面的四年, 竹子將根在土壤里延伸了數百平米, 做人做事亦是如此, 不要擔心你此時此刻的付出得不到回報, 因為這些付出都是為了扎根,


源檔案對比

  • C語言源檔案
檔案擴展名源型別
.h頭檔案,存放代碼宣告
.cC語言源檔案,存放代碼實作
  • Go語言源檔案
檔案擴展名源型別
.goGo語言源檔案,存放代碼實作

代碼管理對比

  • C語言中通過檔案來管理代碼
    • 想使用某一個函式時,只需要include匯入對應的.h檔案即可
  • Go語言中通過包來管理代碼
    • Go語言沒有.h檔案的概念, 在Go中想使用某一個函式時, 只需要import匯入對應的包即可
  • C語言中函式、變數公私有管理
    • 通過extern和static實作是否公開函式和變數
  • Go語言中函式、變數公私有管理
    • 通過函式名稱首字母大小寫實作是否公開函式
    • 通過變數名稱首字母大小寫實作是否公開變數

關鍵字對比

  • C語言中一共有32個關鍵字
12345678
ifelseswitchcasedefaultbreakreturngoto
dowhileforcontinuetypedefstructenumunion
charshortintlongfloatdoublevoidsizeof
signedunsignedconstautoregisterstaticexternvolatile
  • Go語言中一共有25個關鍵字
12345678
ifelseswitchcasedefaultbreakreturngoto
fallthroughforcontinuetypestructvarconstmap
funcinterfacerangeimportpackagedefergoselect
chan

資料型別對比

  • C語言資料型別
  • Go語言資料型別

  • C語言各資料型別占用記憶體空間
型別32位編譯器64位編譯器
char11
int44
float44
double88
short22
long48
long long88
void*48
  • Go語言各資料型別占用記憶體空間
型別32位編譯器64位編譯器本質
int8/uint811signed char/unsigned char
int16/uint1622signed short/unsigned short
int32/uint3244signed int/unsigned int
int64/uint6488signed long long int/unsigned long long int
byte11uint8/unsigned char
rune44int32/signed int
int48根據機器位數決定長度
uintptr48根據機器位數決定長度 uint32/uint64
float3244float
float6488double
true11char型別的整型
false11char型別的整型
  • 和C語言一樣,Go語言也提供了Sizeof計算變數的記憶體空間
    • 1.匯入import "unsafe"包
    • 2.通過unsafe.Sizeof()計算變數記憶體空間

  • Go語言基本資料型別內部實作
    • golang官方網站下載go1.4版本源代碼
      • 越老版本的代碼越純粹,越適合新手學習
      • 隨著代碼的更新迭代會逐步變得非常復雜, 所以此處建議下載1.4版本
    • 解壓后打開路徑: go\src\runtime\runtime.h
    • 得到如下實作代碼
// 第8行到35行
typedef	signed char		int8;
typedef	unsigned char		uint8;
typedef	signed short		int16;
typedef	unsigned short		uint16;
typedef	signed int		int32;
typedef	unsigned int		uint32;
typedef	signed long long int	int64;
typedef	unsigned long long int	uint64;
typedef	float			float32;
typedef	double			float64;

#ifdef _64BIT
typedef	uint64		uintptr;
typedef	int64		intptr;
typedef	int64		intgo; // Go's int
typedef	uint64		uintgo; // Go's uint
#else
typedef	uint32		uintptr;
typedef	int32		intptr;
typedef	int32		intgo; // Go's int
typedef	uint32		uintgo; // Go's uint
#endif

#ifdef _64BITREG
typedef	uint64		uintreg;
#else
typedef	uint32		uintreg;
#endif

// 第153行到157行
enum
{
	true	= 1,
	false	= 0,
};

install B 時刻:
Go本質就是用C語言撰寫的一門高級編程語言
所以江哥前面教你C語言就是為了今天能讓你看懂Go的實作代碼,做到知其然知其所以然


常量變數對比

  • C語言定義常量和變數格式
資料型別 變數名稱 = 值;
const 資料型別 常量名稱 = 值;
  • Go語言定義常量和變數格式
    • 除了以下標準格式外,Go語言還提供了好幾種簡單的語法糖
var 變數名稱 資料型別 = 值;
const 變數名稱 資料型別 = 值;

注釋對比

  • 和C語言一樣,Go語言也支持單行注釋和多行注釋, 并且所有注釋的特性都和C語言一樣
    • 單行注釋 // 被注釋內容
    • 多行注釋 /* 被注釋內容*/
  • 在Go語言中,官方更加推薦使用單行注釋,而非多行注釋(詳情可以直接查看Go官方原始碼)

運算子對比

  • 算數運算子和C語言幾乎一樣
    • Go語言中++、–運算子不支持前置
      • 錯誤寫法: ++i; --i;
    • Go語言中++、–是陳述句,不是運算式,所以必須獨占一行
      • 錯誤寫法: a = i++; return i++;
運算子描述實體
+相加A + B
-相減A - B
*相乘A * B
/相除B / A
%求余B % A
++自增A++
自減A–

  • 關系算符和C語言一樣
運算子描述實體
==檢查兩個值是否相等,如果相等回傳 True 否則回傳 False,A == B
!=檢查兩個值是否不相等,如果不相等回傳 True 否則回傳 False,A != B
>檢查左邊值是否大于右邊值,如果是回傳 True 否則回傳 False,A > B
<檢查左邊值是否小于右邊值,如果是回傳 True 否則回傳 False,A < B
>=檢查左邊值是否大于等于右邊值,如果是回傳 True 否則回傳 False,A >= B
<=檢查左邊值是否小于等于右邊值,如果是回傳 True 否則回傳 False,A <= B

  • 邏輯運算子和C語言一樣
運算子描述實體
&&如果兩邊的運算元都是 True,則條件 True,否則為 False,A && B
\|\|如果兩邊的運算元有一個 True,則條件 True,否則為 False,A || B
!如果條件為 True,則邏輯 NOT 條件 False,否則為 True,!A

  • 位運算子和C語言幾乎一樣
    • 新增一個&^運算子
運算子描述實體
&參與運算的兩數各對應的二進位相與, 對應位只要都是1結果就為1A & B
\|參與運算的兩數各對應的二進位相或,對應位只要其中一個是1結果就為1A | B
^參與運算的兩數各對應的二進位相異或,對應位只要不同結果就是1A ^ B
<<左移運算子,左移n位就是乘以2的n次方A << 2
>>右移運算子,右移n位就是除以2的n次方B >> 2
&^邏輯清零運算子, B對應位是1,A對應位清零,B對應位是0, A對應位保留原樣A &^ B
int main(){
	/*
	  0110      a
	&^1011      b 如果b位位1,那么結果為0, 否則結果為a位對應的值
	----------
	  0100
	*/
	a1 := 6
	b1 := 11
	res1 := a1 &^ b1
	fmt.Println("res1 = ", res1) // 4

	/*
	  1011      a
	&^1101      b 如果b位位1,那么結果為0, 否則結果為a位對應的值
	----------
	  0010
	*/
	a2 := 11
	b2 := 13
	res2 := a2 &^ b2
	fmt.Println("res2 = ", res2) // 2
}

  • 賦值運算子和C語言幾乎一樣
    • 新增一個&^=運算子
運算子描述實體
=將右邊賦值給左邊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
&^=位邏輯清零賦值C &^= 2 等于 C = C &^ 2

流程控制陳述句對比

  • C語言流程控制中的if、switch、for在Go語言都可以使用
  • C語言中的四大跳轉陳述句return、break、continue、goto在Go語言都可以使用
  • Go語言除了實作C語言中if、switch、for、return、break、continue、goto的基本功能以外,還對if、switch、for、break、continue進行了增強
    • 例如: if 條件運算式前面可以添加初始化運算式
    • 例如: break、continue可以指定標簽
    • 例如: switch陳述句可以當做if/elseif來使用
    • … …
  • 值得注意的是Go語言中沒有while回圈和dowhile回圈, 因為它們能做的Go語言中的for回圈都可以做

函式和方法對比

  • C語言定義函式格式
回傳值型別 函式名稱(形參串列) {
        函式體相關陳述句;
        return 回傳值;
}
  • Go語言定義函式格式
func  函式名稱(形參串列)(回傳值串列) {
        函式體相關陳述句;
        return 回傳值;
}
  • C語言中沒有方法的概念, 但是Go語言中有方法
    • 對于初學者而言,可以簡單的把方法理解為一種特殊的函式
func  (接收者 接受者型別)函式名稱(形參串列)(回傳值串列) {
        函式體相關陳述句;
        return 回傳值;
}

編程思想對比

  • C語言是一門面向程序的編程語言
    • 面向程序: 按部就班, 親力親為,關注的是我應該怎么做?
    • 做飯例子: 面向程序做飯
      • 1.上街買菜
      • 2.摘菜
      • 3.洗菜
      • 4.切菜
      • 5.開火炒菜
      • 6.淘米煮飯
      • 7.吃飯
  • Go語言是門面向物件的編程語言
    • 面向物件:化繁為簡, 能不自己干自己就不干,關注的是我應該讓誰來做?
    • 做飯例子: 面向物件做飯
    • 1.找個會做飯女朋友 or 男朋友
    • 2.老婆我餓了 or 老公我餓了
    • 3.躺著…等她/他把飯做好
    • 4.吃飯
  • 不要把面向程序和面向物件想象得那么神奇, 它們只是思考問題的方式不同而已

其它新增特性

  • 介面
  • 并發
  • 反射
  • 例外處理

Go語言SDK安裝和配置

  • 什么是SDK

    • 軟體開發工具包(外語首字母縮寫:SDK、外語全稱:**SoftwareDevelopmentKit)**一般都是一些軟體工程師為特定的軟體包、軟體框架、硬體平臺、作業系統等建立應用軟體時的開發工具的集合
    • 如果不安裝SDK, 你可以撰寫Go語言代碼, 但是你不能編譯執行撰寫好的Go語言代碼
  • 如何安裝?

  • 1.下載SDK安裝包,地址: https://golang.google.cn/dl/

    • 由于新版本一般不太穩定, 所以我們選擇下載上一個版本
  • 2.運行圖形化安裝包




  • 3.檢測配置環境變數


    • 3.1.添加GOROOT環境變數
      • 用于告訴作業系統,我們把Go語言SDK安裝到哪了
  • 3.2.配置GOPATH環境變數
    • 用于告訴作業系統,將來我們要在哪里撰寫Go語言程式
      image.png
  • 3.3.配置GoBin環境變數
    • 用于告訴作業系統,去哪查找Go語言提供的一些應用程式

  • 最終結果
  • 4.檢查是否安裝配置成功
    • 4.1打開CMD
    • 4.2輸入go version
    • 4.3輸入go env

安裝Go語言開發工具

  • 記事本(開發效率極低)
  • Vim(初學者入門門檻高)
  • VSCode(不喜歡)
  • Sublime Test(不喜歡)
  • GoLand(喜歡,當收費)
  • LiteIDE(開源免費, 跨平臺運行,輕量級)
  • 生男生女都一樣, 最關鍵是你中意哪個就用哪個

Goland安裝

  • 下載安裝包: 點我下載Goland
  • 提取碼:lm7v
  • 運行安裝檔案
  • 瘋狂下一步




  • 激活程式: 自行淘寶JetBrains 激活(僅供學生黨參考, 在職人員請支持正版)
  • 看不習慣英文的可以自行百度Goland漢化包
  • 打開專案檔案夾

    在這里插入圖片描述

  • 測驗開發工具是否安裝正確


    在這里插入圖片描述

    在這里插入圖片描述

  • 其它問題:
    • 提示沒有安裝JVM
    • 下載Java SDK 點我下載
    • 安裝即可

Go語言程式組成

  • 和C語言程式一樣,Go語言程式也是由眾多函陣列成的
  • 和C語言程式一樣,程式運行時系統會***自動呼叫***名稱叫做***main的函式***
  • 和C語言程式一樣,如果一個程式***沒有主函式***,則這個程式***不具備運行能力***
  • 和C語言程式一樣,一個Go語言程式***有且只能有一個主函式***

Go語言程式主函式定義格式

  • C語言main函式格式
int main(int argc, const char * argv[]) {
    return 0;
}
  • Go語言main函式格式
    • func 告訴系統這是一個函式
    • main主函式固定名稱
    • 函式左括號必須和函式名在同一行
    • main函式必須在main包中
// 告訴系統當前撰寫的代碼屬于哪個包
package main
// 定義了一個名稱叫做main的函式
func main() {
}

Go語言HelloWorld

package main // 告訴系統當前代碼屬于main這個包
import "fmt" // 匯入列印函式對應的fmt包
func main() {
        // 通過包名.函式名稱的方式, 利用fmt包中的列印函式輸出陳述句
	fmt.Println("Hello World!!!")
}

Go語言HelloWorld和C語言HelloWorld異同

  • 1.檔案型別不同
    • C語言代碼保存在.c為后綴的檔案中
    • Go語言代碼保存在.go為后綴的檔案中
  • 2.代碼管理方式不同
    • C語言程式用***檔案***的方式管理代碼
      • C語言會把不同型別的代碼放到不同的.c檔案中, 然后再撰寫對應的.h檔案
      • 需要使用時直接通過#include匯入對應檔案的.h檔案即可
    • Go語言程式用的形式管理代碼
      • 我們會把不同型別的代碼放到不同的.go檔案中,然后通過package給該檔案指定一個包名
      • 需要使用時直接通過import匯入對應的包名即可
  • 3.main函式書寫檔案不同
  • C語言中main函式可以寫在任意檔案中, 只要保證一個程式只有一個main函式即可
  • Go語言中main函式只能寫在包名為main的檔案夾中, 同樣需要保存一個程式只有一個main函式
  • 4.函式撰寫的格式不同
    • C語言中函式的格式為
      • 注意:C語言函式的左括號可以和函式名稱在同一行, 也可以不在同一行
回傳值型別 函式名稱(形參串列) {
        函式體相關陳述句;
        return 回傳值;
}
  • Go語言函式定義格式
    注意:Go語言函式的左括號必須和函式名稱在同一行,否則會報錯
func  函式名稱(形參串列)(回傳值串列) {
        函式體相關陳述句;
        return 回傳值;
}
  • 5.函式呼叫的格式不同
    • C語言通過#include匯入.h檔案后,直接通過函式名稱呼叫函式
    • Go語言通過import匯入對應的包后,需要通過包名.函式名稱的方式呼叫
#include <stdio.h>
#include "calculate.h"
int main()
{
    int res = sum(2, 3); // 直接利用函式名稱呼叫函式
    printf("res = %d!\n", res);
    return 0;
}
package main
import (
	"fmt"
	"lesson_1/calculate"
)
func main() {
	res := calculate.Sum(2, 3) // 使用包名.函式名稱呼叫函式
	fmt.Println("res1 = ", res)
}
  • 6.陳述句的結束方式不同
    • C語言中每條陳述句都必須以分號結尾
    • Go語言中每條陳述句后面不用添加分號(編譯器會自動添加)
#include <stdio.h>
#include "calculate.h"
int main()
{
    int res = sum(2, 3); // 不寫分號會報錯
    printf("res = %d!\n", res); // 不寫分號會報錯
    return 0; // 不寫分號會報錯
}
package main
import (
	"fmt"
	"lesson_1/calculate"
)
func main() {
	res := calculate.Sum(2, 3) // 不用寫分號
	fmt.Println("res1 = ", res) // 不用寫分號
}

Go語言注釋

  • 和C語言一樣,Go語言也支持單行注釋和多行注釋, 并且所有注釋的特性都和C語言一樣
    • 單行注釋 // 被注釋內容
    • 多行注釋 /* 被注釋內容*/
  • 在Go語言中,官方更加推薦使用單行注釋,而非多行注釋(詳情可以直接查看Go官方原始碼)

Go語言編碼風格

  • 1.go程式撰寫在.go為后綴的檔案中
  • 2.包名一般使用檔案所在檔案夾的名稱
  • 2.包名應該簡潔、清晰且全小寫
  • 3.main函式只能撰寫在main包中
  • 4.每一條陳述句后面可以不用撰寫分號(推薦)
  • 5.如果沒有撰寫分號,一行只能撰寫一條陳述句
  • 6.函式的左括號必須和函式名在同一行
  • 7.匯入包但沒有使用包編譯會報錯
  • 8.定義區域變數但沒有使用變數編譯也會報錯
  • 9.定義函式但沒有使用函式不會報錯
  • 10.給方法、變數添加說明,盡量使用單行注釋

關鍵字

  • Go語言中的關鍵字和C語言中的關鍵字的含義樣, 是指被Go語言賦予特殊含義的單詞
  • Go語言中關鍵字的特征和C語言也一樣
    • 全部都是小寫
    • 在開發工具中會顯示特殊顏色
  • Go語言中關鍵字的注意點和C語言也一樣
    • 因為關鍵字在C語言中有特殊的含義, 所以不能用作變數名、函式名等

C語言關鍵字和Go語言關鍵字對比

  • C語言中一共有32個關鍵字
12345678
ifelseswitchcasedefaultbreakreturngoto
dowhileforcontinuetypedefstructenumunion
charshortintlongfloatdoublevoidsizeof
signedunsignedconstautoregisterstaticexternvolatile
  • Go語言中一共有25個關鍵字
12345678
ifelseswitchcasedefaultbreakreturngoto
fallthroughforcontinuetypestructvarconstmap
funcinterfacerangeimportpackagedefergoselect
chan

  • Go語言中除了關鍵字以外,還有30多個預定義識別符號
內建常量
truefalseiotanil
內建型別
intint8int16int32
int64uintuint8uint16
uint32uint64uintptrfloat32
float64complex64complex128bool
byterunestringerror
內建函式
makelencapnew
appendcopydeletereal
imagpanicrecovercomplex

識別符號

  • Go語言中的識別符號和C語言中的識別符號的含義樣, 是指程式員在程式中自己起的名字(變數名稱、函式名稱等)

  • 和C語言一樣Go語言識別符號也有一套命名規則, Go語言識別符號的命名規則幾乎和C語言一模一樣
    • 只能由字母(a~z、 A~Z)、數字、下劃線組成
    • 不能包含除下劃線以外的其它特殊字串
    • 不能以數字開頭
    • 不能是Go語言中的關鍵字
    • 識別符號嚴格區分大小寫, test和Test是兩個不同的識別符號
  • 和C語言識別符號命名規則不同的是
    • Go語言中_單獨作為識別符號出現時, 代表空識別符號, 它對應的值會被忽略
      package main
      
      import "fmt"
      
      func main() {
      	// 將常量10保存到名稱叫做num的變數中
      	var num int = 10
      	fmt.Println("num = ", num)
      
      	// 忽略常量20,不會分配存盤空間,也不會保存常量20
      	//var _ int = 20
      	//fmt.Println("_ = ", _) // cannot use _ as value
      
      	// Go語言中如果定義了變數沒有使用, 那么編譯會報錯(sub declared and not used)
      	// 所以如果我們只使用了sum,沒有使用sub會報錯
      	// 為了解決這個問題, 我們可以使用_忽略sub的值
      	//var sum, sub int = calculate(20, 10)
      	var sum, _ int = calculate(20, 10)
      	fmt.Println("sum = ", sum)
      
      }
      
      func calculate(a, b int)(int, int)  {
      	var sum int = a + b
      	var sub int = a - b
      	return sum, sub
      }
    
    ```go
    + Go語言默認的編碼方式就是UTF-8, 所以Go語言支持中文, 所以可以用中文作為識別符號(非常非常非常不推薦)
    ```go
    package main
    
    import "fmt"
    
    func main() {
        // 不會報錯, 可以正常運行
        var 年齡 int = 33
        fmt.Println("年齡 = ", 年齡) // 33
    
        // 不會報錯, 可以正常運行
        var 結果 int = 計算器(10, 20)
        fmt.Println("結果 = ", 結果) // 30
    }
    func 計算器(第一個變數, 第二個變數 int)int  {
        return 第一個變數 + 第二個變數
    }
    

  • 和C語言一樣,識別符號除了有命名規則以外,還有識別符號命名規范
    • 規則必須遵守, 規范不一定要遵守, 但是建議遵守
    • Go語言的命名規范和C語言一樣, 都是采用駝峰命名, 避免采用_命名
      • 駝峰命名: sendMessage / sayHello
      • _命名: send_message / say_hello

Go語言資料型別

  • Go語言本質是用C語言撰寫的一套高級開發語言, 所以Go語言中的資料型別大部分都是由C語言演變而來的

  • C語言資料型別

  • Go語言資料類型


  • C語言各資料型別占用記憶體空間
型別32位編譯器64位編譯器
char11
int44
float44
double88
short22
long48
long long88
void*48
  • Go語言各資料型別占用記憶體空間
型別32位編譯器64位編譯器本質
int8/uint811signed char/unsigned char
int16/uint1622signed short/unsigned short
int32/uint3244signed int/unsigned int
int64/uint6488signed long long int/unsigned long long int
byte11uint8/unsigned char
rune44int32/signed int
int48根據機器位數決定長度
uintptr48根據機器位數決定長度 uint32/uint64
float3244float
float6488double
true11char型別的整型
false11char型別的整型
  • 和C語言一樣,Go語言也提供了Sizeof計算變數的記憶體空間
    • 1.匯入import "unsafe"包
    • 2.通過unsafe.Sizeof()計算變數記憶體空間
package main

import (
	"fmt"
	"unsafe"
)

func main() {
	fmt.Println("int size = ", unsafe.Sizeof(int(0)))
	fmt.Println("int8 size = ", unsafe.Sizeof(int8(0)))
	fmt.Println("int16 size = ", unsafe.Sizeof(int16(0)))
	fmt.Println("int32 size = ", unsafe.Sizeof(int32(0)))
	fmt.Println("int64 size = ", unsafe.Sizeof(int64(0)))
	fmt.Println("uint size = ", unsafe.Sizeof(uint(0)))
	fmt.Println("uint8 size = ", unsafe.Sizeof(uint8(0)))
	fmt.Println("uint16 size = ", unsafe.Sizeof(uint16(0)))
	fmt.Println("uint32 size = ", unsafe.Sizeof(uint32(0)))
	fmt.Println("uint64 size = ", unsafe.Sizeof(uint64(0)))
	fmt.Println("uintptr size = ", unsafe.Sizeof(uintptr(0)))
	fmt.Println("byte size = ", unsafe.Sizeof(byte(0)))
	fmt.Println("rune size = ", unsafe.Sizeof(rune(0)))
	fmt.Println("float32 size = ", unsafe.Sizeof(float32(0)))
	fmt.Println("float64 size = ", unsafe.Sizeof(float64(0)))
	fmt.Println("true size = ", unsafe.Sizeof(true))
	fmt.Println("false size = ", unsafe.Sizeof(false))

}

  • Go語言基本資料型別內部實作
    • golang官方網站下載go1.4版本源代碼
      • 越老版本的代碼越純粹,越適合新手學習
      • 隨著代碼的更新迭代會逐步變得非常復雜, 所以此處建議下載1.4版本
    • 解壓后打開路徑: go\src\runtime\runtime.h
    • 得到如下實作代碼
// 第8行到35行
typedef	signed char		int8;
typedef	unsigned char		uint8;
typedef	signed short		int16;
typedef	unsigned short		uint16;
typedef	signed int		int32;
typedef	unsigned int		uint32;
typedef	signed long long int	int64;
typedef	unsigned long long int	uint64;
typedef	float			float32;
typedef	double			float64;

#ifdef _64BIT
typedef	uint64		uintptr;
typedef	int64		intptr;
typedef	int64		intgo; // Go's int
typedef	uint64		uintgo; // Go's uint
#else
typedef	uint32		uintptr;
typedef	int32		intptr;
typedef	int32		intgo; // Go's int
typedef	uint32		uintgo; // Go's uint
#endif

#ifdef _64BITREG
typedef	uint64		uintreg;
#else
typedef	uint32		uintreg;
#endif

// 第153行到157行
enum
{
	true	= 1,
	false	= 0,
};

install B 時刻:
Go本質就是用C語言撰寫的一門高級編程語言
所以江哥前面教你C語言就是為了今天能讓你看懂Go的實作代碼,做到知其然知其所以然
注意點: 企業開發中一般使用int, 因為int會根據你當前的作業系統自動轉換為int32和int64


Go語言變數

  • Go語言中變數的概念和C語言中也一樣, 所以我們直接來看下如何定義和使用變數即可
  • C語言中定義變數的格式
資料型別 變數名稱;
資料型別 變數名稱1, 變數名稱2;
#include <stdio.h>

int main(int argc, const char * argv[])
{
    int num1; // 先定義
    num1 = 10; // 后初始化
    printf("num1 = %d\n", num1);

    int num2 = 20; // 定義的同時初始化
    printf("num2 = %d\n", num2);

    // 注意: 同時定義多個變數,不支持定義時初始化, 只能先定義后初始化
    int num3, num4; //同時定義多個變數
    num3 = 30;
    num4 = 40;
    printf("num3 = %d\n", num3);
    printf("num4 = %d\n", num4);

    return 0;
}
  • Go語言中定義變數有三種格式
// 標準格式
var 變數名稱 資料型別 =;
// 自動推到型別格式
var 變數名稱 =;
// 簡短格式(golang官方推薦格式)
變數名稱 :=;
package main
import "fmt"
func main() {
	var num1 int // 先定義
	num1 = 10 // 后賦值
	fmt.Println("num1 = ", num1)

	var num2 int = 20 // 定義的同時賦值
	fmt.Println("num2 = ", num2)

	var num3  = 30 // 定義的同時賦值, 并省略資料型別
	fmt.Println("num3 = ", num3)
    
	num4  := 40 // 定義的同時賦值, 并省略關鍵字和資料型別
	/*
	num4  := 40 等價于
	var num4 int
	num4 = 40
	*/
	fmt.Println("num4 = ", num4)
}
  • 和C語言一樣,除了可以定義單個變數以外,還支持一次性定義多個變數
    • 方式一, 連續定義
     package main
     import "fmt"
     func main() {
     	var num1, num2 int // 先定義
     	num1 = 10	// 后賦值
     	num2 = 20
     	fmt.Println("num1 = ", num1)
     	fmt.Println("num2 = ", num2)
     
     	var num3, num4 int = 30, 40 // 定義的同時賦值
     	fmt.Println("num3 = ", num3)
     	fmt.Println("num4 = ", num4)
     
     	var num5, num6 = 50, 60 // 定義的同時賦值, 并省略資料型別
     	fmt.Println("num5 = ", num5)
     	fmt.Println("num6 = ", num6)
     
     	num7, num8 := 70, 80 // 定義的同時賦值, 并省略關鍵字和資料型別
     	fmt.Println("num7 = ", num7)
     	fmt.Println("num8 = ", num8)
     }
    
    • 方式二, 變陣列
      package main
      import "fmt"
      func main() {
      	var( // 先定義
      		num1 int
      		num2 float32
      	)
      	num1 = 10 // 后賦值
      	num2 = 3.14
      	fmt.Println("num1 = ", num1)
      	fmt.Println("num2 = ", num2)
    
      	var( // 定義的同時賦值
      		num3 int = 30
      		num4 float32 = 6.66
      	)
      	fmt.Println("num3 = ", num3)
      	fmt.Println("num4 = ", num4)
    
      	var( // 定義的同時賦值, 并省略資料型別
      		num5 = 50
      		num6 = 7.77
      	)
      	fmt.Println("num5 = ", num5)
      	fmt.Println("num6 = ", num6)
    
      	var( // 一行定義多個
      		num7, num8 = 70, 80
      		num9, num10 = 9.99, 100
      	)
      	fmt.Println("num7 = ", num7)
      	fmt.Println("num8 = ", num8)
      	fmt.Println("num9 = ", num9)
      	fmt.Println("num10 = ", num10)
      }
    

Go語言變數定義注意點

  • 簡短模式的含義是定義的同時初始化
package main
import "fmt"
func main() {
	num := 10
	num := 20 // 編譯報錯, 重復定義
	fmt.Println("num = ", num)
}
  • 一定不要把:=當做賦值運算子來使用
package main
import "fmt"
var num = 10 // 定義一個全域變數
func main() {
	num := 20 // 定義一個區域變數
	fmt.Println("num = ", num)
        test()
}
func test() {
	fmt.Println("num = ", num) // 還是輸出10
}

  • :=只能用于定義區域變數,不能用于定義全域變數
package main
import "fmt"
num := 10 // 編譯報錯
func main() {
	fmt.Println("num = ", num)
}
  • 使用:=定義變數時,不能指定var關鍵字和資料型別
package main
import "fmt"
func main() {
	//var num int := 10 // 編譯報錯
	//var num := 10 // 編譯報錯
	num int := 10 // 編譯報錯
	fmt.Println("num = ", num)
	fmt.Println("num = ", num)
}
  • 變陣列中不能夠使用:=
package main
import "fmt"
func main() {
	var(
		num := 10 // 編譯報錯
	)
	fmt.Println("num = ", num)
}
  • 通過:=同時定義多個變數, 必須給所有變數初始化
package main
import "fmt"
func main() {
	//num1, num2 := 666, 888 // 正確
	num1, num2 := 666 // 報錯
	fmt.Printf("%d, %d\n", num1, num2)
}
  • 通過:=同時定義多個變數, 只要任意一個變數沒有定義過,都會做退化賦值操作
package main
import "fmt"
func main() {
	// 定義一個變數num1
	num1 := 10
	// 同時定義兩個變數num1和num2, 由于num2從來沒有定義過,
	// 所以對于num1來說:=退化為賦值運算子, 而對于num2來說:=仍然是定義+賦值
	num1, num2 := 20, 30
	fmt.Println("num1 = ", num1)
	fmt.Println("num2 = ", num2)
}
package main
import "fmt"
func main() {
	num1 := 10
	num2 := 20
	// 報錯, 因為num1,和num2都已經被定義過
	// 至少要有任意一個變數沒有被定義過,才會退化賦值
	num1, num2 := 30, 40
	fmt.Println("num1 = ", num1)
	fmt.Println("num2 = ", num2)
}
  • 定義的區域變數或者匯入的包沒有被使用, 那么編譯器會報錯,無法編譯運行,但是定義的全域變數沒有被使用,編譯器不會報錯, 可以編譯運行

區域變數和全域變數

  • 和C語言一樣,按照變數的作用域,我們可以把變數劃分為區域變數和全域變數

  • Go語言中區域變數的概念以及全域變數的概念和C語言一模一樣

  • 區域變數:

    • 定義在函式內部的變數以及函式的形參稱為區域變數
    • 作用域:從定義哪一行開始直到與其所在的代碼塊結束
    • 生命周期:從程式運行到定義哪一行開始分配存盤空間到程式離開該變數所在的作用域
  • 全域變數:

    • 定義在函式外面的變數稱為全域變數
    • 作用域范圍:從定義哪行開始直到檔案結尾
    • 生命周期:程式一啟動就會分配存盤空間,直到程式結束
  • 和C語言不同的是, C語言中可以定義相同名稱的全域變數, 而Go語言中無論全域變數還是區域變數, 只要作用域相同都不能出現同名的變數

package main
import "fmt"
//var num1 int
//var num1 int // 報錯, 重復定義
var num3 int
func main() {
	//var num2
	//var num2 // 報錯, 重復定義
	
	var num3 int // 不報錯, 因為作用域不同
	fmt.Println("num3 = ", num3)
}

  • C語言中全域變數沒有賦值,那么默認初始值為0, 區域變數沒有賦值,那么默認初始值是隨機值
  • Go語言中無論是全域變數還是區域變數,只要定義了一個變數都有默認的0值
    • int/int8/int16/int32/int64/uint/uint8/uint16/uint32/uint64/byte/rune/uintptr的默認值是0
    • float32/float64的默認值是0.0
    • bool的默認值是false
    • string的默認值是""
    • pointer/function/interface/slice/channel/map/error的默認值是nil
    • 其它復合型別array/struct默認值是內部資料型別的默認值
package main
import "fmt"
func main() {
	var intV int // 整型變數
	var floatV float32 // 實型變數
	var boolV bool // 布爾型變數
	var stringV string // 字串變數
	var pointerV *int // 指標變數
	var funcV func(int, int)int // function變數
	var interfaceV interface{} // 介面變數
	var sliceV []int // 切片變數
	var channelV chan int // channel變數
	var mapV map[string]string // map變數
	var errorV error // error變數

	fmt.Println("int = ", intV) // 0
	fmt.Println("float = ", floatV) // 0
	fmt.Println("bool = ", boolV) // false
	fmt.Println("string = ", stringV) // ""
	fmt.Println("pointer = ", pointerV) // nil
	fmt.Println("func = ", funcV) // nil
	fmt.Println("interface = ", interfaceV) // nil
	fmt.Println("slice = ", sliceV) // []
	fmt.Println("slice = ", sliceV == nil) // true
	fmt.Println("channel = ", channelV) // nil
	fmt.Println("map = ", mapV) // map[]
	fmt.Println("map = ", mapV == nil) // true
	fmt.Println("error = ", errorV) // nil

	var arraryV [3]int // 陣列變數
	type Person struct{
		name string
		age int
	}
	var structV Person // 結構體變數
	fmt.Println("arrary = ", arraryV) // [0, 0, 0]
	fmt.Println("struct = ", structV) // {"" 0}
}

資料型別轉換

  • C語言中資料可以隱式轉換或顯示轉換, 但是Go語言中資料只能顯示轉換
  • C語言隱式轉換
#include <stdio.h>
int main(){
  // 隱式轉換:自動將實型10.6轉換為整型后保存
   int a = 10.6;
 // 自動型別提升: 運算時會自動將小型別轉換為大型別后運算
  double b = 1.0 / 2; // 等價于1.0 / 2.0
}
  • C語言顯示轉換(強制轉換)
#include <stdio.h>
int main(){
  // 顯示轉換:強制將實型10.6轉換為整型后保存
  int a = (int)10.5;
}
  • Go語言數值型別之間轉換
    • 格式: 資料型別(需要轉換的資料)
    • 注意點: 和C語言一樣資料可以從大型別轉換為小型別, 也可以從小型別轉換為大型別. 但是大型別轉換為小型別可能會丟失精度
      package main
      import "fmt"
      func main() {
      	var num0 int = 10
      	var num1 int8 = 20
      	var num2 int16
      	//num2 = num0 // 編譯報錯, 不同長度的int之間也需要顯示轉換
      	//num2 = num1 // 編譯報錯, 不同長度的int之間也需要顯示轉換
      	num2 = int16(num0)
      	num2 = int16(num1)
      	fmt.Println(num2)
      
      	var num3 float32 = 3.14
      	var num4 float64
      	//num4 = num3 // 編譯報錯, 不同長度的float之間也需要顯示轉換
      	num4 = float64(num3)
      	fmt.Println(num4)
      
      	var num5 byte = 11
      	var num6 uint8 // 這里不是隱式轉換, 不報錯的原因是byte的本質就是uint8
      	num6 = num5
      	fmt.Println(num6)
      
      	var num7 rune = 11
      	var num8 int32
      	num8 = num7 // 這里不是隱式轉換, 不報錯的原因是byte的本質就是int32
      	fmt.Println(num8)
      }
    

數值型別和字串型別之間轉換

  • Go語言中不能通過 資料型別(變數)的格式將數值型別轉換為字串, 也不能通過 資料型別(變數)的格式將字串轉換為數值型別
	package main
	import "fmt"
	func main() {
		var num1 int32 = 65
		// 可以將整型強制轉換, 但是會按照ASCII碼表來轉換
		// 但是不推薦這樣使用
		var str1 string = string(num1)
		fmt.Println(str1)

		var num2 float32 = 3.14
		// 不能將其它基本型別強制轉換為字串型別
		var str2 string = string(num2)
		fmt.Println(str2)

		var str3 string = "97"
		// 不能強制轉換, cannot convert str2 (type string) to type int
		var num3  int = int(str3)
		fmt.Println(num3)
	}
  • 數值型別轉字串型別strconv..FormatXxx()
  package main
  import "fmt"
  func main() {
  	var num1 int32 = 10
  	// 第一個引數: 需要被轉換的整型,必須是int64型別
  	// 第二個引數: 轉換為幾進制,  必須在2到36之間
  	// 將32位十進制整型變數10轉換為字串,并繼續保留10進制格式
  	str1 := strconv.FormatInt(int64(num1), 10)
  	fmt.Println(str1) // 10
  	// 將32位十進制整型變數10轉換為字串,并轉換為2進制格式
  	str2 := strconv.FormatInt(int64(num1), 2)
  	fmt.Println(str2) // 1010

  	var num5 float64 = 3.1234567890123456789
  	// 第一個引數: 需要轉換的實型, 必須是float64型別
  	// 第二個引數: 轉換為什么格式,f小數格式, e指數格式
  	// 第三個引數: 轉換之后保留多少位小數, 傳入-1按照指定型別有效位保留
  	// 第四個引數: 被轉換資料的實際位數,float32就傳32, float64就傳64
  	// 將float64位實型,按照小數格式并保留默認有效位轉換為字串
  	str3 := strconv.FormatFloat(num5, 'f', -1, 64)
  	fmt.Println(str3) // 3.1234567
  	str4 := strconv.FormatFloat(num5, 'f', -1, 64)
  	fmt.Println(str4) // 3.1234567890123457
  	// 將float64位實型,按照小數格式并保留2位有效位轉換為字串
  	str5 := strconv.FormatFloat(num5, 'f', 2, 64)
  	fmt.Println(str5) // 3.12
  	// 將float64位實型,按照指數格式并保留2位有效位轉換為字串
  	str6 := strconv.FormatFloat(num5, 'e', 2, 64)
  	fmt.Println(str6) // 3.12

  	var num6 bool = true
  	str7 := strconv.FormatBool(num6)
  	fmt.Println(str7) // true
  }
  • 字串型別轉數值型別strconv.ParseXxx()
  package main
  import "fmt"
  func main() {
  	var str1 string = "125"
  	// 第一個引數: 需要轉換的資料
  	// 第二個引數: 轉換為幾進制
  	// 第三個引數: 轉換為多少位整型
  	// 注意點: ParseInt函式會回傳兩個值, 一個是轉換后的結果, 一個是錯誤
  	// 如果被轉換的資料轉換之后沒有超出指定的范圍或者不能被轉換時,
  	// 那么錯誤為nil, 否則錯誤不為nil
  	// 將字串"125"轉換為10進制的int8
  	num1, err := strconv.ParseInt(str1, 10, 8)
  	if err != nil {
  		fmt.Println(err)
  	}
  	fmt.Println(num1)

  	var str2 string = "150"
  	// 將字串"150"轉換為10進制的int8
  	// 由于int8的取值范圍是-128~127, 所以轉換之后超出了指定的范圍, error不為nil
  	num2, err := strconv.ParseInt(str2, 10, 8)
  	if err != nil {
  		fmt.Println(err)
  	}
  	fmt.Println(num2)

  	var str3 string = "3.1234567890123456789"
  	// 第一個引數: 需要轉換的資料
  	// 第二個引數: 轉換為多少位小數, 32 or 64
  	// ParseFloat同樣有兩個回傳值, 如果能夠正常轉換則錯誤為nil, 否則不為nil
  	num3, err := strconv.ParseFloat(str3, 32)
  	if err != nil {
  		// 例如: 把字串"3.14abc"轉換為小數就會報錯, 因為"3.14abc"不是一個小數
  		fmt.Println(err)
  	}
  	fmt.Println(num3)

  	var str4 string = "true"
  	// 第一個引數: 需要轉換的資料
  	// ParseBool同樣有兩個回傳值, 如果能夠正常轉換則錯誤為nil, 否則不為nil
  	num4, _ := strconv.ParseBool(str4)
  	fmt.Println(num4)
  }
  • 字串型別轉換為數值型別時,如果不能轉換除了回傳error以外,還會回傳對應型別的默認值
	package main
	import "fmt"
	func main() {
		var str1 string = "abc"
		num1, _ := strconv.ParseInt(str1, 10, 32)
		fmt.Println(num1) // 0

		num2, _ := strconv.ParseFloat(str1, 32)
		fmt.Println(num2) // 0

		num3, _ := strconv.ParseBool(str1)
		fmt.Println(num3) // false
	}
  • 看完上面的代碼有沒有種想打人的感覺? 如果有那么請繼續往下看
  • 字串型別和整型快速轉換
   package main
   import "fmt"
   func main() {
   	var num1 int32 = 110
   	// 快速將整型轉換為字串型別
   	// 注意:Itoa方法只能接受int型別
   	var str1 string = strconv.Itoa(int(num1))
   	fmt.Println(str1)

   	var str2 string = "666"
   	// 快速將字串型別轉換為整型
   	// 注意: Atoi方法回傳兩個值, 一個值是int,一個值是error
   	// 如果字串能被轉換為int,那么error為nil, 否則不為nil
   	num2, err := strconv.Atoi(str2)
   	if err != nil{
   		fmt.Println(err)
   	}
   	fmt.Println(num2)
   }
  • 數值型別轉字串型別其它方式
  package main
  import "fmt"
  func main() {
  	var num1 int32 = 110
  	// Sprintf函式和Printf函式很像, 只不過不是輸出而將格式化的字串回傳給我們
  	var str1 string = fmt.Sprintf("%d", num1)
  	fmt.Println(str1)

  	var num2 float32 = 3.14
  	var str2 string = fmt.Sprintf("%f", num2)
  	fmt.Println(str2)

  	var num3 bool = true
  	var str3 string = fmt.Sprintf("%t", num3)
  	fmt.Println(str3)
  }

Go語言常量

  • 和C語言一樣Go語言中的常量也分為整型常量實型常量字符常量字串常量自定義常量

  • 自定義常量

    • C語言自定義常量: const 資料型別 常量名稱 = 值;
      #include <stdio.h>
      int main(int argc, const char * argv[])
      {
          const float PI = 998;
          PI = 110; // 報錯
          printf("PI = %d\n", PI );
          return 0;
      }
    
    • Go語言自定義常量: const 常量名稱 資料型別 = 值or const 常量名稱 = 值
    package main
    import "fmt"
    func main() {
      //const PI float32 = 3.14
      //PI = 110 // 報錯
      //fmt.Println("PI = ", PI )
    
      const PI = 3.14
      PI = 110 // 報錯
      fmt.Println("PI = ", PI )
    }
    
    • 除此之外Go語言還支持一次性定義多個常量
    package main
    import "fmt"
    func main() {
      // 多重賦值方式
      const num1, num2 int  = 100, 200
      fmt.Println("num1 = ", num1)
      fmt.Println("num2 = ", num2)
    
      // 常量組方式
      const (
      	num3 = 100
      	num4 = 200
      )
      fmt.Println("num3 = ", num3)
      fmt.Println("num4 = ", num4)
    
      // 常量組+多重賦值
      const (
      	num5, num6 = 100, 200
      	num7 = 300
      )
      fmt.Println("num5 = ", num5)
      fmt.Println("num6 = ", num6)
      fmt.Println("num7 = ", num7)
    }
    

  • Go語言自定義常量注意點
    • 定義的區域變數或者匯入的包沒有被使用, 那么編譯器會報錯,無法編譯運行
    • 但是定義的常量沒有被使用,編譯器不會報錯, 可以編譯運行
    package main
    import "fmt"
    func main() {
      // 可以編譯運行
      const PI float32 = 3.14
    }
    
    • 在常量組中, 如果上一行常量有初始值,但是下一行沒有初始值, 那么下一行的值就是上一行的值
    package main
    import "fmt"
    func main() {
      const (
      	num1 = 998
      	num2 // 和上一行的值一樣
      	num3 = 666
      	num4 // 和上一行的值一樣
      	num5 // 和上一行的值一樣
      )
      fmt.Println("num1 = ", num1) // 998
      fmt.Println("num2 = ", num2) // 998
      fmt.Println("num3 = ", num3) // 666
      fmt.Println("num4 = ", num4) // 666
      fmt.Println("num5 = ", num5) // 666
    
      const (
      	num1, num2 = 100, 200
      	num3, num4  // 和上一行的值一樣, 注意變數個數必須也和上一行一樣
      )
      fmt.Println("num1 = ", num1)
      fmt.Println("num2 = ", num2)
      fmt.Println("num3 = ", num3)
      fmt.Println("num4 = ", num4)
    }
    

  • 列舉常量
    • C語言中列舉型別的本質就是整型常量
    • Go語言中沒有C語言中明確意義上的enum定義, 但是可以借助iota識別符號來實作列舉型別
  • C語言列舉格式:
 enum 列舉名 {
    列舉元素1,
    列舉元素2,
    … …
 };
  • C語言列舉中,如果沒有指定初始值,那么從0開始遞增
#include <stdio.h>
int main(int argc, const char * argv[])
{
    enum Gender{
        male,
        female,
        yao,
    };
//    enum Gender g = male;
//    printf("%d\n", g); // 0
//    enum Gender g = female;
//    printf("%d\n", g); // 1
    enum Gender g = yao;
    printf("%d\n", g); // 2
    return 0;
}
  • C語言列舉中, 如果指定了初始值,那么從指定的數開始遞增
#include <stdio.h>
int main(int argc, const char * argv[])
{
    enum Gender{
        male = 5,
        female,
        yao,
    };
//    enum Gender g = male;
//    printf("%d\n", g); // 5
//    enum Gender g = female;
//    printf("%d\n", g); // 6
    enum Gender g = yao;
    printf("%d\n", g); // 7
    return 0;
}

  • Go語言實作列舉格式
const(
  列舉元素1 = iota
  列舉元素2 = iota
  ... ...
)
  • 利用iota識別符號識別符號實作從0開始遞增的列舉
    package main
    import "fmt"
    func main() {
    	const (
    		male = iota
    		female = iota
    		yao = iota
    	)
    	fmt.Println("male = ", male) // 0
    	fmt.Println("male = ", female) // 1
    	fmt.Println("male = ", yao) // 2
    }
    
  • iota注意點:
    • 在同一個常量組中,iota從0開始遞增, 每一行遞增1
    • 在同一個常量組中,只要上一行出現了iota,那么后續行就會自動遞增
    package main
    import "fmt"
    func main() {
      const (
      	male = iota // 這里出現了iota
      	female // 這里會自動遞增
      	yao
      )
      fmt.Println("male = ", male) // 0
      fmt.Println("male = ", female) // 1
      fmt.Println("male = ", yao) // 2
    }
    
    • 在同一個常量組中,如果iota被中斷, 那么必須顯示恢復
    package main
    import "fmt"
    func main() {
      const (
      	male = iota 
      	female = 666 // 這里被中斷, 如果沒有顯示恢復, 那么下面沒有賦值的常量都和上一行一樣
      	yao
      )
      fmt.Println("male = ", male) // 0
      fmt.Println("male = ", female) // 666
      fmt.Println("male = ", yao) // 666
    }
    
    package main
    import "fmt"
    func main() {
      const (
      	male = iota 
      	female = 666 // 這里被中斷
      	yao = iota // 這里顯示恢復, 會從當前常量組第一次出現iota的地方開始,每一行遞增1, 當前是第3行,所以值就是2
      )
      fmt.Println("male = ", male) // 0
      fmt.Println("male = ", female) // 666
      fmt.Println("male = ", yao) // 2
    }
    
    • iota也支持常量組+多重賦值, 在同一行的iota值相同
    package main
    import "fmt"
    func main() {
      const (
        a, b = iota, iota
        c, d = iota, iota
      )
      fmt.Println("a = ", a) // 0
      fmt.Println("b = ", b) // 0
      fmt.Println("c = ", c) // 1
      fmt.Println("d = ", d) // 1
    }
    
    • iota自增默認資料型別為int型別, 也可以顯示指定型別
    package main
    import "fmt"
    func main() {
    const (
        male float32 = iota // 顯示指定型別,后續自增都會按照指定型別自增
        female
        yao
    )
    fmt.Printf("%f\n", male) // 0.0
    fmt.Printf("%f\n", female) // 1.0
    fmt.Printf("%f\n", yao) // 2.0
    fmt.Println("male = ", reflect.TypeOf(female)) // float32
    }
    
  • Go語言fmt包實作了類似C語言printf和scanf的格式化I/O, 格式化動作源自C語言但更簡單
    ##輸出函式
  • func Printf(format string, a …interface{}) (n int, err error)
    • 和C語言用法幾乎一模一樣, 只不過新增了一些格式化符號
    package main
    import "fmt"
    func main() {
        name := "微信搜索:代碼情緣"
        age := 33
        fmt.Printf("name = %s, age = %d\n", name, age) // name = lnj, age = 33
    }
    
    • 值得注意的是,輸出十進制只能通過%d,不能像C語言一樣通過%i
    • 除了和C語言一樣,可以通過%o、%x輸出八進制和十六進制外,還可以直接通過%b輸出二進制
    package main
    import "fmt"
    func main() {
        num := 15
        fmt.Printf("十進制 = %d\n", num)
        fmt.Printf("八進制 = %o\n", num)
        fmt.Printf("十六進制 = %x\n", num)
        fmt.Printf("二進制 = %b\n", num)
    }
    
    • 除此之外,Go語言還增加了%T控制符, 用于輸出值的型別
    package main
    import "fmt"
    func main() {
      type Person struct {
      	name string
      	age int
      }
      num1 := 10
      num2 := 3.14
      per := Person{"lnj", 33}
      fmt.Printf("num1 = %T\n", num1) // int
      fmt.Printf("num2 = %T\n", num2) // float64
      fmt.Printf("per = %T\n", per) // main.Person
    }
    
    • 除此之外,Go語言還增加了%v控制符,用于列印所有型別資料
      • Go語言中輸出某一個值,很少使用%d%f等, 一般都使用%v即可
      • 輸出復合型別時會自動生成對應格式后再輸出
    package main
    import "fmt"
    func main() {
      type Person struct {
      	name string
      	age int
      }
      num1 := 10
      num2 := 3.14
      per := Person{"lnj", 33}
      // 此時相當于把%v當做%d
      fmt.Printf("num1 = %v\n", num1) // 10
      // 此時相當于把%v當做%f
      fmt.Printf("num2 = %v\n", num2) // 3.14
    }
    
    • Go語言Printf函式其它特性,如寬度、標志、精度、長度、轉移符號等,和C語言一樣.

  • func Println(a …interface{}) (n int, err error)
    • 采用默認格式將其引數格式化并寫入標準輸出,
      • 輸出之后在結尾處添加換行
      • 傳入多個引數時, 會自動在相鄰引數之間添加空格
      • 傳入符合型別資料時, 會自動生成對應格式后再輸出
    package main
    import "fmt"
    func main() {
      num1 := 10
      num2 := 3.14
      fmt.Println(num1, num2) // 10 3.14
      fmt.Println("num1 =", num1, "num2 =", num2) // num1 = 10 num2 = 3.14
    
      type Person struct {
      	name string
      	age int
      }
      per := Person{"lnj", 33}
      fmt.Println(per) // {lnj 33}
    }
    

  • func Print(a …interface{}) (n int, err error)
    • 和Println幾乎一樣
      • 輸出之后不會在結尾處添加換行
      • 傳入多個引數時, 只有兩個相鄰的引數都不是字串,才會在相鄰引數之間添加空格
      • 傳入符合型別資料時, 會自動生成對應格式后再輸出
    package main
    import "fmt"
    func main() {
      num1 := 10
      num2 := 3.14
      fmt.Print(num1, num2) // 10 3.14
      fmt.Print("num1 =", num1, "num2 =", num2) // num1 =10 num2 =3.14
    
      type Person struct {
      	name string
      	age int
      }
      per := Person{"lnj", 33}
      fmt.Print(per) // {lnj 33}
    }
    

  • 以下三個函式和Printf/Println/Print函式一樣, 只不過上面三個函式是輸出到標準輸出, 而下面三個函式可以通過w指定輸出到什么地方
  • func Fprintf(w io.Writer, format string, a …interface{}) (n int, err error)
  • func Fprintln(w io.Writer, a …interface{}) (n int, err error)
  • func Fprint(w io.Writer, a …interface{}) (n int, err error)
package main
import (
	"fmt"
	"net/http"
	"os"
)
func main() {
	// os.Stdout 寫入到標準輸出
	name := "lnj"
	age := 33
	// 第一個引數: 指定輸出到什么地方
	// 第二個引數: 指定格式控制字串
	// 第三個引數: 指定要輸出的資料
	fmt.Fprintf(os.Stdout, "name = %s, age = %d\n", name, age)

	// http.ResponseWriter 寫入到網路回應
	http.HandleFunc("/", func(writer http.ResponseWriter, request *http.Request) {
		fmt.Fprintf(writer, "name = %s, age = %d\n", name, age)
	})
	http.ListenAndServe(":8888", nil)
}

  • 以下三個函式和Printf/Println/Print函式一樣, 只不過上面三個函式是輸出到標準輸出,
    而下面三個函式不會輸出,而是將字串回傳給我們
  • func Sprintf(format string, a …interface{}) string
  • func Sprint(a …interface{}) string
  • func Sprintln(a …interface{}) string
package main
import (
	"fmt"
	"net/http"
	"os"
)
func main() {
	name := "lnj"
	age := 33
	// 按照指定的格式生成字串
	str := fmt.Sprintf("name = %s, age = %d\n", name, age)
	// 輸出生成的字串
	fmt.Println(str)
}

輸入函式

  • func Scanf(format string, a …interface{}) (n int, err error)
    • 和C語言用法幾乎一模一樣, 但是只能輸入一行資料
    package main
    import "fmt"
    func main() {
        var num1 int
        var num2 int
        fmt.Scanf("%d%d", &num1, &num2)
        fmt.Println(num1, num2)
    }
    
  • func Scan(a …interface{}) (n int, err error)
    • 和C語言scanf函式幾乎一樣, 只不過不用指定格式化字串
    package main
    import "fmt"
    func main() {
      var num1 int
      var num2 int
      fmt.Scan(&num1, &num2)
      fmt.Println(num1, num2)
    
      var num3 float32
      var num4 float32
      fmt.Scan(&num3, &num4)
      fmt.Println(num3, num4)
    }
    
  • func Scanln(a …interface{}) (n int, err error)
    • 和C語言用法幾乎一模一樣, 只不過不用指定格式化字串, 并且只能輸入一行資料
    package main
    import "fmt"
    func main() {
        var num1 int
        var num2 int
        fmt.Scanln(&num1, &num2)
        fmt.Println(num1, num2)
    }
    

  • 以下三個函式和Scan/Scanln/Scanf函式一樣, 只不過上面三個函式是從標準輸入讀取資料, 而下面三個函式可以通過r指定從哪讀取資料
  • func Fscanf(r io.Reader, format string, a …interface{}) (n int, err error)
  • func Fscanln(r io.Reader, a …interface{}) (n int, err error)
  • func Fscan(r io.Reader, a …interface{}) (n int, err error)
  package main
  import "fmt"
  func main() {
	  var num1 int
	  var num2 int
	  // 第一個引數: 指定從哪讀取資料
	  // 第二個引數: 指定格式控制字串
	  // 第三個引數: 指定要輸出的資料
	  fmt.Fscanf(os.Stdin, "%d%d", &num1, &num2)
	  fmt.Println(num1, num2)


	  s := strings.NewReader("lnj 33")
	  var name string
	  var age int
	  // 從指定字串中掃描出想要的資料
	  // 注意:
	  fmt.Fscanf(s, "%s%d", &name, &age)
	  fmt.Println("name =",name, "age =",age)
  }
  • 以下三個函式和Scan/Scanln/Scanf函式一樣, 只不過上面三個函式是從標準輸入讀取資料, 而下面三個函式是從字串中讀取資料
  • func Sscan(str string, a …interface{}) (n int, err error)
  • func Sscanf(str string, format string, a …interface{}) (n int, err error)
  • func Sscanln(str string, a …interface{}) (n int, err error)
  package main
  import "fmt"
  func main() {
	str := "lnj 33"
	var name string
	var age int
	//fmt.Sscanf(str, "%s %d",&name, &age)
	//fmt.Sscanln(str,&name, &age)
	fmt.Sscan(str,&name, &age)
	fmt.Println("name =",name, "age =",age)
  }

go命令列操作指令

  • 標準go語言專案檔案目錄格式
    • 專案檔案夾就是GOPATH指向的檔案夾
    • src檔案夾是專門用于存放原始碼檔案的
      • main檔案夾是專門用于存盤package main包相關原始碼檔案的
      • 其它檔案夾是專門用于存盤除package main包以外原始碼檔案的
    • bin檔案夾是專門用于存盤編譯之后的可執行程式的
    • pag檔案夾是專門用于存盤編譯之后的.a檔案的

|—專案檔案夾
-----------|--------src檔案夾
-----------------------------|--------main檔案夾
-----------------------------|--------其它檔案夾
-----------|--------bin檔案夾
-----------|--------pkg檔案夾


  • go version 查看當前安裝的go版本
  • go env 查看當前go的環境變數
  • go fmt 格式化代碼
    • 會將指定檔案中凌亂的代碼按照go語言規范格式化
  • go run 命令檔案 編譯并運行go程式
    • package main包中包含main函式的檔案, 我們稱之為命令檔案
    • 其它包中的檔案, 我們稱之為原始碼檔案
  • go build 編譯檢查
    • 對于非命令檔案只會執行編譯檢查, 不會產生任何檔案
    • 對于命令檔案除了編譯檢查外,還會在當前目錄下生成一個可執行檔案
    • 對應只想編譯某個檔案, 可以在命令后面指定檔案名稱go build 檔案名稱
  • go install 安裝程式
    • 對于非命令檔案會執行編譯檢查, 并生成.a結尾的包, 放到 $GOPATH/pkg目錄中
    • 對于命令檔案會執行編譯檢查, 并生成可執行程式, 放到$GOPATH/bin目錄中

通過os包獲取命令列引數

  • C語言中的命令列引數
    • argc: argv中保存資料的個數
    • argv: 默認情況下系統只會傳入一個值, 這個值就是main函式執行檔案的路徑
    • 我們可以通過配置開發工具,或者命令列運行時以空格+引數形式傳遞其它引數
    • 注意點: 無論外界傳入的是什么型別, 我們拿到的都是字串型別
#include <stdio.h>
int main(int argc, const char * argv[])
{
    for(int i = 0; i < argc; i++){
        printf("argv[%d] = %s\n", i, argv[i]);
    }
    return 0;
}

  • Go語言中的命令列引數
    • Go語言中main函式沒有形參, 所以不能直接通過main函式獲取命令列引數
    • 想要獲取命令列引數必須匯入os包, 通過os包的Args獲取
    • 注意點: 無論外界傳入的是什么型別, 我們拿到的都是字串型別
package main

import (
	"fmt"
	"os" // 用于獲取命令列引數的包
)
func main() {
	// 1.獲取傳入引數個數
	num := len(os.Args)
	// 2.列印所有獲取到的引數
	for i := 0; i < num; i++ {
		fmt.Println(os.Args[i])
	}
}


通過flag包獲取命令列引數

  • Go語言中除了可以通過os包獲取命令列引數以外,還可以通過flag包獲取命令列引數
package main
import (
	"flag"
	"fmt"
)
func main() {
	/*
    flag.Xxxx(name, value, usage)
	第一個引數: 命令列引數名稱
	第二個引數: 命令列引數對應的默認值
	第三個引數: 命令列引數對應的說明
	*/
	// 1.設定命令列引數
	name := flag.String("name", "lnj", "請輸入人的姓名")
	age := flag.Int("age", 33, "請輸入人的年齡")
	// 2.將命令列引數決議到注冊的引數
	flag.Parse()
	// 3.使用命令列引數
	// 注意flag對應方法回傳的都是指標型別, 所以使用時必須通過指標訪問
	fmt.Println("name = ", *name)
	fmt.Println("age = ", *age)
}

  • flag獲取命令列引數第二種寫法
package main
import (
	"flag"
	"fmt"
)
func main() {
	/*
	flag.Xxxx(*type, name, value, usage)
	第一個引數:保存命令列引數變數地址
	第二個引數: 命令列引數名稱
	第三個引數: 命令列引數對應的默認值
	第四個引數: 命令列引數對應的說明
	*/
	// 1.定義變數,保存命令列引數的值
	var name string
	var age int
	// 2.設定命令列引數
	flag.StringVar(&name, "name", "lnj", "請輸入人的姓名")
	flag.IntVar(&age, "age", 33,"請輸入人的姓名")
	// 3.注冊決議命令列引數
	flag.Parse()
	// 4.使用命令列引數
	fmt.Println("name = ", name)
	fmt.Println("age = ", age)
}

os包和flag包獲取命令列引數對比

  • 通過os包獲取命令列引數
    • 如果用戶沒有傳遞引數報錯
    • 需要嚴格按照代碼中的順序傳遞引數, 否則造成資料混亂
    • 不能指定引數的名稱
    • 獲取到的資料都是字串型別
package main
import (
	"os"
	"fmt"
)
int main(){
	name := os.Args[1]
	age := os.Args[2]
	fmt.Println("name = ", name)
	fmt.Println("age = ", age)
}
  • 通過flag包獲取命令列引數

    • 如果用戶沒有傳遞引數不會報錯
    • 不需要嚴格按照代碼中的順序傳遞引數, 不會造成資料混亂
    • 可以指定引數的名稱
    • 獲取到的資料是我們自己指定的型別
package main
import (
	"flag"
	"fmt"
)
int main(){
	name := flag.String("name", "lnj", "請輸入人的姓名")
	age := flag.Int("age", 33, "請輸入人的年齡")
	// 2.注冊決議命令列引數
	flag.Parse()
	// 3.使用命令列引數
	// 注意flag對應方法回傳的都是指標型別, 所以使用時必須通過指標訪問
	fmt.Println("name = ", *name)
	fmt.Println("age = ", *age)
}

算數運算子

  • 算數運算子和C語言幾乎一樣
運算子描述實體
+相加A + B
-相減A - B
*相乘A * B
/相除B / A
%求余B % A
++自增A++
自減A–
  • 注意點:
    • 只有相同型別的資料才能進行運算
package main
import "fmt"
int main(){
	var num1 int32 = 10
	//var num2 int64 = num1 // 型別不同不能進行賦值運算
	var num2 int64 = int64(num1) // 型別不同不能進行賦值運算
	fmt.Println(num2)

	var num3 int32 = 10
	var num4 int64 = 20
	//var res int64 = num3 + num4 // 型別不同不能進行算數運算
	var res1 int64 = int64(num3) + num4 // 型別不同不能進行算數運算
	fmt.Println(res1)

	var num5 int32 = 10
	var num6 int64 = 20
	//var res2 bool = (num5 == num6) // 型別不同不能進行關系運算
	var res2 bool = (num5 == int32(num6)) // 型別不同不能進行關系運算
	fmt.Println(res2)

	// ... ... 其它以此類推
}
  • Go語言中++、–運算子不支持前置
    • 錯誤寫法: ++i; --i;
  • Go語言中++、–是陳述句,不是運算式,所以必須獨占一行
    • 錯誤寫法: a = i++; return i++;
package main
import "fmt"
func main() {
	num1 := 0
	num1++
	fmt.Println(num1)
	//++num1 // 編譯報錯, Go語言中++只能后置,不能前置
	//fmt.Println(num1)
	//var num2 int = num1++ // 編譯報錯, num1++是陳述句不是運算式, 所以必須獨占一行
	//fmt.Println(num2)
}
  • Go語言中字串支持利用+號進行拼接
package main
import "fmt"
func main() {
	str := "abc" + "def"
	//fmt.Println(str)
}

關系算符

  • 關系算符和C語言一樣
運算子描述實體
==檢查兩個值是否相等,如果相等回傳 True 否則回傳 False,A == B
!=檢查兩個值是否不相等,如果不相等回傳 True 否則回傳 False,A != B
>檢查左邊值是否大于右邊值,如果是回傳 True 否則回傳 False,A > B
<檢查左邊值是否小于右邊值,如果是回傳 True 否則回傳 False,A < B
>=檢查左邊值是否大于等于右邊值,如果是回傳 True 否則回傳 False,A >= B
<=檢查左邊值是否小于等于右邊值,如果是回傳 True 否則回傳 False,A <= B
  • 注意點:
    • 和C語言不通的是, Go語言中關系運算子只能回傳true和false

邏輯運算子

  • 邏輯運算子和C語言一樣
運算子描述實體
&&如果兩邊的運算元都是 True,則條件 True,否則為 False,A && B
\|\|如果兩邊的運算元有一個 True,則條件 True,否則為 False,A || B
!如果條件為 True,則邏輯 NOT 條件 False,否則為 True,!A
  • 注意點:
    • 和C語言不通的是, Go語言中關系運算子只能回傳true和false
    • 邏輯非只能用于true和false

位運算子

  • 位運算子和C語言幾乎一樣
運算子描述實體
&參與運算的兩數各對應的二進位相與, 對應位只要都是1結果就為1A & B
\|參與運算的兩數各對應的二進位相或,對應位只要其中一個是1結果就為1A | B
^參與運算的兩數各對應的二進位相異或,對應位只要不同結果就是1A ^ B
<<左移運算子,左移n位就是乘以2的n次方A << 2
>>右移運算子,右移n位就是除以2的n次方B >> 2
&^邏輯清零運算子, B對應位是1,A對應位清零,B對應位是0, A對應位保留原樣A &^ B
  • 新增一個&^運算子
int main(){
	/*
	  0110      a
	&^1011      b 如果b位位1,那么結果為0, 否則結果為a位對應的值
	----------
	  0100
	*/
	a1 := 6
	b1 := 11
	res1 := a1 &^ b1
	fmt.Println("res1 = ", res1) // 4

	/*
	  1011      a
	&^1101      b 如果b位位1,那么結果為0, 否則結果為a位對應的值
	----------
	  0010
	*/
	a2 := 11
	b2 := 13
	res2 := a2 &^ b2
	fmt.Println("res2 = ", res2) // 2
}

賦值運算子

  • 賦值運算子和C語言幾乎一樣
    • 新增一個&^=運算子
運算子描述實體
=將右邊賦值給左邊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
&^=位邏輯清零賦值C &^= 2 等于 C = C &^ 2

其它運算子

運算子描述實體
&回傳變數存盤地址&a; 將給出變數的實際地址
*訪問指標指向記憶體*p; 訪問指標p指向記憶體
package main
import "fmt"
int main(){
	var num int = 666
	var p *int = &num
	fmt.Println(num)
	fmt.Println(*p)
	num = 777
	fmt.Println(num)
	*p = 999
	fmt.Println(num)
}
  • 注意點
    • 指標型別只支持相等運算, 不能做加減運算
#include <stdio.h>
int main()
{
    int ages[3] = {19, 23, 22};
    int *arrayP = &ages[0];
    printf("ages[0] = %i\n", *(arrayP + 0)); // *(arrayP + 0) == *arrayP
    printf("ages[1] = %i\n", *(arrayP + 1));
    printf("ages[2] = %i\n", *(arrayP + 2));
    return 0;
}
package main
import "fmt"
int main(){
	var ages [3]int = [3]int{19, 23, 22}
	var p *int = &ages[0]
	//fmt.Println(&ages[0])
	//fmt.Println(*p) // 19
	fmt.Println(*(p + 0)) // 編譯報錯
}

運算子優先級

  • 和C語言一樣, 只需記住()優先級最高即可

Go語言流程控制基本概念

  • Go語言流程控制和C語言一樣, 也有三大流程控制結構
    • 順序結構(默認結構)
    • 選擇結構(if / switch)
    • 回圈結構(for)

選擇結構if

  • 和C語言不同的的是
    • 條件運算式的值必須是布爾型別(Go語言中沒有非零即真的概念)
    • 條件運算式前面可以添加初始化陳述句
    • 不需要撰寫圓括號
    • 左大括號必須和條件陳述句在同一行
  • 第一種格式:
    • 條件運算式結果為true,那么執行if后面{}中代碼
if 初始化陳述句; 條件運算式{
    陳述句塊;
}
package main
import "fmt"
func main() {
	// 如果后續需要用到age變數, 可以將變數放到if外面
	age := 18
	if age >= 18{
		fmt.Println("成年人")
	}
}
package main
import "fmt"
func main() {
	// 如果后續不需要用到age變數, 可以將變數放到條件運算式前面
	if age := 18; age >= 18{
		fmt.Println("成年人")
	}
}

  • 第二種格式:
    • 條件運算式結果為true,那么執行if后面{}中代碼
    • 否則執行else后面{}中代碼
if 初始化陳述句; 條件運算式{
    陳述句塊;
}else{
    陳述句塊;
}
package main
import "fmt"
func main() {
	if age := 18;age >= 18 {
		fmt.Println("成年人")
	}else{
		fmt.Println("未成年人")
	}
}
  • 第三種格式:
    • if后面條件運算式結果為true,那么執行if后面{}中代碼
    • 否則判斷else if條件運算式是否為true,為true執行else if后面{}中代碼
    • 否則依次判斷后續else if條件運算式是否為true,哪個為true就執行哪個else if后面{}中代碼
    • 都不滿足則執行else后面{}中代碼
if 初始化陳述句; 條件運算式{
    陳述句塊;
}else if 條件運算式{
    陳述句塊;
}
... ...
else{
    陳述句塊;
}
package main
import "fmt"
func main() {
	if age := 18;age > 55{
		fmt.Println("老年人")
	}else if age >= 40{
		fmt.Println("中年人")
	}else if age >= 18{
		fmt.Println("成年人")
	}else{
		fmt.Println("未成年人")
	}
}
  • 值得一提的是Go語言中沒有C語言中的三目運算子, 所以C語言中三目能干的在Go語言中都只能通過if else的形式來完成

選擇結構switch

  • 和C語言不同的的是
    • 和if一樣,運算式前面可以添加初始化陳述句
    • 和if一樣,不需要撰寫圓括號
    • 和if一樣,左大括號必須和運算式在同一行
    • case運算式的值不一定要是常量, 甚至可以不用傳遞
    • 一個case后面可以有多個運算式, 滿足其中一個就算匹配
    • case后面不需要添加break
    • 可以在case陳述句塊最后添加fallthrough,實作case穿透
    • case后面定義變數不需要添加{}明確范圍
  • 格式
switch 初始化陳述句; 運算式{
  case 運算式1, 運算式2:
        陳述句塊;
  case 運算式1, 運算式2:
        陳述句塊;
  default:
        陳述句塊;
}
package main
import "fmt"
func main() {
	switch num := 3;num {
		case 1:
			fmt.Println("星期一")
		case 2:
			fmt.Println("星期二")
		case 3:
			fmt.Println("星期三")
		case 4:
			fmt.Println("星期四")
		case 5:
			fmt.Println("星期五")
		case 6:
			fmt.Println("星期六")
		case 7:
			fmt.Println("星期日")
		default:
			fmt.Println("Other...")
	}
}
package main
import "fmt"
func main() {
	switch num := 3;num {
	case 1,2,3,4,5:
		fmt.Println("作業日")
	case 6,7:
		fmt.Println("非作業日")
	default:
		fmt.Println("Other...")
	}
}
  • 注意點:
    • case后面不用撰寫break, 不會出現case穿透問題
    • 如果想讓case穿透,必須在case陳述句塊最后添加fallthrough關鍵
package main
import "fmt"
func main() {
	switch num := 3;num {
	case 1:
		fallthrough
	case 2:
		fallthrough
	case 3:
		fallthrough
	case 4:
		fallthrough
	case 5:
		fmt.Println("作業日")
	case 6:
		fallthrough
	case 7:
		fmt.Println("非作業日")
	default:
		fmt.Println("Other...")
	}
}
  • case后面不僅僅可以放常量,還可以放變數和運算式
package main
import "fmt"
func main() {
	value := 2
	switch num:=1; num {
	case value: // 變數
		fmt.Println("num等于value")
	default:
		fmt.Println("num不等于value")
	}
}
package main
import "fmt"
func main() {
	value := 2
	switch num:=1; num {
	case getValue(): // 函式
		fmt.Println("num等于value")
	default:
		fmt.Println("num不等于value")
	}
}
func getValue() int {
	return 1
}
package main
import "fmt"
func main() {
	switch num:=18; {
		case num >=0 && num <=10: // 運算式
			fmt.Println("num是一個0~10之間的數")
		case num >10 && num <=20:
			fmt.Println("num是一個11~20之間的數")
		default:
			fmt.Println("num是一個大于20的數")
	}
}
  • case后面定義變數不用添加{}明確作用于范圍
package main
import "fmt"
func main() {
    switch num := 1;num {
	  case 1:
		value := 10 // 不會報錯
		fmt.Println(value)
	  default:
		fmt.Println("Other...")
	}
}
  • 其它特性和C語言一樣

回圈結構for

  • 和C語言不同的的是
    • 和if一樣,條件運算式的值必須是布爾型別
    • 和if一樣,不需要撰寫圓括號
    • 和if一樣,左大括號必須和運算式在同一行
  • 格式:
for 初始化運算式;回圈條件運算式;回圈后的操作運算式 {
    回圈體陳述句;
}
package main
import "fmt"
func main() {
	for i:=0; i<10; i++{
		fmt.Println(i)
	}
}
  • Go語言中沒有while/dowhile回圈, 所以可以通過如下格式實作C語言中while回圈用法
package main
import "fmt"
func main() {
	i:=0
	for i<10 {
		fmt.Println(i)
		i++
	}
}
  • 最簡單死回圈
package main
import "fmt"
func main() {
	for{
		fmt.Println("根本停不下來")
	}
}
  • 除了實作基本的回圈結構以外,Go語言還實作了一種高級for回圈for...range回圈
    • for...range回圈可以快速完成對字串、陣列、slice、map、channel遍歷
  • 格式
for 索引,:= range 被遍歷資料{
}
package main
import "fmt"
func main() {
	// 1.定義一個陣列
	arr := [3]int{1, 3, 5}
	// 2.快速遍歷陣列
	// i用于保存當前遍歷到陣列的索引
	// v用于保存當前遍歷到陣列的值
	for i, v := range arr{
		fmt.Println(i, v)
	}
}

四大跳轉

  • 和C語言一樣,Go語言中也有四大跳轉陳述句, 分別是return、break、continue、goto
  • break陳述句
    • Go語言中的break陳述句可以用于,立即跳出switch、for和select
    • 但不同的是Go語言中的break陳述句可以指定標簽
package main
import "fmt"
func main() {
	for i:=0; i<10; i++{
		if(i == 5){
			break // 跳出所在回圈
		}
		fmt.Println(i)
	}
}
  • 利用break跳轉到指定標簽
    • 標簽必須在使用之前定義
    • 標簽后面只能跟switch和回圈陳述句, 不能插入其它陳述句
    • 跳轉到標簽之后switch和回圈不會再次被執行
package main
import "fmt"
func main() {
outer:
	switch num:=2; num {
	case 1:
		fmt.Println(1)
	case 2:
		fmt.Println(2)
		break outer
	default:
		fmt.Println("other")
	}
	fmt.Println("come here")
}
package main
import "fmt"
func main() {
outer:
	for i:=0; i<5; i++{
		for j:=0; j<10; j++ {
			if (j == 5) {
				break outer// 跳出到指定標簽
			}
			fmt.Println(j)
		}
	}
	fmt.Println("come here")
}
  • continue陳述句
    • Go語言中的continue陳述句可以用于,立即進入下一次回圈
    • 但不同的是Go語言中的continue陳述句可以指定標簽
package main
import "fmt"
func main() {
	for i:=0; i<5; i++{
		if (i == 2) {
			continue
		}
		fmt.Println(i)
	}
}
  • 利用continue 跳轉到指定標簽
    • 標簽必須在使用之前定義
    • 標簽后面只能跟回圈陳述句, 不能插入其它陳述句
    • 對于單層回圈和直接撰寫continue一樣
    • 對于多層回圈,相當于跳到最外層回圈繼續判斷條件執行
package main
import "fmt"
func main() {
outer:
	for i:=0; i<5; i++{
		fmt.Println("i =", i) // 0 1 2 3 4
		for j:=0; j<10; j++ {
			if (j == 5) {
				continue outer// 跳出到指定標簽
			}
		}
	}
	fmt.Println("come here")
}
  • goto陳述句
    • Go語言中的goto和C語言中用法一樣, 用于在同一個函式中瞎跳
package main
import "fmt"
func main() {
	num := 1
outer:
	if(num <= 10){
		fmt.Println(num)
		num++
		goto outer // 死回圈
	}
	fmt.Println("come here")
}
package main
import "fmt"
func main() {
	num := 1
	if(num <= 10){
		fmt.Println(num)
		num++
		goto outer // 死回圈
	}
outer:
	fmt.Println("come here")
}
  • Go語言中的return陳述句和C語言一模一樣,都是用于結束函式,將結果回傳給呼叫者

函式

  • Go語言和C語言一樣也有函式的概念, Go語言中函式除了定義格式和不用宣告以外,其它方面幾乎和C語言一模一樣
  • 格式:
func 函式名稱(形參串列)(回傳值串列){
    函式體;
}
  • 無引數無回傳值函式
func say()  {
	fmt.Println("Hello World!!!")
}
  • 有引數無回傳值函式
func say(name string)  {
	fmt.Println("Hello ", name)
}
  • 無引數有回傳值函式
func sum() int { // 只有一個回傳值時,回傳值串列的()可以省略
	return 1 + 1
}
  • 有引數有回傳值函式
func sum(a int, b int) int {
	return a + b
}

和C語言函式差異

  • 和C語言不同的是,Go語言中可以給函式的回傳值指定名稱
// 給回傳值指定了一個名稱叫做res, return時會自動將函式體內部res作為回傳值
// 其實本質就是提前定義了一個區域變數res, 在函式體中使用的res就是這個區域變數,回傳的也是這個區域變數
func sum() (res int) { 
	res = 1 + 1
	return
}
  • 和C語言不同的是,Go語言中的函式允許有多個回傳值函式
func calculate(a int, b int) (sum int, sub int) {
	sum = a + b
	sub = a - b
	return
}
  • 相鄰同型別形參OR回傳值型別可以合并, 可以將資料型別寫到最后一個同型別形參OR回傳值后面
// a, b都是int型別, 所以只需要在b后面添加int即可
func calculate(a, b int) (sum, sub int) {
	sum = a + b
	sub = a - b
	return
}
  • 和C語言不同的是Go語言中的函式不需要先宣告在使用
package main
import "fmt"
func main() {
	say();
}
func say()  { // 在后面定義也可以在前面使用
	fmt.Println("Hello World!!!")
}

值傳遞和參考傳遞

  • Go語言中值型別有: int系列、float系列、bool、string、陣列、結構體
    • 值型別通常在堆疊中分配存盤空間
    • 值型別作為函式引數傳遞, 是拷貝傳遞
    • 在函式體內修改值型別引數, 不會影響到函式外的值
package main
import "fmt"
func main() {
	num := 10
	change(num)
	fmt.Println(num) // 10
}
func change(num int)  {
	num = 998
}
package main
import "fmt"
func main() {
	arr := [3]int{1, 3, 5}
	change(arr)
	fmt.Println(arr) // 1, 3, 5
}
func change(arr [3]int)  {
	arr[1] = 8
}
package main
import "fmt"
type Person struct {
	name string
	age int
}
func main() {
	p := Person{"lnj", 33}
	change(p)
	fmt.Println(p.name) // lnj
}
func change(p Person)  {
	p.name = "zs"
}
  • Go語言中參考型別有: 指標、slice、map、channel
    • 參考型別通常在堆中分配存盤空間
    • 參考型別作為函式引數傳遞,是參考傳遞
    • 在函式體內修改參考型別引數,會影響到函式外的值
package main
import "fmt"
func main() {
	num := 10
	change(&num)
	fmt.Println(num) // 998
}
func change(num *int)  {
	*num = 998
}
package main
import "fmt"
func main() {
	arr := []int{1, 3, 5}
	change(arr)
	fmt.Println(arr) // 1, 8, 5
}
func change(arr []int)  {
	arr[1] = 8
}
package main
import "fmt"
func main() {
	mp := map[string]string{"name":"lnj", "age":"33"}
	change(mp)
	fmt.Println(mp["name"]) // zs
}
func change(mp map[string]string)  {
	mp["name"] = "zs"
}

匿名函式

  • 匿名函式也是函式的一種, 它的格式和普通函式一模一樣,只不過沒有名字而已
    • 普通函式的函式名稱是固定的, 匿名函式的函式名稱是系統隨機的
  • 匿名函式可以定義在函式外(全域匿名函式),也可以定義在函式內(區域匿名函式), Go語言中的普通函式不能嵌套定義, 但是可以通過匿名函式來實作函式的嵌套定義
    • 全域匿名函式
    package main
    import "fmt"
    // 方式一
    var a = func()  {
    	fmt.Println("hello world1")
    }
    // 方式二
    var (
    	  b  = func()  {
    	    fmt.Println("hello world2")
    	  }
    )
    func main() {
    	  a()
        b()
    }
    
  • 一般情況下我們很少使用全域匿名函式, 大多數情況都是使用區域匿名函式, 匿名函式可以直接呼叫、保存到變數、作為引數或者回傳值
  • 直接呼叫
package main
import "fmt"
func main() {
	func(s string){
		fmt.Println(s)
	}("hello lnj")
}
  • 保存到變數
package main
import "fmt"
func main() {
	a := func(s string) {
		fmt.Println(s)
	}
	a("hello lnj")
}
  • 作為引數
package main
import "fmt"
func main() {
	test(func(s string) {
		fmt.Println(s)
	})
}
func test(f func(s string))  {
	f("hello lnj")
}
  • 作為回傳值
package main
import "fmt"
func main() {
	res := test()
	res(10, 20)
}
func test() func(int, int) {
	return func(a int, b int) {
		fmt.Println(a + b)
	}
}
  • 匿名函式應用場景
    • 當某個函式只需要被呼叫一次時, 可以使用匿名函式
    • 需要執行一些不確定的操作時,可以使用匿名函式
package main
import "fmt"
func main() {
	// 專案經理的一天
	work(func() {
		fmt.Println("組織部門開會")
		fmt.Println("給部門員工分配今天作業任務")
		fmt.Println("檢查部門員工昨天提交的代碼")
		fmt.Println("... ...")
	})
	// 程式員的一天
	work(func() {
		fmt.Println("參加部門會議")
		fmt.Println("修改測驗提交的BUG")
		fmt.Println("完成老大今天安排的任務")
		fmt.Println("... ...")
	})
}
// 假設我們需要撰寫一個函式,用于描述一個人每天上班都需要干嘛
// 職場中的人每天上班前,上班后要做的事幾乎都是相同的, 但是每天上班程序中要做的事確實不確定的
// 所以此時我們可以使用匿名函式來解決, 讓上班的人自己覺得自己每天上班需要干什么
func work(custom func())  {
	// 上班前
	fmt.Println("起床")
	fmt.Println("刷牙")
	fmt.Println("洗臉")
	fmt.Println("出門")
	fmt.Println("上班打卡")
	fmt.Println("開電腦")

	// 上班中
	custom()

	// 上班后
	fmt.Println("關電腦")
	fmt.Println("下班打卡")
	fmt.Println("出門")
	fmt.Println("到家")
	fmt.Println("吃飯")
	fmt.Println("睡覺")

}
  • 為了提升代碼的可讀性,我們還可以將這個大函式拆解為獨立的匿名函式
func work(custom func())  {
	// 這種寫法的好處是代碼層次清晰,并且如果有一些變數
	// 只需要在上班前或上班后使用,還可以將這些變數隔離,不對外界造成污染
	// 上班前
	func(){
		fmt.Println("起床")
		fmt.Println("刷牙")
		fmt.Println("洗臉")
		fmt.Println("出門")
		fmt.Println("上班打卡")
		fmt.Println("開電腦")
	}()

	// 上班中
	custom()

	// 上班后
	func(){
		fmt.Println("關電腦")
		fmt.Println("下班打卡")
		fmt.Println("出門")
		fmt.Println("到家")
		fmt.Println("吃飯")
		fmt.Println("睡覺")
	}()

}
func work(custom func())  {
	// 前提條件是這個函式只在work函式中使用, 兩者有較強的關聯性, 否則建議定義為普通函式
	pre := func(){
		fmt.Println("起床")
		fmt.Println("刷牙")
		fmt.Println("洗臉")
		fmt.Println("出門")
		fmt.Println("上班打卡")
		fmt.Println("開電腦")
	}
	latter := func(){
		fmt.Println("關電腦")
		fmt.Println("下班打卡")
		fmt.Println("出門")
		fmt.Println("到家")
		fmt.Println("吃飯")
		fmt.Println("睡覺")
	}
	
	// 上班前
	pre()
	// 上班中
	custom()
	// 上班后
	latter()
}

閉包

  • 閉包是一個特殊的匿名函式, 它是匿名函式和相關參考環境組成的一個整體
    • 也就是說只要匿名函式中用到了外界的變數, 那么這個匿名函式就是一個閉包
    package main
    import "fmt"
    func main() {
        num := 10
        a := func() {
      	  num++ // 在閉包中用到了main函式中的num, 所以這個匿名函式就是一個閉包
      	  fmt.Println(num) // 11
        }
        a()
    }
    
    • 閉包中使用的變數和外界的變數是同一個變數, 所以可以閉包中可以修改外界變數
    package main
    import "fmt"
    func main() {
        num := 10
        a := func() {
      	  num = 6 // 在閉包中用到了main函式中的num, 所以這個匿名函式就是一個閉包
      	  fmt.Println(num) // 6
        }
        fmt.Println("執行閉包前", num) // 10
        a()
        fmt.Println("執行閉包后", num) // 6
    }
    
    • 只要閉包還在使用外界的變數, 那么外界的變數就會一直存在
    package main
    import "fmt"
    func main() {
      res := addUpper() // 執行addUpper函式,得到一個閉包
      fmt.Println(res()) // 2 
      fmt.Println(res()) // 3
      fmt.Println(res()) // 4
      fmt.Println(res()) // 5
    }
    func addUpper() func() int {
        x := 1
        return func() int {
      	  x++ // 匿名函式中用到了addUpper中的x,所以這是一個閉包
      	  return x
        }
    }
    

延遲呼叫

  • Go語言中沒有提供其它面向物件語言的解構式, 但是Go語言提供了defer陳述句用于實作其它面向物件語言解構式的功能
  • defer陳述句常用于釋放資源解除鎖定以及錯誤處理
    • 例如C語言中我們申請了一塊記憶體空間,那么不使用時我們就必須釋放這塊存盤空間
    • 例如C語言中我們打開了一個檔案,那么我們不使用時就要關閉這個檔案
    • 例如C語言中我們打開了一個資料庫, 那么我們不使用時就要關閉這個資料庫
    • 這一類的操作在Go語言中都可以通過defer陳述句來完成
  • 無論你在什么地方注冊defer陳述句,它都會在所屬函式執行完畢之后才會執行, 并且如果注冊了多個defer陳述句,那么它們會按照后進先出的原則執行
    • 正是因為defer陳述句的這種特性, 所以在Go語言中關閉資源不用像C語言那樣用完了再關閉, 我們完全可以打開的同時就關閉, 因為無論如何defer陳述句都會在所屬函式執行完畢之后才會執行
  package main
  import "fmt"
  func main() {
	defer fmt.Println("我是第一個被注冊的") // 3
	fmt.Println("main函式中呼叫的Println") // 1
	defer fmt.Println("我是第二個被注冊的") // 2
  }

init函式

  • golang里面有兩個保留的函式:
    • init函式(能夠應用于所有的package)
    • main函式(只能應用于package main)
    • 這兩個函式在定義時不能有任何的引數和回傳值
  • go程式會自動呼叫init()和main(),所以你不能在任何地方呼叫這兩個函式
  • package main必須包含一個main函式, 但是每個package中的init函式都是可選的
  • 一個package里面可以寫任意多個init函式,但這無論是對于可讀性還是以后的可維護性來說,我們都強烈建議用戶在一個package中每個檔案只寫一個init函式
  • 單個包中代碼執行順序如下
    • main包-->常量-->全域變數-->init函式-->main函式-->Exit
package main
import  "fmt"
const constValue  = 998 // 1
var gloalVarValue int = abc() // 2
func init() { // 3
	fmt.Println("執行main包中main.go中init函式")
}
func main() { // 4
	fmt.Println("執行main包中main.go中main函式")
}
func abc() int {
	fmt.Println("執行main包中全域變數初始化")
	return 998
}
  • 多個包之間代碼執行順序如下

  • init函式的作用

    • init函式用于處理當前檔案的初始化操作, 在使用某個檔案時的一些準備作業應該放到這里

陣列

  • 和C語言一樣,Go語言中也有陣列的概念, Go語言中的陣列也是用于保存一組相同型別的資料
  • 和C語言一樣,Go語言中的陣列也分為一維陣列多維陣列

一維陣列

  • 格式:var arr [元素個數]資料型別
    • 和C語言中陣列不同, Go語言中陣列定義之后就有默認的初始值
    • 默認初始值就是保存資料型別的默認值(零值)
    package main
    import "fmt"
    func main() {
        // 1.定義一個陣列
        var arr [3]int
        // 2.列印陣列
        fmt.Println(arr) //[0 0 0]
    
        // 1.定義一個陣列
        var arr [3]bool
        // 2.列印陣列
        fmt.Println(arr) //[false false  false]
    }
    
    • 和C語言一樣,Go語言中的陣列也提供了好幾種初始化方式
    package main
    import "fmt"
    func main() {
      	// 1.定義的同時完全初始化
      	var arr1 [3]int = [3]int{1, 3, 5}
      	// 2.列印陣列
      	fmt.Println(arr1) // [1 3 5]
    
      	// 1.定義的同時部分初始化
      	var arr4 [3]int = [3]int{8, 9}
      	// 2.列印陣列
      	fmt.Println(arr4) // [8 9 0]
    
      	// 1.定義的同時指定元素初始化
      	var arr5 [3]int = [3]int{0:8, 2:9}
      	// 2.列印陣列
      	fmt.Println(arr5) // [8 0 9]
    
      	// 1.先定義再逐個初始化
      	var arr3 [3]int
      	arr3[0] = 1
      	arr3[1] = 2
      	arr3[2] = 3
      	// 2.列印陣列
      	fmt.Println(arr3) // [1 2 3]
    }
    
    • 和C語言中的陣列不同,Go語言中陣列除了可以定義的同時初始化以外,還可以先定義再一次性初始化
    package main
    import "fmt"
    func main() {
      	// 1.先定義再一次性初始化
      	var arr2 [3]int
      	arr2 = [3]int{2, 4, 6}
      	// 2.列印陣列
      	fmt.Println(arr2) // [2 4 6]
    }
    
    • 和C語言一樣,Go語言中如果定義陣列的同時初始化,那么元素個數可以省略,但是必須使用...來替代
      • …會根據初始化元素個數自動確定陣列長度
    package main
    import "fmt"
    func main() {
      // 1.定義的同時完全初始化
      var arr1  = [...]int{1, 3, 5}
      // 2.列印陣列
      fmt.Println(arr1) // [1 3 5]
    
      // 1.定義的同時指定元素初始化
      var arr2  = [...]int{6:5}
      // 2.列印陣列
      fmt.Println(arr2) // [0 0 0 0 0 0 5]
    }
    
  • Go語言中陣列的訪問和使用方式和C語言一樣都是通過陣列名稱[索引]的方式
package main
import "fmt"
func main() {
	arr := [...]int{1, 3, 5}
	// 使用陣列, 往陣列中存放資料
	arr[1] = 666
	// 訪問陣列, 從陣列中獲取資料
	fmt.Println(arr[0])
	fmt.Println(arr[1])
	fmt.Println(arr[2])
}
  • 遍歷陣列
    • Go語言中提供了兩種遍歷陣列的方式, 一種是通過傳統for回圈遍歷, 一種是通過for…range回圈遍歷
package main
import "fmt"
func main() {
	arr := [...]int{1, 3, 5}
	// 傳統for回圈遍歷
	for i:=0; i<len(arr); i++{
		fmt.Println(i, arr[i])
	}
	// for...range回圈遍歷
	for i, v := range arr{
		fmt.Println(i, v)
	}
}
  • 陣列注意點
    • Go語言中陣列長度也是資料型別的一部分
    package main
    import "fmt"
    func main() {
        var arr1 [3]int
        var arr2 [3]int
        //var arr3 [2]int
        fmt.Println(arr1 == arr2) // true
        //fmt.Println(arr1 == arr3) // 編譯報錯, 不是相同型別不能比較
    }
    
    • 如果元素型別支持==、!=操作時,那么陣列也支持此操作
    package main
    import "fmt"
    func main() {
        var arr1 [3]int = [3]int{1, 3, 5}
        var arr2 [3]int = [3]int{1, 3, 5}
        var arr3 [3]int = [3]int{2, 4, 6}
        // 首先會判斷`資料型別`是否相同,如果相同會依次取出陣列中`對應索引的元素`進行比較, 
        // 如果所有元素都相同回傳true,否則回傳false
        fmt.Println(arr1 == arr2) // true
        fmt.Println(arr1 == arr3) // false
     }
    
    • Go語言中的陣列是值型別, 賦值和傳參時會復制整個陣列
    package main
    import "fmt"
    func main() {
      var arr1 [3]int = [3]int{1, 3, 5}
      var arr2 [3]int = arr1
      arr2[0] = 666
      fmt.Println(arr1) // [1 3 5]
      fmt.Println(arr2) // [666 3 5]
     }
    

二維陣列

  • 用法和C語言陣列一樣, 只是創建的格式不同
  • 格式: [行數][列數]型別
  package main
  import "fmt"
  func main() {
	// 創建一個兩行三列陣列
	arr := [2][3]int{
		{1, 2, 3},
		{4, 5, 6}, //注意: 陣列換行需要以逗號結尾
	}
	fmt.Println(arr)// [[1 2 3] [4 5 6]]
   }
  • 創建多維陣列時只允許第一維度使用…
  • 格式: [...][列數]型別
package main
  import "fmt"
  func main() {
	// 創建一個兩行三列陣列
	arr := [...][3]int{
		{1, 2, 3},
		{4, 5, 6},
	}
	fmt.Println(arr)// [[1 2 3] [4 5 6]]
   }

切片

  • 無論是C語言中的陣列還是Go語言中的陣列,陣列的長度一旦確定就不能改變, 但在實際開發中我們可能事先不能確定陣列的長度, 為了解決這類問題Go語言中推出了一種新的資料型別切片
  • 切片可以簡單的理解為長度可以變化的陣列, 但是Go語言中的切片本質上是一個結構體
    • 切片原始碼
    type slice struct{
      array unsafe.Pointer // 指向底層陣列指標
      len int // 切片長度(保存了多少個元素)
      cap int // 切片容量(可以保存多少個元素)
    }
    

  • 切片創建的三種方式
  • 方式一: 通過陣列創建切片array[startIndex:endIndex]
    package main
    import "fmt"
    func main() {
        var arr = [5]int{1, 3, 5, 7, 9}
        // 從陣列0下標開始取,一直取到2下標前面一個索引
        var sce = arr[0:2]
        fmt.Println(sce) // [1 3]
        // 切片len = 結束位置 - 開始位置
        fmt.Println(len(sce)) // 2 - 0 = 2
        fmt.Println(cap(sce)) // 5 - 0 = 5
        // 陣列地址就是陣列首元素的地址
        fmt.Printf("%p\n", &arr) // 0xc04200a330
        fmt.Printf("%p\n", &arr[0]) // 0xc04200a330
        // 切片地址就是陣列中指定的開始元素的地址
        //  arr[0:2]開始地址為0, 所以就是arr[0]的地址
        fmt.Printf("%p\n", sce) // 0xc04200a330
    }
    

image.png

package main
import "fmt"
func main() {
  var arr = [5]int{1, 3, 5, 7, 9}
  // 根據陣列的索引片段創建切片
  var sce = arr[2:4]
  fmt.Println(sce) // [5 7]
  fmt.Println(len(sce)) // 4 - 2 = 2
  fmt.Println(cap(sce)) // 5 - 2 = 3
  fmt.Printf("%p\n", &arr[2]) // 0xc042076070
  fmt.Printf("%p\n", sce) // 0xc042076070
}

  • 指定起始位置時有三種方式可以指定
    • 開始位置和結束位置都指定
    • 只指定開始位置或結束位置
    • 開始位置和結束位置都不指定
  package main
  import "fmt"
  func main() {
	var arr = [5]int{1, 3, 5, 7, 9}
	// 同時指定開始位置和結束位置
	var sce1 = arr[0:2]
	fmt.Println(sce1) // [1 3]

	// 只指定結束位置
	var sce3 = arr[:2]
	fmt.Println(sce3) // [1 3]

	// 只指定開始位置
	var sce2 = arr[0:]
	fmt.Println(sce2) // [1 3 5 7 9]

	// 都不指定
	var sce4 = arr[:]
	fmt.Println(sce4) // [1 3 5 7 9]
  }
  • 方式二: 通過make函式創建make(型別, 長度, 容量)
    • 內部會先創建一個陣列, 然后讓切片指向陣列
    • 如果沒有指定容量,那么容量和長度一樣
 package main
 import "fmt"
 func main() {
	// 第一個引數: 指定切片資料型別
	// 第二個引數: 指定切片的長度
	// 第三個引數: 指定切片的容量
	var sce = make([]int, 3, 5)
	fmt.Println(sce) // [0 0 0]
	fmt.Println(len(sce)) // 3
	fmt.Println(cap(sce)) // 5
	/*
	內部實作原理
	var arr = [5]int{0, 0, 0}
	var sce = arr[0:3]
	*/
 }
  • 方式三:通過Go提供的語法糖快速創建
    • 和創建陣列一模一樣, 但是不能指定長度
    • 通過該方式創建時, 切片的長度和容量相等
 package main
 import "fmt"
 func main() {
	var sce = []int{1, 3, 5}
	fmt.Println(sce) // [1 3 5]
	fmt.Println(len(sce)) // 3
	fmt.Println(cap(sce)) // 3
 }

  • 切片的使用
    • 切片的基本使用方式和陣列一樣, 可以通過切片名稱[索引]方式操作切片
     package main
     import "fmt"
     func main() {
        var sce = []int{1, 3, 5}
        // 使用切片, 往切片中存放資料
        sce[1] = 666
        // 訪問切片, 從切片中獲取資料
        fmt.Println(sce) // [1 666 5]
     }
    
    • 和陣列一樣, 如果通過切片名稱[索引]方式操作切片, 不能越界
      package main
      import "fmt"
      func main() {
         var sce = []int{1, 3, 5}
         // 編譯報錯, 越界
         sce[3] = 666
      }
    
    • 如果希望切片自動擴容,那么添加資料時必須使用append方法
      • append函式會在切片末尾添加一個元素, 并回傳一個追加資料之后的切片
      • 利用append函式追加資料時,如果追加之后沒有超出切片的容量,那么回傳原來的切片, 如果追加之后超出了切片的容量,那么回傳一個新的切片
      • append函式每次給切片擴容都會按照原有切片容量*2的方式擴容
      package main
      import "fmt"
      func main() {
       var sce = []int{1, 3, 5}
       fmt.Println("追加資料前:", sce) // [1 3 5]
       fmt.Println("追加資料前:", len(sce)) // 3
       fmt.Println("追加資料前:", cap(sce)) // 3
       fmt.Printf("追加資料前: %p\n", sce) // 0xc0420600a0
       // 第一個引數: 需要把資料追加到哪個切片中
       // 第二個引數: 需要追加的資料, 可以是一個或多個
       sce = append(sce, 666)
       fmt.Println("追加資料后:", sce) // [1 3 5 666]
       fmt.Println("追加資料后:", len(sce)) // 4
       fmt.Println("追加資料后:", cap(sce)) // 6
       fmt.Printf("追加資料前: %p\n", sce) // 0xc042076b60
      }
    
    • 除了append函式外,Go語言還提供了一個copy函式, 用于兩個切片之間資料的快速拷貝
      • 格式: copy(目標切片, 源切片), 會將源切片中資料拷貝到目標切片中
       package main
       import "fmt"
       func main() {
      var sce1 = []int{1, 3, 5}
      var sce2 = make([]int, 5)
      fmt.Printf("賦值前:%p\n", sce1) // 0xc0420600a0
      fmt.Printf("賦值前:%p\n", sce2) // 0xc042076060
      // 將sce2的指向修改為sce1, 此時sce1和sce2底層指向同一個陣列
      sce2 = sce1
      fmt.Printf("賦值后:%p\n", sce1) // 0xc0420600a0
      fmt.Printf("賦值后:%p\n", sce2) // 0xc0420600a0
      //copy(sce2, sce1)
      fmt.Println(sce1) // [1 3 5]
      fmt.Println(sce2) // [1 3 5]
      sce2[1] = 666
      fmt.Println(sce1) // [1 666 5]
      fmt.Println(sce2) // [1 666 5]
       }
    
       package main
       import "fmt"
       func main() {
      var sce1 = []int{1, 3, 5}
      var sce2 = make([]int, 5)
      fmt.Printf("賦值前:%p\n", sce1) // 0xc0420600a0
      fmt.Printf("賦值前:%p\n", sce2) // 0xc042076060
      // 將sce1中的資料拷貝到sce2中,, 此時sce1和sce2底層指向不同陣列
      copy(sce2, sce1)
      fmt.Printf("賦值后:%p\n", sce1) // 0xc0420600a0
      fmt.Printf("賦值后:%p\n", sce2) // 0xc042076060
      //copy(sce2, sce1)
      fmt.Println(sce1) // [1 3 5]
      fmt.Println(sce2) // [1 3 5 0 0]
      sce2[1] = 666
      fmt.Println(sce1) // [1 3 5]
      fmt.Println(sce2) // [1 666 5 0 0]
       }
    
    • copy函式在拷貝資料時永遠以小容量為準
       package main
       import "fmt"
       func main() {
      // 容量為3
      var sce1 = []int{1, 3, 5}
      // 容量為5
      var sce2 = make([]int, 5)
      fmt.Println("拷貝前:", sce2) // [0 0 0 0 0]
      // sce2容量足夠, 會將sce1所有內容拷貝到sce2
      copy(sce2, sce1)
      fmt.Println("拷貝后:", sce2) // [1 3 5 0 0]
       }
    
       package main
       import "fmt"
       func main() {
      // 容量為3
      var sce1 = []int{1, 3, 5}
      // 容量為2
      var sce2 = make([]int, 2)
      fmt.Println("拷貝前:", sce2) // [0 0]
      // sce2容量不夠, 會將sce1前2個元素拷貝到sce2中
      copy(sce2, sce1)
      fmt.Println("拷貝后:", sce2) // [1 3]
       }
    

  • 切片的注意點
    • 可以通過切片再次生成新的切片, 兩個切片底層指向同一陣列
      package main
      import "fmt"
      func main() {
          arr := [5]int{1, 3, 5, 7, 9}
          sce1 := arr[0:4]
          sce2 := sce1[0:3]
          fmt.Println(sce1) // [1 3 5 7]
          fmt.Println(sce2) // [1 3 5]
          // 由于底層指向同一陣列, 所以修改sce2會影響sce1
          sce2[1] = 666
          fmt.Println(sce1) // [1 666 5 7]
          fmt.Println(sce2) // [1 666 5]
       }
    
    • 和陣列不同, 切片只支持判斷是否為nil, 不支持==、!=判斷
    package main
    import "fmt"
    func main() {
        var arr1 [3]int = [3]int{1, 3, 5}
        var arr2 [3]int = [3]int{1, 3, 5}
        var arr3 [3]int = [3]int{2, 4, 6}
        // 首先會判斷`資料型別`是否相同,如果相同會依次取出陣列中`對應索引的元素`進行比較, 
        // 如果所有元素都相同回傳true,否則回傳false
        fmt.Println(arr1 == arr2) // true
        fmt.Println(arr1 == arr3) // false
    
        sce1 := []int{1, 3, 5}
        sce2 := []int{1, 3, 5}
        //fmt.Println(sce1 == sce2) // 編譯報錯
        fmt.Println(sce1 != nil) // true
        fmt.Println(sce2 == nil) // false
     }
    
    • 只宣告當沒有被創建的切片是不能使用的
    package main
    import "fmt"
    func main() {
      // 陣列宣告后就可以直接使用, 宣告時就會開辟存盤空間
      var arr [3]int
      arr[0] = 2
      arr[1] = 4
      arr[2] = 6
      fmt.Println(arr) // [2 4 6]
    
      // 切片宣告后不能直接使用, 只有通過make或語法糖創建之后才會開辟空間,才能使用
      var sce []int
      sce[0] = 2 // 編譯報錯
      sce[1] = 4
      sce[2] = 6
      fmt.Println(sce)
     }
    
    • 字串的底層是[]byte陣列, 所以字符也支持切片相關操作
    package main
    import "fmt"
    func main() {
      str := "abcdefg"
      // 通過字串生成切片
      sce1 := str[3:]
      fmt.Println(sce1) // defg
    
      sce2 := make([]byte, 10)
      // 第二個引數只能是slice或者是陣列
      // 將字串拷貝到切片中
      copy(sce2, str)
      fmt.Println(sce2) //[97 98 99 100 101 102 103 0 0 0]
     }
    

map(字典、映射)

  • map翻譯過來就是字典或者映射, 可以把map看做是切片的升級版
    • 切片是用來存盤一組相同型別的資料的, map也是用來存盤一組相同型別的資料的
    • 在切片中我們可以通過索引獲取對應的元素, 在map中我們可以通過key獲取對應的元素
    • 切片的索引是系統自動生成的,從0開始遞增. map中的key需要我們自己指定
      • 只要是可以做==、!=判斷的資料型別都可以作為key(數值型別、字串、陣列、指標、結構體、介面)
      • map的key的資料型別不能是:slice、map、function
      • map和切片一樣容量都不是固定的, 當容量不足時底層會自動擴容
  • map格式:var dic map[key資料型別]value資料型別
package main
import "fmt"
func main() {
	var dic map[int]int = map[int]int{0:1, 1:3, 2:5}
	fmt.Println(dic) // map[0:1 1:3 2:5]

	// 獲取map中某個key對應的值
	fmt.Println(dic[0]) // 1
	
	// 修改map中某個key對應的值
	dic[1] = 666
	fmt.Println(dic) // map[0:1 1:666 2:5]
}
package main
import "fmt"
func main() {
	var dict map[string]string = map[string]string{"name":"lnj", "age":"33", "gender":"male"}
	fmt.Println(dict)// map[name:lnj age:33 gender:male]
}

  • 創建map的三種方式
    • 方式一: 通過Go提供的語法糖快速創建
package main
import "fmt"
func main() {
	dict  := map[string]string{"name":"lnj", "age":"33", "gender":"male"}
	fmt.Println(dict)// map[name:lnj age:33 gender:male]
}
  • 方式二:通過make函式創建make(型別, 容量)
package main
import "fmt"
func main() {
	var dict = make(map[string]string, 3)
	dict["name"] = "lnj"
	dict["age"] = "33"
	dict["gender"] = "male"
	fmt.Println(dict)// map[age:33 gender:male name:lnj]
}
  • 方式二:通過make函式創建make(型別)
package main
import "fmt"
func main() {
	var dict = make(map[string]string)
	dict["name"] = "lnj"
	dict["age"] = "33"
	dict["gender"] = "male"
	fmt.Println(dict)// map[age:33 gender:male name:lnj]
}
  • 和切片一樣,沒有被創建的map是不能使用的
package main
import "fmt"
func main() {
	// map宣告后不能直接使用, 只有通過make或語法糖創建之后才會開辟空間,才能使用
	var dict map[string]string
	dict["name"] = "lnj" // 編譯報錯
	dict["age"] = "33"
	dict["gender"] = "male"
	fmt.Println(dict)
}

  • map的增刪改查
    • 增加: 當map中沒有指定的key時就會自動增加
package main
import "fmt"
func main() {
	var dict = make(map[string]string)
	fmt.Println("增加前:", dict) // map[]
	dict["name"] = "lnj"
	fmt.Println("增加后:", dict) // map[name:lnj]
}
  • 修改: 當map中有指定的key時就會自動修改
package main
import "fmt"
func main() {
	var dict = map[string]string{"name":"lnj", "age":"33", "gender":"male"}
	fmt.Println("修改前:", dict) // map[name:lnj age:33 gender:male]
	dict["name"] = "zs"
	fmt.Println("修改后:", dict) // map[age:33 gender:male name:zs]
}
  • 洗掉: 可以通過Go語言內置delete函式洗掉指定元素
package main
import "fmt"
func main() {
	var dict = map[string]string{"name":"lnj", "age":"33", "gender":"male"}
	fmt.Println("洗掉前:", dict) // map[name:lnj age:33 gender:male]
	// 第一個引數: 被操作的字典
	// 第二個引數: 需要洗掉元素對應的key
	delete(dict, "name")
	fmt.Println("洗掉后:", dict) // map[age:33 gender:male]
}
  • 查詢: 通過ok-idiom模式判斷指定鍵值對是否存盤
package main
import "fmt"
func main() {
	var dict = map[string]string{"name":"lnj", "age":"33", "gender":"male"}
	//value, ok := dict["age"]
	//if(ok){
	//	fmt.Println("有age這個key,值為", value)
	//}else{
	//	fmt.Println("沒有age這個key,值為", value)
	//}
	if value, ok := dict["age"]; ok{
		fmt.Println("有age這個key,值為", value)
	}
}
```go
- ***map遍歷***
  + 注意: map和陣列以及切片不同,map中存盤的資料是無序的, 所以多次列印輸出的順序可能不同
```go
	var dict = map[string]string{"name":"lnj", "age":"33", "gender":"male"}
	for key, value := range dict{
		fmt.Println(key, value)
	}

結構體

  • Go語言中的結構體和C語言中結構體一樣, 都是用來保存一組不同型別的資料
  • Go語言中的結構體和C語言中結構體一樣, 都需要先定義結構體型別再利用結構體型別定義結構體變數
  • 定義結構體型別
type 型別名稱 struct{
  屬性名稱 屬性型別
  屬性名稱 屬性型別
  ... ...
}
type Studentstruct {
	name string
	age int
}
  • 創建結構體變數的兩種方式
    • 方式一: 先定義結構體型別, 再定義結構體變數
      • 和C語言中的結構體一樣, 如果結構體型別需要多次使用, 那么建議先定義型別再定義變數
    package main
    import "fmt"
    func main() {
      type Student struct {
      	name string
      	age int
      }	
    
      // 完全初始化
      var stu1= Student{"lnj", 33}
      fmt.Println(stu1)
      // 部分初始化
      // 部分初始化必須通過 屬性名稱: 方式指定要初始化的屬性
      var stu2 = Student{name:"lnj"}
      fmt.Println(stu2)
    }
    
    • 方式二: 定義結構體型別同時定義結構體變數(匿名結構體)
      • 和C語言中的結構體一樣, 如果結構體型別只需要使用一次, 那么建議定義型別同時定義變數
    package main
    import "fmt"
    func main() {
      // 注意: 這里不用寫type和結構體型別名稱
      var stu2 = struct {
      	name string
      	age int
      }{
      	name: "lnj",
      	age: 33,
      }
      fmt.Println(stu2)
    }
    

  • 結構體型別操作
  package main
  import "fmt"
  type Student struct {
	  name string
	  age int
  }
  func main() {
	var stu= Student{"lnj", 33}
	// 獲取結構體中某個屬性對應的值
	fmt.Println(stu.name)

	// 修改結構體中某個屬性對應的值
	stu.name = "zs"
	fmt.Println(stu)
  }
  • 和slice、map不同的是, 只要定義了結構體變數就可以使用結構體變數
    • 默認情況下結構體中的所有屬性都是屬性對應型別的默認值
  package main
  import "fmt"
  type Student struct {
	  name string
	  age int
  }
  func main() {
	var stu Student // 相當于 var stu = Student{}
	fmt.Println(stu) // { 0}
	stu.name = "lnj" // 不會報錯
	stu.age = 33
	fmt.Println(stu) // {lnj 33}
  }

  • 復雜結構體成員
    • 創建時可以按照屬性單獨存在時初始化方式初始化
    package main
    import "fmt"
    type Student struct {
        name string
        age int
    }
    func main() {
      type Demo struct {
      	age int // 基本型別作為屬性
      	arr [3]int // 陣列型別作為屬性
      	sce []int // 切片型別作為屬性
      	mp map[string]string // 字典型別作為屬性
      	stu Student // 結構體型別作為屬性
      }
    
      var d Demo = Demo{
      	33,
      	[3]int{1, 3, 5},
      	[]int{2, 4, 6},
      	map[string]string{"class":"one"},
      	Student{
      		"lnj",
      		33,
      	},
      }
      fmt.Println(d) // {33 [1 3 5] [2 4 6] map[class:one] {lnj 33}}
    }
    
    • slice、map型別屬性默認值是nil,不能直接使用
    package main
    import "fmt"
    type Student struct {
        name string
        age int
    }
    func main() {
      type Demo struct {
      	age int // 基本型別作為屬性
      	arr [3]int // 陣列型別作為屬性
      	sce []int // 切片型別作為屬性
      	mp map[string]string // 字典型別作為屬性
      	stu Student // 結構體型別作為屬性
      }
    
      // 定義結構體變數
      var d Demo
      // 可以直接使用基本型別屬性
      d.age = 33
      // 可以直接使用陣列型別屬性
      d.arr[0] = 666
      // 不可以直接使用切片型別屬性
      //d.sce[0] = 888 // 編譯報錯
      d.sce = make([]int, 5) // 先創建
      d.sce[0] = 888 // 后使用
      // 不可以直接使用字典型別屬性
      //d.mp["class"] = "one" // 編譯報錯
      d.mp = make(map[string]string)// 先創建
      d.mp["class"] = "one"// 后使用
      // 可以直接使用結構體型別屬性
      d.stu.name = "lnj"
      fmt.Println(d) // {33 [666 0 0] [888 0 0 0 0] map[class:one] {lnj 0}}
    }
    

  • 結構體型別轉換
    • 屬性名、屬性型別、屬性個數、排列順序都是型別組成部分
    • 只有屬性名、屬性型別、屬性個數、排列順序都相同的結構體型別才能轉換
  package main
  import "fmt"
  func main() {
	type Person1 struct {
		name string
		age int
	}
	type Person2 struct {
		name string
		age int
	}
	type Person3 struct {
		age int
		name string
	}
	type Person4 struct {
		nm string
		age int
	}
	type Person5 struct {
		name string
		age string
	}
	type Person6 struct {
		age int
		name string
		gender string
	}

	var p1 Person1 = Person1{"lnj", 33}
	var p2 Person2
	// 型別名稱不一樣不能直接賦值(Person1、Person2)
	//p2 = p1

	// 雖然型別名稱不一樣, 但是兩個型別中的`屬性名稱`、`屬性型別`、`屬性個數`、`排列順序`都一樣,所以可以強制轉換
	p2 = Person2(p1)
	fmt.Println(p2)
	// 兩個結構體型別中的`屬性名稱`、`屬性型別`、`屬性個數`都一樣,但是`排列順序`不一樣,所以不能強制轉換
	//var p3 Person3
	//p3 = Person3(p1)
	//fmt.Println(p3)

	// 兩個結構體型別中的`屬性型別`、`屬性個數`、`排列順序`都一樣,但是`屬性名稱`不一樣,所以不能強制轉換
	//var p4 Person4
	//p4 = Person4(p1)
	//fmt.Println(p4)

	// 兩個結構體型別中的`屬性名稱`、`屬性個數`、`排列順序`都一樣,但是`屬性型別`不一樣,所以不能強制轉換
	//var p5 Person5
	//p5 = Person5(p1)
	//fmt.Println(p5)

	// 兩個結構體型別中的`屬性名稱`、`屬性型別`、`排列順序`都一樣,但是`屬性個數`不一樣,所以不能強制轉換
	//var p6 Person6
	//p6 = Person6(p1)
	//fmt.Println(p6)
  }

  • 匿名屬性
    • 沒有指定屬性名稱,只有屬性的型別, 我們稱之為匿名屬性
    • 任何有命名的資料型別都可以作為匿名屬性(int、float、bool、string、struct等)
    package main
    import "fmt"
    func main() {
      type Person struct {
      	int
      	float32
      	bool
      	string
      }
      // 不指定名稱初始化
      per1 := Person{3, 3.14, false, "lnj"}
      fmt.Println(per1)
    
      // 可以把資料型別作為名字顯示初始化
      per2 := Person{
      	int: 3,
      	float32: 3.14,
      	bool: true,
      	string: "lnj",
      }
      fmt.Println(per2)
    
      // 可以把資料型別當做屬性名稱操作結構體
      per2.int = 666
      fmt.Println(per2.int) // 666
    }
    
    • Go語言中最常見的匿名屬性是用結構體型別作為匿名屬性
    package main
    import "fmt"
    func main() {
     type Person struct {
     	name string
     	age int
     }
     type Student struct {
     	Person // 匿名屬性
     	class string
     }
    
     stu := Student{
     	Person{"lnj", 33},
     	"學前一班",
     }
     fmt.Println(stu) // {{lnj 33} 學前一班}
    }
    
    • 如果結構體作為匿名屬性, 想訪問匿名屬性的屬性有兩種方式
    package main
    import "fmt"
    func main() {
      type Person struct {
      	name string
      	age int
      }
      type Student struct {
      	Person // 匿名屬性
      	class string
      }
    
      stu := Student{
      	Person{"lnj", 33},
      	"學前一班",
      }
      fmt.Println(stu) // {{lnj 33} 學前一班}
    
      // 方式一: 先找到匿名屬性,再訪問匿名屬性中的屬性
      stu.Person.name = "zs"
      fmt.Println(stu) // {{zs 33} 學前一班}
      // 方式二: 直接訪問匿名屬性中的屬性
      // 系統會先查找當前結構體有沒有名稱叫做name的屬性
      // 如果沒有會繼續查找匿名屬性中有沒有名稱叫做name的屬性
      stu.name = "ww"
      fmt.Println(stu) // {{ww 33} 學前一班}
    }
    
    • 注意點: 如果多個匿名屬性的屬性名稱相同,那么不能通過方式二操作,只能通過方式一
    package main
    import "fmt"
    func main() {
      type Person struct {
      	name string
      	age int
      }
      type Class struct {
      	name string
      	time string
      }
      type Student struct {
      	Person // 匿名屬性
      	Class // 匿名屬性
      }
      stu := Student{
      	Person{"lnj", 33},
      	Class{"學前一班", "2020-12-12"},
      }
      fmt.Println(stu) // {{lnj 33} {學前一班 2020-12-12}}
      // 編譯報錯, 系統搞不清楚要找哪個name
      //stu.name = "zs"
    
      stu.Person.name = "zs"
      stu.Class.name = "小學一年級"
      fmt.Println(stu) // {{zs 33} {小學一年級 2020-12-12}}
    }
    
    • 注意點: 只有匿名結構體才支持向上查找
    package main
    import "fmt"
    func main() {
      type Person struct {
      	name string
      }
      type Student struct {
      	per Person
      	age int
      }
    
      var stu Student = Student{Person{"lnj"}, 18}
      //fmt.Println(stu.name) // 報錯
      fmt.Println(stu.per.name) // 必須通過屬性進一步查找
      fmt.Println(stu.age)
    }
    
    • 注意點: 如果匿名屬性是一個結構體型別, 那么這個結構體型別不能是自己
    package main
    import "fmt"
    func main() {
      type Person struct {
      	Person // 錯誤
      	name string
      }
      type Student struct {
      	*Student  // 正確, 鏈表
      	age int
      }
    }
    

普通指標

  • 和C語言一樣, 允許用一個變數來存放其它變數的地址, 這種專門用于存盤其它變數地址的變數, 我們稱之為指標變數
  • 和C語言一樣, Go語言中的指標無論是什么型別占用記憶體都一樣(32位4個位元組, 64位8個位元組)
package main

import (
	"fmt"
	"unsafe"
)

func main() {

	var p1 *int;
	var p2 *float64;
	var p3 *bool;
	fmt.Println(unsafe.Sizeof(p1)) // 8
	fmt.Println(unsafe.Sizeof(p2)) // 8
	fmt.Println(unsafe.Sizeof(p3)) // 8
}
  • 和C語言一樣, 只要一個指標變數保存了另一個變數對應的記憶體地址, 那么就可以通過*來訪問指標變數指向的存盤空間
package main

import (
	"fmt"
)

func main() {

	// 1.定義一個普通變數
	var num int = 666
	// 2.定義一個指標變數
	var p *int = &num
	fmt.Printf("%p\n", &num) // 0xc042064080
	fmt.Printf("%p\n", p) // 0xc042064080
	fmt.Printf("%T\n", p) // *int
	// 3.通過指標變數操作指向的存盤空間
	*p = 888
	// 4.指標變數操作的就是指向變數的存盤空間
	fmt.Println(num) // 888
	fmt.Println(*p) // 888
}

指向陣列指標

  • 在C語言中, 陣列名,&陣列名,&陣列首元素保存的都是同一個地址
#include <stdio.h>

int main(){
     int arr[3] = {1, 3, 5};
     printf("%p\n", arr); // 0060FEA4
     printf("%p\n", &arr); // 0060FEA4
     printf("%p\n", &arr[0]); // 0060FEA4
}
  • 在Go語言中通過陣列名無法直接獲取陣列的記憶體地址
package main
import "fmt"

func main() {
	var arr [3]int = [3]int{1, 3, 5}
	fmt.Printf("%p\n", arr) // 亂七八糟東西
	fmt.Printf("%p\n", &arr) // 0xc0420620a0
	fmt.Printf("%p\n", &arr[0]) // 0xc0420620a0
}
  • 在C語言中, 無論我們將陣列名,&陣列名,&陣列首元素賦值給指標變數, 都代表指標變數指向了這個陣列
#include <stdio.h>

int main(){
     int arr[3] = {1, 3, 5};
     int *p1 = arr;
     p1[1] = 6;
     printf("%d\n", arr[1]);

     int *p2 = &arr;
     p2[1] = 7;
     printf("%d\n", arr[1]);

     int *p3 = &arr[0];
     p3[1] = 8;
     printf("%d\n", arr[1]);
}
  • 在Go語言中, 因為只有資料型別一模一樣才能賦值, 所以只能通過&陣列名賦值給指標變數, 才代表指標變數指向了這個陣列
package main

import "fmt"

func main() {
	// 1.錯誤, 在Go語言中必須型別一模一樣才能賦值
	// arr型別是[3]int, p1的型別是*[3]int
	var p1 *[3]int
	fmt.Printf("%T\n", arr)
	fmt.Printf("%T\n", p1)
	p1 = arr // 報錯
	p1[1] = 6
	fmt.Println(arr[1])

	// 2.正確, &arr的型別是*[3]int, p2的型別也是*[3]int
	var p2 *[3]int
	fmt.Printf("%T\n", &arr)
	fmt.Printf("%T\n", p2)
	p2 = &arr
	p2[1] = 6
	fmt.Println(arr[1])

	// 3.錯誤, &arr[0]的型別是*int, p3的型別也是*[3]int
	var p3 *[3]int
	fmt.Printf("%T\n", &arr[0])
	fmt.Printf("%T\n", p3)
	p3 = &arr[0] // 報錯
	p3[1] = 6
	fmt.Println(arr[1])
}
  • 注意點:
    • Go語言中的指標, 不支持C語言中的+1 -1和++ – 操作
package main

import "fmt"

func main() {


	var arr [3]int = [3]int{1, 3, 5}
	var p *[3]int
	p = &arr
	fmt.Printf("%p\n", &arr) // 0xc0420620a0
	fmt.Printf("%p\n", p) // 0xc0420620a0
	fmt.Println(&arr) // &[1 3 5]
	fmt.Println(p) // &[1 3 5]
	// 指標指向陣列之后操作陣列的幾種方式
	// 1.直接通過陣列名操作
	arr[1] = 6
	fmt.Println(arr[1])
	// 2.通過指標間接操作
	(*p)[1] = 7
	fmt.Println((*p)[1])
	fmt.Println(arr[1])
	// 3.通過指標間接操作
	p[1] = 8
	fmt.Println(p[1])
	fmt.Println(arr[1])

	// 注意點: Go語言中的指標, 不支持+1 -1和++ --操作
	*(p + 1) = 9 // 報錯
	fmt.Println(*p++) // 報錯
	fmt.Println(arr[1])
}

指向切片的指標

  • 值得注意點的是切片的本質就是一個指標指向陣列, 所以指向切片的指標是一個二級指標

package main

import "fmt"

func main() {
	// 1.定義一個切片
	var sce[]int = []int{1, 3, 5}
	// 2.列印切片的地址
	// 切片變數中保存的地址, 也就是指向的那個陣列的地址 sce = 0xc0420620a0
	fmt.Printf("sce = %p\n",sce )
	fmt.Println(sce) // [1 3 5]
	// 切片變數自己的地址, &sce = 0xc04205e3e0
	fmt.Printf("&sce = %p\n",&sce )
	fmt.Println(&sce) // &[1 3 5]
	// 3.定義一個指向切片的指標
	var p *[]int
	// 因為必須型別一致才能賦值, 所以將切片變數自己的地址給了指標
	p = &sce
	// 4.列印指標保存的地址
	// 直接列印p列印出來的是保存的切片變數的地址 p = 0xc04205e3e0
	fmt.Printf("p = %p\n", p)
	fmt.Println(p) // &[1 3 5]
	// 列印*p列印出來的是切片變數保存的地址, 也就是陣列的地址 *p = 0xc0420620a0
	fmt.Printf("*p = %p\n", *p)
	fmt.Println(*p) // [1 3 5]
	
	// 5.修改切片的值
	// 通過*p找到切片變數指向的存盤空間(陣列), 然后修改陣列中保存的資料
	(*p)[1] = 666
	fmt.Println(sce[1])
}

指向字典指標

  • 與普通指標并無差異
package main
import "fmt"
func main() {

	var dict map[string]string = map[string]string{"name":"lnj", "age":"33"}
	var p *map[string]string = &dict
	(*p)["name"] = "zs"
	fmt.Println(dict)
}

指向結構體指標

  • Go語言中指向結構體的指標和C語言一樣
  • 結構體和指標
    • 創建結構體指標變數有兩種方式
    package main
    import "fmt"
    type Student struct {
        name string
        age int
    }
    func main() {
      // 創建時利用取地址符號獲取結構體變數地址
      var p1 = &Student{"lnj", 33}
      fmt.Println(p1) // &{lnj 33}
    
      // 通過new內置函式傳入資料型別創建
      // 內部會創建一個空的結構體變數, 然后回傳這個結構體變數的地址
      var p2 = new(Student)
      fmt.Println(p2) // &{ 0}
    }
    
    • 利用結構體指標操作結構體屬性
    package main
    import "fmt"
    type Student struct {
        name string
        age int
    }
    func main() {
      var p = &Student{}
      // 方式一: 傳統方式操作
      // 修改結構體中某個屬性對應的值
      // 注意: 由于.運算子優先級比*高, 所以一定要加上()
      (*p).name = "lnj"
      // 獲取結構體中某個屬性對應的值
      fmt.Println((*p).name) // lnj
    
      // 方式二: 通過Go語法糖操作
      // Go語言作者為了程式員使用起來更加方便, 在操作指向結構體的指標時可以像操作接頭體變數一樣通過.來操作
      // 編譯時底層會自動轉發為(*p).age方式
      p.age = 33
      fmt.Println(p.age) // 33
    }
    

指標作為函式引數和回傳值

  • 如果指標型別作為函式引數, 修改形參會影響實參
  • 不能將函式內的指向區域變數的指標作為回傳值, 函式結束指向空間會被釋放
  • 可以將函式內的區域變數作為回傳值, 本質是拷貝一份

方法

  • Go語言中的方法其實就是一個特殊函式, 只不過這個函式是和某種屬性型別系結在一起的而已

  • Go語言中的方法一般用于函式和結構體系結在一起, 讓結構體除了能夠保存資料外還能具備某些行為

  • 將函式和資料型別系結的格式

    • 只需要在函式名稱前面加上(接收者 資料型別), 即可將函式和某種資料型別系結在一起
func (接收者 資料型別)方法名稱(形參串列)(回傳值串列){
  方法體
}
  • 示例: 給結構體添加一個方法,此時結構體除了可以保存資料,還具備說出自己名字和年齡的行為
package main
import "fmt"
// 1.定義一個結構體
type Person struct {
	name string
	age int
}
// 2.定義一個函式, 并將這個函式和Person結構體系結在一起
func (p Person)say()  {
	fmt.Println("my name is", p.name, "my age is", p.age)
}
func main() {
	// 3.創建一個結構體變數
	per := Person{"lnj", 33}
	// 4.利用結構體變數呼叫和結構體系結的方法
	// 呼叫時會自動將呼叫者(per)傳遞給方法的接收者(p)
	// 所以可以在方法內部通過p方法結構體變數的屬性
	per.say()
}

  • 方法和函式異同
    • 方法的資料型別也是函式型別, 所以也可以定義變數保存(作為引數回傳值等)
    package main
    import "fmt"
    type Person struct {
      name string
      age int
    }
    // 定義一個方法
    func (p Person)say()  {
      fmt.Println("say方法")
    }
    // 定義一個函式
    func say()  {
      fmt.Println("say函式")
    }
    func main() {
      p := Person{"lnj", 33}
      fmt.Printf("%T\n", p.say) // func()
      fmt.Printf("%T\n", say) // func()
    
      // 定義一個保存沒有形參沒有回傳值的函式型別變數
      var fn func()
      // 利用函式型別變數保存一個方法
      fn = p.say
      // 利用函式型別變數呼叫一個方法
      fn()
    
      // 利用函式型別變數保存一個函式
      fn = say
      // 利用函式型別變數呼叫一個函式
      fn()
    }
    
    • 方法只能通過系結型別的變數呼叫, 函式可以直接呼叫
    package main
    import "fmt"
    type Person struct {
       name string
       age int
    }
    // 定義一個方法
    func (p Person)say()  {
       fmt.Println("my name is", p.name, "my age is", p.age)
    }
    // 定義一個函式
    func test()  {
       fmt.Println("test")
    }
    func main() {
       per := Person{"lnj", 33}
       per.say() // 方法只能通過系結型別的變數呼叫
       //say() // 編譯報錯, 不能直接呼叫
       test() // 編譯通過, 可以直接呼叫
    }
    
    • 方法的接收者可以看做就是函式的一個形參
    package main
    import "fmt"
    type Person struct {
       name string
       age int
    }
    // 定義一個方法
    func (p Person)say()  {
       fmt.Println("my name is", p.name, "my age is", p.age)
    }
    // 定義一個函式
    func test(p Person)  {
       fmt.Println("my name is", p.name, "my age is", p.age)
    }
    func main() {
       per := Person{"lnj", 33}
       per.say() // my name is lnj my age is 33
       test(per) // my name is lnj my age is 33
    }
    
    • 既然可以看做形參, 那么自然也具備形參的特點(值傳遞和地址傳遞)
    package main
    import "fmt"
    type Person struct {
       name string
       age int
    }
    // 接收者是一個變數
    func (p Person)setName(name string)  {
       p.name = name
    }
    // 接收者是一個指標
    func (p *Person)setAge(age int)  {
       p.age = age
    }
    func main() {
       per := Person{"lnj", 33}
       fmt.Println(per) // {lnj 33}
       // 值傳遞, 方法內部修改不會影響方法外部
       per.setName("zs")
       fmt.Println(per) // {lnj 33}
       p := &per
       // 地址傳遞, 方法內部修改會影響方法外部
       (*p).setAge(18)
       fmt.Println(per) // {lnj 18}
    }
    
    • 地址傳遞的幾種呼叫方式
    package main
    import "fmt"
    type Person struct {
       name string
       age int
    }
    // 接收者是一個變數
    func (p Person)setName(name string)  {
       p.name = name
    }
    // 接收者是一個指標
    func (p *Person)setAge(age int)  {
       p.age = age
    }
    func main() {
       per := Person{"lnj", 33}
       // 方式一: 先拿到指標,然后再通過指標呼叫
       p := &per
       (*p).setAge(18)
       fmt.Println(per) // {lnj 18}
       // 方式二: 直接利用變數呼叫, 底層會自動獲取變數地址傳遞給接收者
       per.setAge(66)
       fmt.Println(per) // {lnj 66}
    }
    

介面

  • Go語言中的介面和現實生活中的USB插槽很像, 它定義某種標準, 但不關心具體實作
    • 無論你到哪個商店里面去購買USB線,只要你告訴商家你需要一根USB線, 買回家之后就一定能插到電腦上使用. 之所以能用,原因就是電腦廠商在指定了USB插槽的標準(尺寸、排線等等), 生產廠家只需要按照標準生產即可
    • 同樣在Go語言中我們可以通過介面來定義某種標準(函式宣告),但不用不關心具體實作(函式實作), 只要將來有人按照標準實作了介面,我們就可以使用
  • 定義介面格式
type 介面名稱 interface{
  函式宣告
}
  • 示例: 定義一個通用的USB介面
package main
import "fmt"
// 1.定義一個介面
type usber interface {
	start()
	stop()
}
type Computer struct {
	name string
	model string
}
// 2.實作介面中的所有方法
func (cm Computer)start() {
	fmt.Println("啟動電腦")
}
func (cm Computer)stop() {
	fmt.Println("關閉電腦")
}

type Phone struct {
	name string
	model string
}
// 2.實作介面中的所有方法
func (p Phone)start()  {
	fmt.Println("啟動手機")
}
func (p Phone)stop()  {
	fmt.Println("關閉手機")
}

// 3.使用介面定義的方法
func working(u usber)  {
	u.start()
	u.stop()
}
func main() {
	cm := Computer{"戴爾", "F1234"}
	working(cm) // 啟動電腦 關閉電腦

	p := Phone{"華為", "M10"}
	working(p)  // 啟動手機 關閉手機
}
  • 介面注意點
    • 介面中只能有方法的宣告不能有方法的實作
    type usber interface {
        func start(){ // 錯誤
      	  fmt.Println("啟動")
        }
        func stop()  { // 錯誤
      	  fmt.Println("停止")
        }
    }
    
    • 介面中只能有方法什么不能有欄位
    type usber interface {
        name string // 錯誤
        start()
        stop()
    }
    
    • 只有實作了介面中所有的方法, 才算實作了介面, 才能用該介面型別接收
    package main
    import "fmt"
    // 1.定義一個介面
    type usber interface {
        start()
        stop()
    }
    type Computer struct {
        name string
        model string
    }
    // 2.實作介面中的所有方法
    func (cm Computer)start() {
        fmt.Println("啟動電腦")
    }
    func (cm Computer)stop() {
        fmt.Println("關閉電腦")
    }
    // 2.只實作了介面中部分方法
    type Phone struct {
        name string
        model string
    }
    func (p Phone)start()  {
        fmt.Println("啟動手機")
    }
    func main() {
        // 1.定義一個usber介面型別變數
        var i usber
        // 2.用usber介面型別變數接收Computer型別結構體
        i = Computer{"戴爾", "F1234"} // 實作了所有方法, 不會報錯
        // 3.用usber介面型別變數接收Phone型別結構體
        //i = Phone{"華為", "M10"} // 只實作了部分方法, 會報錯
        fmt.Println(i)
    }
    
    • 和結構體一樣,介面中也可以嵌入介面
    package main
    import "fmt"
    type A interface {
        fna()
    }
    type B interface {
        fnb()
    }
    type C interface {
        A // 嵌入A介面
        B // 嵌入B介面
        fnc()
    }
    type Person struct {}
    func (p Person)fna()  {
        fmt.Println("實作A介面中的方法")
    }
    func (p Person)fnb()  {
        fmt.Println("實作B介面中的方法")
    }
    func (p Person)fnc()  {
        fmt.Println("實作C介面中的方法")
    }
    func main() {
        p := Person{}
        p.fna() // 實作A介面中的方法
        p.fnb() // 實作B介面中的方法
        p.fnc() // 實作C介面中的方法
    }
    
    • 和結構體一樣,介面中嵌入介面時不能嵌入自己
    type A interface {
        A // 報錯, 不能自己搞自己
    }
    
    • 介面中嵌入介面時不能出現相同的方法名稱
    type A interface {
        fn()
    }
    type B interface {
        fn()
    }
    type C interface {
        A 
        B // 報錯, A介面和B介面都有名稱叫做fn的方法
        fnc()
    }
    
    • 超集介面變數可以自動轉換成子集介面變數, 子集介面變數不能轉換為超集介面變數(本質就是沒有實作所有方法)
    package main
    import "fmt"
    type aer interface {
        fna()
    }
    type ber interface {
        aer
        fnb()
    }
    // Person實作了超集介面所有方法
    type Person struct {}
    func (p Person)fna()  {
        fmt.Println("實作A介面中的方法")
    }
    func (p Person)fnb()  {
        fmt.Println("實作B介面中的方法")
    }
    // Student實作了子集介面所有方法
    type Student struct { }
    func (p Student)fna()  {
      fmt.Println("實作A介面中的方法")
    }
    func main() {
        var i ber
        // 子集介面變數不能轉換為超集介面變數
        //i = Student{}
        fmt.Println(i)
        var j aer
        // 超集介面變數可以自動轉換成子集介面變數,
        j = Person{}
        fmt.Println(j)
    }
    
    • 空介面型別可以接收任意型別資料
    package main
    import "fmt"
    func main() {
        // 1.定義一個空介面型別變數
        var i interface{}
        // 2.用介面型別保存任意型別資料
        i = 123
        fmt.Println(i) // 123
        i = 3.14
        fmt.Println(i) // 3.14
        i = "lnj"
        fmt.Println(i) // lnj
        i = [3]int{1, 3, 5}
        fmt.Println(i) // [1 3 5]
        i = []int{2, 4, 6}
        fmt.Println(i) // [2 4 6]
        i = map[string]string{"name": "lnj"}
        fmt.Println(i) // map[name:lnj]
        i = Computer{"戴爾", "F1234"}
        fmt.Println(i) // {戴爾 F1234}
    }
    
    • 只要是自定義型別就可以實作介面
    package main
    import "fmt"
    // 1.定義一個介面
    type usber interface {
        start()
        stop()
    }
    // 2.自定義int型別
    type integer int
    // 2.實作介面中的所有方法
    func (i integer)start()  {
        fmt.Println("int型別實作介面")
    }
    func (i integer)stop()  {
        fmt.Println("int型別實作介面")
    }
    func main() {
        var i integer = 666
        i.start() // int型別實作介面
        i.stop() // int型別實作介面
    }
    
  • 介面型別轉換
    • 介面型別變數可以接收實作了該介面型別的變數, 但是只能呼叫該變數中的方法, 不能訪問該變數的屬性
    package main
    import "fmt"
    type studier interface {
        read()
    }
    type Person struct {
        name string
        age int
    }
    func (p Person)read()  {
        fmt.Println(p.name, "正在學習")
    }
    func main() {
        // 1.定義一個介面型別變數
        var s studier
        // 2.用介面型別變數接收實作了介面的結構體
        s = Person{"lnj", 33}
        s.name = "zs" // 報錯, 由于s是介面型別, 所以不能訪問屬性
        fmt.Println(s)
    }
    
    • 想要訪問變數中的屬性, 必須將介面型別還原為原始型別
    package main
    import "fmt"
    type studier interface {
        read()
    }
    type Person struct {
        name string
        age int
    }
    func (p Person)read()  {
        fmt.Println(p.name, "正在學習")
    }
    func main() {
        var s studier
        s = Person{"lnj", 33}
        s.name = "zs" // 報錯, 由于s是介面型別, 所以不能訪問屬性
        // 2.定義一個結構體型別變數
        //var p Person
        // 不能用強制型別轉換方式將介面型別轉換為原始型別
        //p = Person(s) // 報錯
    
        // 2.利用ok-idiom模式將介面型別還原為原始型別
        // s.(Person)這種格式我們稱之為: 型別斷言
        if p, ok := s.(Person); ok {
      	  p.name = "zs"
      	  fmt.Println(p)
        }
    
        // 2.通過 type switch將介面型別還原為原始型別
       // 注意: type switch不支持fallthrought
        switch p := s.(type) {
      	  case Person:
      		  p.name = "zs"
      		  fmt.Println(p) // {zs 33}
      	  default:
      		  fmt.Println("不是Person型別")
        }
    }
    
    • 除了可以將介面型別轉換為原始型別以外, 企業開發中有時候可能我們還需要將抽象介面型別轉換為具體介面型別
    package main
    import "fmt"
    type studier interface {
        read()
    }
    type Person struct {
        name string
        age int
    }
    func (p Person)read()  {
        fmt.Println(p.name, "正在學習")
    }
    func main() {
        // 1.定義一個抽象介面型別
        var i interface{}
        i = Person{"lnj", 33}
        // 不能呼叫read方法, 因為抽象介面中沒有這個方法
        //i.read()
        // 2.利用ok-idiom模式將抽象介面轉換為具體介面
        if s, ok := i.(studier); ok{
      	  // 可以呼叫read方法,因為studier中宣告了這個方法,并且結構體中實作了這個方法
      	  s.read() // lnj 正在學習
        }
      }
    

面向物件基本概念

面向物件思想

  • 面向物件(Object Oriented,OO)是軟體開發方法
  • 面向物件是一種對現實世界抽象的理解,是計算機編程技術發展到一定階段后的產物
  • Object Oriented Programming-OOP ——面向物件編程

面向物件和面向程序區別

  • 面向物件是相對面向程序而言

  • 面向物件和面向程序都是一種思想

  • 面向程序

    • 強調的是功能行為
    • 關注的是解決問題需要哪些步驟
  • 回想下前面我們完成一個需求的步驟:

    • 首先搞清楚我們要做什么
    • 然后分析怎么做
    • 最后我用代碼體現
    • 一步一步去實作,而具體的每一步都需要我們去實作和操作
  • 在上面每一個具體步驟中我們都是參與者, 并且需要面對具體的每一個步驟和程序, 這就是面向程序最直接的體現


  • 面向物件
    • 將功能封裝進物件,強調具備了功能的物件
    • 關注的是解決問題需要哪些物件
  • 當需求單一, 或者簡單時, 我們一步一步去操作沒問題, 并且效率也挺高, 可隨著需求的更改, 功能的增加, 發現需要面對每一個步驟非常麻煩, 這時就開始思索, 能不能把這些步驟和功能再進行封裝, 封裝時根據不同的功能,進行不同的封裝,功能類似的封裝在一起,這樣結構就清晰多了, 用的時候, 找到對應的類就可以了, 這就是面向物件思想

  • 示例
  • 買電腦
    • 面向程序

      • 了解電腦
      • 了解自己的需求
      • 對比引數
      • 去電腦城
      • 砍價,付錢
      • 買回電腦
      • 被坑
    • 面向物件

      • 找班長
      • 描述需求
      • 班長把電腦買回來

  • 吃飯
    • 面向程序

      • 去超市賣菜
      • 摘菜
      • 洗菜
      • 切菜
      • 炒菜
      • 盛菜
    • 面向物件

      • 去飯店
      • 點菜

  • 洗衣服
    • 面向程序
      • 脫衣服
      • 放進盆里
      • 放洗衣液
      • 加水
      • 放衣服
      • 搓一搓
      • 清一清
      • 擰一擰
      • 曬起來
    • 面向物件
      • 脫衣服
      • 打開洗衣機
      • 丟進去
      • 一鍵洗衣烘干
    • 終極面向物件
      • 買電腦/吃飯/洗衣服
      • 找個物件
  • 現實生活中我們是如何應用面相物件思想的
    • 包工頭
    • 汽車壞了
    • 面試

面向物件的特點

  • 是一種符合人們思考習慣的思想
  • 可以將復雜的事情簡單化
  • 將程式員從執行者轉換成了指揮者
  • 完成需求時:
    • 先要去找具有所需的功能的物件來用
    • 如果該物件不存在,那么創建一個具有所需功能的物件
    • 這樣簡化開發并提高復用

類與物件的關系

  • 面向物件的核心就是物件,那怎么創建物件?

    • 現實生活中可以根據模板創建物件,編程語言也一樣,也必須先有一個模板,在這個模板中說清楚將來創建出來的物件有哪些屬性行為
  • Go語言中的類相當于圖紙,用來描述一類事物,也就是說要想創建物件必須先有類

  • Go語言利用類來創建物件,物件是類的具體存在, 因此面向物件解決問題應該是先考慮需要設計哪些類,再利用類創建多少個物件


如何設計一個類

  • 生活中描述事物無非就是描述事物的屬性行為
    • 如:人有身高,體重等屬性,有說話,打架等行為,
事物名稱(類名):(Person)
屬性:身高(height)、年齡(age)
行為(功能):(run)、打架(fight)
  • Go語言中用類來描述事物也是如此
    • 屬性:對應類中的成員變數,
    • 行為:對應類中的成員方法,
  • 定義類其實在定義類中的成員(成員變數和成員方法)
  • 擁有相同或者類似屬性(狀態特征)和行為(能干什么事)的物件都可以抽像成為一個類

如何分析一個類

  • 一般名詞都是類(名詞提煉法)
    • 飛機發射兩顆炮彈摧毀了8輛裝甲車
飛機
炮彈
裝甲車
  • 隔壁老王在公車上牽著一條叼著熱狗的草泥馬
老王
熱狗
草泥馬

如何定義一個類

  • 類是用于描述事物的的屬性和行為的, 而Go語言中的結構體正好可以用于描述事物的屬性和行為
  • 所以在Go語言中我們使用結構體來定義一個型別
type Person struct {
    name string // 人的屬性
    age int // 人的屬性
}
// 人的行為
func (p Person)Say()  {
    fmt.Println("my name is", p.name, "my age is", p.age)
}

如何通過類創建一個物件

  • 不過就是創建結構體的時候, 根據每個物件的特征賦值不同的屬性罷了
// 3.創建一個結構體變數
    p1 := Person{"lnj", 33}
    per.say()

    p2 := Person{"zs", 18}
    per.Say()

不同包中變數、函式、方法、型別公私有問題

  • 在Go語言中通過首字母大小寫來控制變數、函式、方法、型別的公私有
  • 如果首字母小寫, 那么代表私有, 僅在當前包中可以使用
  • 如果首字母大寫, 那么代表共有, 其它包中也可以使用
package demo

import "fmt"

var num1 int = 123 // 當前包可用
var Num1 int = 123 // 其它包也可用

type person struct { // 當前包可用
	name string // 當前包可用
	age int // 當前包可用
}

type Student struct { // 其它包也可用
	Name string // 其它包也可用
	Age int // 其它包也可用
}

func test1()  { // 當前包可用
	fmt.Println("test1")
}
func Test2()  { // 其它包也可用
	fmt.Println("Test2")
}

func (p person)say()  { // 當前包可用
	fmt.Println(p.name, p.age)
}
func (s Student)Say()  { // 其它包也可用
	fmt.Println(s.Name, s.Age)
}

面向物件三大特性

  • 封裝性
    • 封裝性就是隱藏實作細節,僅對外公開介面
    • 類是資料與功能的封裝,資料就是成員變數,功能就是方法
  • 為什么要封裝?
    • 不封裝的缺點:當一個類把自己的成員變數暴露給外部的時候,那么該類就失去對該成員變數的管理權,別人可以任意的修改你的成員變數
    • 封裝就是將資料隱藏起來,只能用此類的方法才可以讀取或者設定資料,不可被外部任意修改是面向物件設計本質(將變化隔離),這樣降低了資料被誤用的可能 (提高安全性靈活性)
package model
import "fmt"
type Person struct { // 其它包也可用
	name string // 當前包可用
	age int // 當前包可用
}
func (p *person)SetAge(age int)  {
	// 安全校驗
	if age < 0 {
		fmt.Println("年齡不能為負數")
	}
	p.age = age
}
package main
import (
	"fmt"
	"main/model"
)
func main()  {
	// 報錯, 因為name和age不是公開的
	//p := model.Person{"lnj", 18}

	// 方式一
	//p := model.Person{}
	//p.SetAge(18)
	//fmt.Println(p)

	// 方式二
	//p := new(model.Person)
	//p.SetAge(18)
	//fmt.Println(p)
}
  • 封裝原則
    • 將不需要對外提供的內容都隱藏起來,把屬性都隱藏,提供公共的方法對其訪問

  • 繼承性

    • Go語言認為雖然繼承能夠提升代碼的復用性, 但是會讓代碼腐爛, 并增加代碼的復雜度.
    • 所以go語言堅持了〃組合優于繼承〃的原則, Go語言中所謂的繼承其實是利用組合實作的(匿名結構體屬性)

  • 普通繼承(組合)

package main

import "fmt"

type Person struct {
	name string
	age int
}
type Student struct {
	Person // 學生繼承了人的特性
	score int
}
type Teacher struct {
	Person // 老師繼承了人的特性
	Title string
}
func main()  {
	s := Student{Person{"lnj", 18}, 99}
	//fmt.Println(s.Person.name)
	fmt.Println(s.name) // 兩種方式都能訪問
	//fmt.Println(s.Person.age)
	fmt.Println(s.age) // 兩種方式都能訪問
	fmt.Println(s.score)
}
  • 繼承結構中出現重名情況, 采用就近原則
package main

import "fmt"

type Person struct {
	name string // 屬性重名
	age int
}
type Student struct {
	Person
	name string // 屬性重名
	score int
}
func main()  {
	s := Student{Person{"zs", 18}, "ls", 99}

	fmt.Println(s.Person.name) // zs
	fmt.Println(s.name) // ls
	//fmt.Println(s.Person.age)
	fmt.Println(s.age) // 兩種方式都能訪問
	fmt.Println(s.score)
}
  • 多重繼承
package main

import "fmt"

type Object struct {
	life int
}
type Person struct {
	Object
	name string
	age int
}
type Student struct {
	Person
	score int
}
func main()  {
	s := Student{Person{Object{77}, "zs", 33}, 99}
	//fmt.Println(s.Person.Object.life)
	//fmt.Println(s.Person.life)
	fmt.Println(s.life) // 三種方式都可以
	//fmt.Println(s.Person.name)
	fmt.Println(s.name) // 兩種方式都能訪問
	//fmt.Println(s.Person.age)
	fmt.Println(s.age) // 兩種方式都能訪問
	fmt.Println(s.score)
}
package main

import "fmt"

type Object struct {
	life int
}
type Person struct {
	name string
	age int
}
type Student struct {
	Object
	Person
	score int
}
func main()  {
	s := Student{Object{77}, Person{"zs", 33}, 99}
	//fmt.Println(s.Person.life)
	fmt.Println(s.life) // 兩種方式都可以
	//fmt.Println(s.Person.name)
	fmt.Println(s.name) // 兩種方式都能訪問
	//fmt.Println(s.Person.age)
	fmt.Println(s.age) // 兩種方式都能訪問
	fmt.Println(s.score)
  • 方法繼承
    • 在Go語言中子類不僅僅能夠繼承父類的屬性, 還能夠繼承父類的方法
package main

import "fmt"
type Person struct {
	name string
	age int
}
// 父類方法
func (p Person)say()  {
	fmt.Println("name is ", p.name, "age is ", p.age)
}

type Student struct {
	Person
	score float32
} 

func main() {
	stu := Student{Person{"zs", 18}, 59.9}
	stu.say()
}
  • 繼承中的方法重寫
    • 如果子類有和父類同名的方法, 那么我們稱之為方法重寫
package main

import "fmt"
type Person struct {
	name string
	age int
}
// 父類方法
func (p Person)say()  {
	fmt.Println("name is ", p.name, "age is ", p.age)
}

type Student struct {
	Person
	score float32
}
// 子類方法
func (s Student)say()  {
	fmt.Println("name is ", s.name, "age is ", s.age, "score is ", s.score)
}
func main() {
	stu := Student{Person{"zs", 18}, 59.9}
	// 和屬性一樣, 訪問時采用就近原則
	stu.say() 
	// 和屬性一樣, 方法同名時可以通過指定父類名稱的方式, 訪問父類方法
	stu.Person.say()
}
  • 注意點:
    • 無論是屬性繼承還是方法繼承, 都只能子類訪問父類, 不能父類訪問子類

  • 多型性
    • 多型就是某一類事物的多種形態
貓: 貓-->動物
狗: 狗-->動物
男人 : 男人 -->人 -->高級動物
女人 : 女人 -->人 -->高級動物
  • Go語言中的多型是采用介面來實作的
package main

import "fmt"
// 1.定義介面
type Animal interface { 
	Eat()
}
type Dog struct {
	name string
	age int
}
// 2.實作介面方法
func (d Dog)Eat()  {
	fmt.Println(d.name, "正在吃東西")
}

type Cat struct {
	name string
	age int
}
// 2.實作介面方法
func (c Cat)Eat()  {
	fmt.Println(c.name, "正在吃東西")
}
// 3.物件特有方法
func (c Cat)Special()  {
	fmt.Println(c.name, "特有方法")
}

func main()  {
	// 1.利用介面型別保存實作了所有介面方法的物件
	var a Animal
	a = Dog{"旺財", 18}
	// 2.利用介面型別呼叫物件中實作的方法
	a.Eat()
	a = Cat{"喵喵", 18}
	a.Eat()
	// 3.利用介面型別呼叫物件特有的方法
	//a.Special() // 介面型別只能呼叫介面中宣告的方法, 不能呼叫物件特有方法
	if cat, ok := a.(Cat); ok{
		cat.Special() // 只有物件本身才能呼叫物件的特有方法
	}
}
  • 多型優點
    • 多型的主要好處就是簡化了編程介面,它允許在類和類之間重用一些習慣性的命名,而不用為每一個新的方法命名一個新名字,這樣,編程介面就是一些抽象的行為的集合,從而和實作介面的類的區分開來
    • 多型也使得代碼可以分散在不同的物件中而不用試圖在一個方法中考慮到所有可能的物件, 這樣使得您的代碼擴展性和復用性更好一些,當一個新的情景出現時,您無須對現有的代碼進行改動,而只需要增加一個新的類和新的同名方法

例外處理

  • 程式運行時,發生的不被期望的事件,它阻止了程式按照程式員的預期正常執行,這就是例外
  • golang中提供了兩種處理例外的方式
    • 一種是程式發生例外時, 將例外資訊反饋給使用者
    • 一種是程式發生例外時, 立刻退出終止程式繼續運行

列印例外資訊

  • Go語言中提供了兩種創建例外資訊的方式
  • 方式一: 通過fmt包中的Errorf函式創建錯誤資訊, 然后列印
package main
import "fmt"
func main() {
	// 1.創建錯誤資訊
	var err error = fmt.Errorf("這里是錯誤資訊")
	// 2.列印錯誤資訊
	fmt.Println(err) // 這里是錯誤資訊
}
  • 方式二: 通過errors包中的New函式創建錯誤資訊,然后列印
package main
import "fmt"
func main() {
	// 1.創建錯誤資訊
	var err error = errors.New("這里是錯誤資訊")
	// 2.列印錯誤資訊
	fmt.Println(err) // 這里是錯誤資訊
}
  • 兩種創建例外資訊實作原理決議
    • Go語言中創建例外資訊其實都是通過一個error介面實作的
    • Go語言再builtin包中定義了一個名稱叫做error的介面. 原始碼如下
package builtin
// 定義了一個名稱叫做error的介面
// 介面中宣告了一個叫做Error() 的方法
type error interface {
	Error() string
}
  • 在errors包中定義了一個名稱叫做做errorString的結構體, 利用這個結構體實作了error介面中指定的方法
  • 并且在errors 包中還提供了一個New方法, 用于創建實作了error介面的結構體物件, 并且在創建時就會把指定的字串傳遞給這個結構體
// 指定包名為errors
package errors 
// 定義了一個名稱叫做errorString的結構體, 里面有一個字串型別屬性s
type errorString struct {
	s string
}
// 實作了error介面中的Error方法
// 內部直接將結構體中保存的字串回傳
func (e *errorString) Error() string {
	return e.s
}
// 定義了一個New函式, 用于創建例外資訊
// 注意: New函式的回傳值是一個介面型別
func New(text string) error {
        // 回傳一個創建好的errorString結構體地址
	return &errorString{text}
}
  • fmt包中Errorf底層的實作原理其實就是在內部自動呼叫了errors包中的New函式
func Errorf(format string, a ...interface{}) error {
	return errors.New(Sprintf(format, a...))
}
  • 應用場景
package main
import "fmt"
func div(a, b int) (res int, err error) {
	if(b == 0){
		// 一旦傳入的除數為0, 就會回傳error資訊
		err = errors.New("除數不能為0")
	}else{
		res = a / b
	}
	return
}
func main() {
	//res, err := div(10, 5)
	res, err := div(10, 0)
	if(err != nil){
		fmt.Println(err) // 除數不能為0
	}else{
		fmt.Println(res) // 2
	}
}

中斷程式

  • Go語言中提供了一個叫做panic函式, 用于發生例外時終止程式繼續運行
package main
import "fmt"
func div(a, b int) (res int) {
	if(b == 0){
		//一旦傳入的除數為0, 程式就會終止
		panic("除數不能為0")
	}else{
		res = a / b
	}
	return
}
func main() {
	res := div(10, 0)
	fmt.Println(res)
}
  • Go語言中有兩種方式可以觸發panic終止程式
    • 我們自己手動呼叫panic函式
    • 程式內部出現問題自動觸發panic函式
package main
import "fmt"
func main() {
	// 例如:陣列角標越界, 就會自動觸發panic
	var arr = [3]int{1, 3, 5}
	arr[5] = 666 // 報錯
	fmt.Println(arr)

	// 例如:除數為0, 就會自動觸發panic
	var res = 10 / 0
	fmt.Println(res)
}
  • 除非是不可恢復性、導致系統無法正常作業的錯誤, 否則不建議使用panic

恢復程式

  • 程式和人一樣都需要具備一定的容錯能力, 學會知錯就改. 所以如果不是不可恢復性、導致系統無法正常作業的錯誤, 如果發生了panic我們需要恢復程式, 讓程式繼續執行,并且需要記錄到底犯了什么錯誤
  • 在Go語言中我們可以通過defer和recover來實作panic例外的捕獲, 讓程式繼續執行
package main
import "fmt"
func div(a, b int) (res int) {
	// 定義一個延遲呼叫的函式, 用于捕獲panic例外
	// 注意: 一定要在panic之前定義
	defer func() {
		if err := recover(); err != nil{
			res = -1
			fmt.Println(err) // 除數不能為0
		}
	}()
	if(b == 0){
		//err = errors.New("除數不能為0")
		panic("除數不能為0")
	}else{
		res = a / b
	}
	return
}

func setValue(arr []int, index int ,value int)  {
	arr[index] = value
}
func main() {
	res := div(10, 0)
	fmt.Println(res) // -1
}
  • panic注意點
    • panic例外會沿著呼叫堆疊向外傳遞, 所以也可以在外層捕獲
package main
import "fmt"
func div(a, b int) (res int) {
	if(b == 0){
		//err = errors.New("除數不能為0")
		panic("除數不能為0")
	}else{
		res = a / b
	}
	return
}
func main() {
	// panic例外會沿著呼叫堆疊向外傳遞, 所以也可以在外層捕獲
	defer func() {
		if err := recover(); err != nil{
			fmt.Println(err) // 除數不能為0
		}
	}()
	div(10, 0)
}
  • 多個例外,只有第一個會被捕獲
package main
import "fmt"
func test1()  {
	// 多個例外,只有第一個會被捕獲
	defer func() {
		if err := recover(); err != nil{
			fmt.Println(err) // 例外A
		}
	}()
	panic("例外A") // 相當于return, 后面代碼不會繼續執行
	panic("例外B")
}
func main() {
	test1(10, 0)
}
  • 如果有例外寫在defer中, 那么只有defer中的例外會被捕獲
package main
import "fmt"
func test2()  {
	// 如果有例外寫在defer中, 并且其它例外寫在defer后面, 那么只有defer中的例外會被捕獲
	defer func() {
		if err := recover(); err != nil{
			fmt.Println(err) // 例外A
		}
	}()
	
	defer func() { 
		panic("例外B")
	}()
	panic("例外A")
}
func main() {
	test1(10, 0)
}

字串相關方法

  • 獲取字串長度
    • 注意: Go語言編碼方式是UTF-8,在UTF-8中一個漢字占3個位元組
package main
import "fmt"
func main() {
	str1 := "lnj"
	fmt.Println(len(str1)) // 3
	str2 := "公號:代碼情緣"
	fmt.Println(len(str2)) // 12
}
  • 如果字串中包含中文, 又想精確的計算字串中字符的個數而不是占用的位元組, 那么必須先將字串轉換為rune型別陣列
    • Go語言中byte用于保存字符, rune用于保存漢字
package main
import "fmt"
func main() {
	str := "公號:代碼情緣"
	// 注意byte占1個位元組, 只能保存字符不能保存漢字,因為一個漢字占用3個位元組
	arr1 := []byte(str) // 12
	fmt.Println(len(arr1))
	for _, v := range arr1{
		fmt.Printf("%c", v) // lnj?????—?±?
	}

	// Go語言中rune型別就是專門用于保存漢字的
	arr2 := []rune(str)
	fmt.Println(len(arr2)) // 6
	for _, v := range arr2{
		fmt.Printf("%c", v) // lnj李南江
	}
}
  • 查找子串在字串中出現的位置
    • func Index(s, sep string) int
    • func IndexByte(s string, c byte) int
    • func IndexRune(s string, r rune) int
    • func IndexAny(s, chars string) int
    • func IndexFunc(s string, f func(rune) bool) int
    • func LastIndex(s, sep string) int
    • func LastIndexByte(s string, c byte) int
    • func LastIndexAny(s, chars string) int
    • func LastIndexFunc(s string, f func(rune) bool) int
package main
import (
	"strings"
	"fmt"
)
func main() {
	// 查找`字符`在字串中第一次出現的位置, 找不到回傳-1
	res := strings.IndexByte("hello 李南江", 'l')
	fmt.Println(res) // 2

	// 查找`漢字`OR`字符`在字串中第一次出現的位置, 找不到回傳-1
	res = strings.IndexRune("hello 李南江", '李')
	fmt.Println(res) // 6
	res = strings.IndexRune("hello 李南江", 'l')
	fmt.Println(res) // 2

	// 查找`漢字`OR`字符`中任意一個在字串中第一次出現的位置, 找不到回傳-1
	res = strings.IndexAny("hello 李南江", "wml")
	fmt.Println(res) // 2
	// 會把wmhl拆開逐個查找, w、m、h、l只要任意一個被找到, 立刻停止查找
	res = strings.IndexAny("hello 李南江", "wmhl")
	fmt.Println(res) // 0
	// 查找`子串`在字串第一次出現的位置, 找不到回傳-1
	res = strings.Index("hello 李南江", "llo")
	fmt.Println(res) // 2
	// 會把lle當做一個整體去查找, 而不是拆開
	res = strings.Index("hello 李南江", "lle")
	fmt.Println(res) // -1
	// 可以查找字符也可以查找漢字
	res = strings.Index("hello 李南江", "李")
	fmt.Println(res) // 6

	// 會將字串先轉換為[]rune, 然后遍歷rune切片逐個取出傳給自定義函式
	// 只要函式回傳true,代表符合我們的需求, 既立即停止查找
	res = strings.IndexFunc("hello 李南江", custom)
	fmt.Println(res) // 6

	// 倒序查找`子串`在字串第一次出現的位置, 找不到回傳-1
	res := strings.LastIndex("hello 李南江", "l")
	fmt.Println(res) // 3
}
func custom(r rune) bool {
	fmt.Printf("被呼叫了, 當前傳入的是%c\n", r)
	if r == 'o' {
		return true
	}
	return false
}
  • 判斷字串中是否包含子串
    • func Contains(s, substr string) bool
    • func ContainsRune(s string, r rune) bool
    • func ContainsAny(s, chars string) bool
    • func HasPrefix(s, prefix string) bool
    • func HasSuffix(s, suffix string) bool
package main
import (
	"strings"
	"fmt"
)
func main() {
	// 查找`子串`在字串中是否存在, 存在回傳true, 不存在回傳false
	// 底層實作就是呼叫strings.Index函式
	res := strings.Contains( "hello 代碼情緣", "llo")
	fmt.Println(res) // true

	// 查找`漢字`OR`字符`在字串中是否存在, 存在回傳true, 不存在回傳false
	// 底層實作就是呼叫strings.IndexRune函式
	res = strings.ContainsRune( "hello 代碼情緣", 'l')
	fmt.Println(res) // true
	res = strings.ContainsRune( "hello 代碼情緣", '李')
	fmt.Println(res) // true

	// 查找`漢字`OR`字符`中任意一個在字串中是否存在, 存在回傳true, 不存在回傳false
	// 底層實作就是呼叫strings.IndexAny函式
	res = strings.ContainsAny( "hello 代碼情緣", "wmhl")
	fmt.Println(res) // true

	// 判斷字串是否已某個字串開頭
	res = strings.HasPrefix("lnj-book.avi", "lnj")
	fmt.Println(res) // true

	// 判斷字串是否已某個字串結尾
	res = strings.HasSuffix("lnj-book.avi", ".avi")
	fmt.Println(res) // true
}
  • 字串比較
    • func Compare(a, b string) int
    • func EqualFold(s, t string) bool
package main
import (
	"strings"
	"fmt"
)
func main() {
	// 比較兩個字串大小, 會逐個字符地進行比較ASCII值
	// 第一個引數 >  第二個引數 回傳 1
	// 第一個引數 <  第二個引數 回傳 -1
	// 第一個引數 == 第二個引數 回傳 0
	res := strings.Compare("bcd", "abc")
	fmt.Println(res) // 1
	res = strings.Compare("bcd", "bdc")
	fmt.Println(res) // -1
	res = strings.Compare("bcd", "bcd")
	fmt.Println(res) // 0

	// 判斷兩個字串是否相等, 可以判斷字符和中文
	// 判斷時會忽略大小寫進行判斷
	res2 := strings.EqualFold("abc", "def")
	fmt.Println(res2) // false
	res2 = strings.EqualFold("abc", "abc")
	fmt.Println(res2) // true
	res2 = strings.EqualFold("abc", "ABC")
	fmt.Println(res2) // true
	res2 = strings.EqualFold("代碼情緣", "代碼情緣")
	fmt.Println(res2) // true
}
  • 字串轉換
    • func ToUpper(s string) string
    • func ToLower(s string) string
    • func ToTitle(s string) string
    • func ToUpperSpecial(_case unicode.SpecialCase, s string) string
    • func ToLowerSpecial(_case unicode.SpecialCase, s string) string
    • func ToTitleSpecial(_case unicode.SpecialCase, s string) string
    • func Title(s string) string
package main
import (
	"strings"
	"fmt"
)
func main() {
	// 將字串轉換為小寫
	res := strings.ToLower("ABC")
	fmt.Println(res) // abc
	
	// 將字串轉換為大寫
	res = strings.ToUpper("abc")
	fmt.Println(res) // ABC

	// 將字串轉換為標題格式, 大部分`字符`標題格式就是大寫
	res = strings.ToTitle("hello world")
	fmt.Println(res) // HELLO WORLD
	res = strings.ToTitle("HELLO WORLD")
	fmt.Println(res) // HELLO WORLD

	// 將單詞首字母變為大寫, 其它字符不變
	// 單詞之間用空格OR特殊字符隔開
	res = strings.Title("hello world")
	fmt.Println(res) // Hello World
}
  • 字串拆合
    • func Split(s, sep string) []string
    • func SplitN(s, sep string, n int) []string
    • func SplitAfter(s, sep string) []string
    • func SplitAfterN(s, sep string, n int) []string
    • func Fields(s string) []string
    • func FieldsFunc(s string, f func(rune) bool) []string
    • func Join(a []string, sep string) string
    • func Repeat(s string, count int) string
    • func Replace(s, old, new string, n int) string
package main
import (
	"strings"
	"fmt"
)
func main() {
	// 按照指定字串切割原字串
	// 用,切割字串
	arr1 := strings.Split("a,b,c", ",")
	fmt.Println(arr1) // [a b c]
	arr2 := strings.Split("ambmc", "m")
	fmt.Println(arr2) // [a b c]

	// 按照指定字串切割原字串, 并且指定切割為幾份
	// 如果最后一個引數為0, 那么會范圍一個空陣列
	arr3 := strings.SplitN("a,b,c", ",", 2)
	fmt.Println(arr3) // [a b,c]
	arr4 := strings.SplitN("a,b,c", ",", 0)
	fmt.Println(arr4) // []

	// 按照指定字串切割原字串, 切割時包含指定字串
	arr5 := strings.SplitAfter("a,b,c", ",")
	fmt.Println(arr5) // [a, b, c]

	// 按照指定字串切割原字串, 切割時包含指定字串, 并且指定切割為幾份
	arr6 := strings.SplitAfterN("a,b,c", ",", 2)
	fmt.Println(arr6) // [a, b,c]

	// 按照空格切割字串, 多個空格會合并為一個空格處理
	arr7 := strings.Fields("a  b c    d")
	fmt.Println(arr7) // [a b c d]

	// 將字串轉換成切片傳遞給函式之后由函式決定如何切割
	// 類似于IndexFunc
	arr8 := strings.FieldsFunc("a,b,c", custom)
	fmt.Println(arr8) // [a b c]

	// 將字串切片按照指定連接符號轉換為字串
	sce := []string{"aa", "bb", "cc"}
	str1 := strings.Join(sce, "-")
	fmt.Println(str1) // aa-bb-cc


	// 回傳count個s串聯的指定字串
	str2 := strings.Repeat("abc", 2)
	fmt.Println(str2) // abcabc

	// 第一個引數: 需要替換的字串
	// 第二個引數: 舊字串
	// 第三個引數: 新字串
	// 第四個引數: 用新字串 替換 多少個舊字串
	// 注意點: 傳入-1代表只要有舊字串就替換
	// 注意點: 替換之后會生成新字串, 原字串不會受到影響
	str3 := "abcdefabcdefabc"
	str4 := strings.Replace(str3, "abc", "mmm", -1)
	fmt.Println(str3) // abcdefabcdefabc
	fmt.Println(str4) // mmmdefmmmdefmmm
}
func custom(r rune) bool {
	fmt.Printf("被呼叫了, 當前傳入的是%c\n", r)
	if r == ',' {
		return true
	}
	return false
}
  • 字串清理
    • func Trim(s string, cutset string) string
    • func TrimLeft(s string, cutset string) string
    • func TrimRight(s string, cutset string) string
    • func TrimFunc(s string, f func(rune) bool) string
    • func TrimLeftFunc(s string, f func(rune) bool) string
    • func TrimRightFunc(s string, f func(rune) bool) string
    • func TrimSpace(s string) string
    • func TrimPrefix(s, prefix string) string
    • func TrimSuffix(s, suffix string) string
package main
import (
	"strings"
	"fmt"
)
func main() {
	// 去除字串兩端指定字符
	str1 := strings.Trim("!!!abc!!!def!!!", "!")
	fmt.Println(str1) // abc!!!def
	// 去除字串左端指定字符
	str2 := strings.TrimLeft("!!!abc!!!def!!!", "!")
	fmt.Println(str2) // abc!!!def!!!
	// 去除字串右端指定字符
	str3 := strings.TrimRight("!!!abc!!!def!!!", "!")
	fmt.Println(str3) // !!!abc!!!def
	// // 去除字串兩端空格
	str4 := strings.TrimSpace("   abc!!!def ")
	fmt.Println(str4) // abc!!!def

	// 按照方法定義規則,去除字串兩端符合規則內容
	str5 := strings.TrimFunc("!!!abc!!!def!!!", custom)
	fmt.Println(str5) // abc!!!def
	// 按照方法定義規則,去除字串左端符合規則內容
	str6 := strings.TrimLeftFunc("!!!abc!!!def!!!", custom)
	fmt.Println(str6) // abc!!!def!!!
	//  按照方法定義規則,去除字串右端符合規則內容
	str7 := strings.TrimRightFunc("!!!abc!!!def!!!", custom)
	fmt.Println(str7) // !!!abc!!!def

	// 取出字串開頭的指定字串
	str8 := strings.TrimPrefix("lnj-book.avi", "lnj-")
	fmt.Println(str8) // book.avi

	// 取出字串結尾的指定字串
	str9 := strings.TrimSuffix("lnj-book.avi", ".avi")
	fmt.Println(str9) // lnj-book
}

正則運算式

  • 正則運算式是對字串操作的一種邏輯公式,就是用事先定義好的一些特定字符、及這些特定字符的組合,組成一個“規則字串”,這個“規則字串”用來表達對字串的一種過濾邏輯,
  • 相關規則標準詳見
    • 百度百科
    • Go語言官方檔案regexp包
  • Go語言中正則運算式使用步驟
    • 1.創建一個正則運算式匹配規則物件
    • 2.利用正則運算式匹配規則物件匹配指定字串
package main
import (
	"strings"
	"fmt"
)
func main() {
	// 創建一個正則運算式匹配規則物件
	// reg := regexp.MustCompile(規則字串)
	// 利用正則運算式匹配規則物件匹配指定字串
	// 會將所有匹配到的資料放到一個字串切片中回傳
	// 如果沒有匹配到資料會回傳nil
	// res := reg.FindAllString(需要匹配的字串, 匹配多少個)

	str := "Hello 李南江 1232"
	reg := regexp.MustCompile("2")
	res := reg.FindAllString(str, -1)
	fmt.Println(res) // [2 2]
	res = reg.FindAllString(str, 1)
	fmt.Println(res) // [2]
}
  • 匹配電話號碼
package main
import (
	"strings"
	"fmt"
)
func main() {
	res2 := findPhoneNumber("13554499311")
	fmt.Println(res2) // true

	res2 = findPhoneNumber("03554499311")
	fmt.Println(res2) // false

	res2 = findPhoneNumber("1355449931")
	fmt.Println(res2) // false
}
func findPhoneNumber(str string) bool {
	// 創建一個正則運算式匹配規則物件
	reg := regexp.MustCompile("^1[1-9]{10}")
	// 利用正則運算式匹配規則物件匹配指定字串
	res := reg.FindAllString(str, -1)
	if(res == nil){
		return  false
	}
	return  true
}
  • 匹配Email
package main
import (
	"strings"
	"fmt"
)
func main() {
	res2 = findEmail("123@qq.com")
	fmt.Println(res2) // true

	res2 = findEmail("ab?de@qq.com")
	fmt.Println(res2) // false

	res2 = findEmail("123@qqcom")
	fmt.Println(res2) // false
}
func findEmail(str string) bool {
	reg := regexp.MustCompile("^[a-zA-Z0-9_]+@[a-zA-Z0-9]+\\.[a-zA-Z0-9]+")
	res := reg.FindAllString(str, -1)
	if(res == nil){
		return  false
	}
	return  true
}

時間和日期函式

  • 獲取當前時間
package main

import (
	"fmt"
	"time"
)
func main()  {
	var t time.Time = time.Now()
	// 2018-09-27 17:25:11.653198 +0800 CST m=+0.009759201
	fmt.Println(t)
}
  • 獲取年月日時分秒
package main

import (
	"fmt"
	"time"
)
func main()  {
	var t time.Time = time.Now()
	fmt.Println(t.Year())
	fmt.Println(t.Month())
	fmt.Println(t.Day())
	fmt.Println(t.Hour())
	fmt.Println(t.Minute())
	fmt.Println(t.Second())
}
  • 格式化時間
package main

import (
	"fmt"
	"time"
)
func main()  {
	var t time.Time = time.Now()
	fmt.Printf("當前的時間是: %d-%d-%d %d:%d:%d\n", t.Year(), 
		t.Month(), t.Day(), t.Hour(), t.Minute(), t.Second())

	var dateStr = fmt.Sprintf("%d-%d-%d %d:%d:%d", t.Year(), 
		t.Month(), t.Day(), t.Hour(), t.Minute(), t.Second())
fmt.Println("當前的時間是:", dateStr)
}
package main

import (
	"fmt"
	"time"
)
func main()  {
	var t time.Time = time.Now()
	// 2006/01/02 15:04:05這個字串是Go語言規定的, 各個數字都是固定的
	// 字串中的各個數字可以只有組合, 這樣就能按照需求回傳格式化好的時間
	str1 := t.Format("2006/01/02 15:04:05")
	fmt.Println(str1)
	str2 := t.Format("2006/01/02")
	fmt.Println(str2)
	str3 := t.Format("15:04:05")
	fmt.Println(str3)
}
  • 時間常量
    • 一般用于指定時間單位, 和休眠函式配合使用
    • 例如: 100毫秒, 100 *time.Millisecond
const (
	Nanosecond  Duration = 1    // 納秒
	Microsecond          = 1000 * Nanosecond // 微秒
	Millisecond          = 1000 * Microsecond // 毫秒
	Second               = 1000 * Millisecond // 秒
	Minute               = 60 * Second // 分鐘
	Hour                 = 60 * Minute // 小時
)
package main

import (
	"fmt"
	"time"
)
func main()  {
	for{
		// 1秒鐘列印一次
		time.Sleep(time.Second * 1)
		// 0.1秒列印一次
		//time.Sleep(time.Second * 0.1)
		time.Sleep(time.Millisecond * 100)
		fmt.Println("Hello LNJ")
	}
}
  • 獲取當前時間戳
    • Unix秒
    • UnixNano納秒
    • 一般用于配合隨機函式使用, 作為隨機函式隨機種子
package main

import (
	"fmt"
	"time"
)

func main()  {
	t := time.Now()
	// 獲取1970年1月1日距離當前的時間(秒)
	fmt.Println(t.Unix())
	// 獲取1970年1月1日距離當前的時間(納秒)
	fmt.Println(t.UnixNano())
}
package main

import (
	"fmt"
	"math/rand"
	"time"
)
func main()  {
	// 創建亂數種子
	rand.Seed(time.Now().UnixNano())
	// 生成一個亂數
	fmt.Println(rand.Intn(10))
}

Go語言中呼叫C語言函式

  • 在Go語言開篇中我們已經知道, Go語言與C語言之間有著千絲萬縷的關系, 甚至被稱之為21世紀的C語言
  • 所以在Go與C語言互操作方面,Go更是提供了強大的支持,尤其是在Go中使用C,你甚至可以直接在Go源檔案中撰寫C代碼,這是其他語言所無法望其項背的
  • 格式:
    • 在import "C"之前通過單行注釋或者通過多行注釋撰寫C語言代碼
    • 在import "C"之后撰寫Go語言代碼
    • 在Go語言代碼中通過C.函式名稱() 呼叫C語言代碼即可
    • 注意: import "C"和前面的注釋之間不能出現空行或其它內容, 必須緊緊相連
package main
//#include <stdio.h>
//void say(){
// printf("Hello World\n");
//}
import "C"

func main()  {
	C.say()
}
package main
/*
#include <stdio.h>
void say(){
	printf("Hello World\n");
}
*/
import "C"

func main()  {
	C.say()
}
  • Go語言中沒有包名是C的包, 但是這個匯入會促使Go編譯器利用cgo工具預處理檔案
  • 在預處理程序中,cgo會產生一個臨時包, 這個包里包含了所有C函式和型別對應的Go語言宣告
  • 最終使得cgo工具可以通過一種特殊的方式來呼叫import "C"之前的C語言代碼
  • 常規問題:
    • 如果編譯報錯cc1.exe: sorry, unimplemented: 64-bit mode not compiled in
    • 說明你使用的是64的golang,而你使用的32位的MinGW,所以需要下載64位的mingw并配置環境變數
    • 下載地址: https://www.baidu.com/s?wd=sorry%2C%20unimplemented%3A%2064-bit%20mode%20not%20compiled%20in

C語言中呼叫Go語言函式(很少使用)

  • 在Go代碼中通過 //export Go函式名稱匯出Go的函式名稱
  • 在C代碼中通過 extern 回傳值型別 Go函式名稱(形參串列); 宣告Go中匯出的函式名稱
  • 注意: //export Go函式名稱extern 回傳值型別 Go函式名稱(形參串列);不能在同一個檔案中
package main

import "C"
import "fmt"
// 匯出Go函式宣告, 給C使用
//export GoFunction
func GoFunction() {
	fmt.Println("GoFunction!!!")
}
package main
/*
#include <stdio.h>
// 宣告Go中的函式
extern void GoFunction();

void CFunction() {
        printf("CFunction!\n");
        GoFunction();
}
*/
import "C"

func main()  {
	C.CFunction()
}
  • 由于不在同一個檔案, 所以需要通過go build或者go install同時編譯多個檔案

  • Go中使用C語言的型別
  • 基本資料型別
    • 在Go中可以用如下方式訪問C原生的數值型別:
C.char,
C.schar (signed char),
C.uchar (unsigned char),
C.short,
C.ushort (unsigned short),
C.int, C.uint (unsigned int),
C.long,
C.ulong (unsigned long),
C.longlong (long long),
C.ulonglong (unsigned long long),
C.float,
C.double
  • Go的數值型別與C中的數值型別不是一一對應的,因此在使用對方型別變數時必須顯式轉型操作
package main
/*
#include <stdio.h>
int num = 123;
float value = 3.14;
char ch = 'N';
*/
import "C"
import "fmt"

func main()  {
	var num1 C.int = C.num
	fmt.Println(num1)
	var num2 int
	//num2 = num1 // 報錯
	num2 = int(num1)
	fmt.Println(num2)


	var value1 C.float = C.value
	fmt.Println(value1)
	var value2 float32 = float32(C.value)
	fmt.Println(value2)


	var ch1 C.char = C.ch
	fmt.Println(ch1)
	var ch2 byte = byte(C.ch)
	fmt.Println(ch2)
}

  • 字串型別
    • C語言中并不沒有字串型別,在C中用帶結尾’\0’的字符陣列來表示字串;而在Go中string型別是原生型別,因此在兩種語言互操作是必須要做字串型別的轉換
    • C字串轉換Go字串: C.GoString(str)
    • Go字串轉換C字串: C.CString(str)
package main
/*
#include <stdio.h>
char *str = "www.it666.com";
void say(char *name){
	printf("my name is %s", name);
}
*/
import "C"
import (
	"fmt"
	"unsafe"
)

func main()  {
	// 1.C語言字串轉換Go語言字串
	str1 := C.str
	str2 := C.GoString(str1)
	fmt.Println(str2)

	// 2.Go語言字串轉換C語言字串
	str := "lnj"
	cs := C.CString(str)
	C.say(cs)
	// 注意: 轉換后所得到的C字串cs并不能由Go的gc所管理,我們必須手動釋放cs所占用的記憶體
	C.free(unsafe.Pointer(cs))
}

  • 指標型別
    • 原生數值型別的指標型別可按Go語法在型別前面加上*,例如:var p *C.int,
    • 而void*比較特殊,用Go中的unsafe.Pointer表示,
      • unsafe.Pointer:通用指標型別,用于轉換不同型別的指標,不能進行指標運算
    • uintptr:用于指標運算,GC 不把 uintptr 當指標,uintptr 無法持有物件,uintptr 型別的目標會被回收
    • 也就是說 unsafe.Pointer 是橋梁,可以讓任意型別的指標實作相互轉換,也可以將任意型別的指標轉換為uintptr 進行指標運算
package main
/*
#include <stdio.h>
int num = 123;
void *ptr = &num;
*/
import "C"
import (
	"fmt"
	"unsafe"
)

func main()  {
	// 這是一個C語言變數
	var num C.int = C.num
	// 這是一個C語言指標
	var p1 *C.int = &num
	fmt.Println(*p1)

	//var p2 *C.void = C.ptr // 報錯
	// 利用unsafe.Pointer接收viod *
	var p2 unsafe.Pointer = C.ptr
	// 將unsafe.Pointer轉換為具體型別
	var p3 *C.int = (*C.int)(p2)
	fmt.Println(*p3)
}

  • 列舉型別
    • C語言中的列舉型別在Go語言中的表現形式為C.enum_XXX
    • 訪問列舉和訪問普通變數無異, 直接通過C.列舉值即可
package main
/*
#include <stdio.h>
enum Gender {
   GenderMale,
   GenderFemale,
   GenderYao
};
*/
import "C"
import "fmt"

func main()  {
	var sex C.enum_Gender = C.GenderMale
	fmt.Println(sex)
	sex = C.GenderFemale
	fmt.Println(sex)
	sex = C.GenderYao
	fmt.Println(sex)
}

  • 結構體型別
    • C語言中的結構體型別在Go語言中的表現形式為C.struct_XXX
    • 訪問結構體 直接通過結構體變數.屬性名稱即可
package main
/*
#include <stdio.h>
struct Point {
    float x;
    float y;
};
*/
import "C"
import (
	"fmt"
)

func main()  {
	// 1.利用C的結構體型別創建結構體
	var cp C.struct_Point = C.struct_Point{6.6, 8.8}
	fmt.Println(cp)
	fmt.Printf("%T\n", cp)

	// 2.將C語言結構體轉換為Go語言結構體
	type GoPoint struct {
		x float32
		y float32
	}
	var gp GoPoint
	gp.x = float32(cp.x)
	gp.y = float32(cp.y)
	fmt.Println(gp)
}

  • 陣列型別
    • C語言中的陣列與Go語言中的陣列差異較大, C中的陣列是指標型別, Go中的陣列是值型別
    • 目前似乎無法直接顯式的在兩者之間進行轉型,官方檔案也沒有說明,
package main
/*
#include <stdio.h>
int cArray[5] = {1, 2, 3, 4, 5};
*/
import "C"
import "fmt"

func main()  {
	var cArr [5]C.int = C.cArray
	fmt.Println(cArr)
	fmt.Printf("%T\n", cArr)
}

  • 利用Go語言呼叫C語言函式, 實作無緩沖區輸入
    • 請在終端運行
package main
/*
#include <stdio.h>
char lowerCase(char ch){
    // 1.判斷當前是否是小寫字母
    if(ch >= 'a' && ch <= 'z'){
        return ch;
    }
    // 注意點: 不能直接撰寫else, 因為執行到else不一定是一個大寫字母
    else if(ch >= 'A' && ch <= 'Z'){
        return ch + ('a' - 'A');
    }
    return ' ';
}
char getCh(){
    // 1.接收用戶輸入的資料
    char ch;
    scanf("%c", &ch);
    setbuf(stdin, NULL);
    // 2.大小寫轉換
    ch = lowerCase(ch);
    // 3.回傳轉換好的字符
    return ch;
}
 */
import "C"
import "fmt"

func main()  {
	for{
		fmt.Println("請輸入w a s d其中一個字符, 控制小人走出迷宮")
		var ch byte = byte(C.getCh())
		fmt.Printf("%c", ch)
	}
}

檔案的打開和關閉

  • 和C語言一樣, Go語言中操作檔案也是通過一個FILE結構體
type file struct {
	pfd     poll.FD
	name    string
	dirinfo *dirInfo 
}
type File struct {
	*file // os specific
}
  • Open函式
    • func Open(name string) (file *File, err error)
    • Open打開一個檔案用于讀取
  • Close函式
    • func (f *File) Close() error
    • Close關閉檔案f
package main
import (
	"fmt"
	"os"
)

func main() {
	// 1.打開一個檔案
	// 注意: 檔案不存在不會創建, 會報錯
	// 注意: 通過Open打開只能讀取, 不能寫入
	fp, err := os.Open("d:/lnj.txt")
	if err != nil{
		fmt.Println(err)
	}else{
		fmt.Println(fp)
	}

	// 2.關閉一個檔案
	defer func() {
		err = fp.Close()
		if err != nil {
			fmt.Println(err)
		}
	}()
}

檔案讀取

  • Read函式(不帶緩沖區去讀)
    • func (f *File) Read(b []byte) (n int, err error)
    • Read方法從f中讀取最多len(b)位元組資料并寫入b,
package main

import (
	"fmt"
	"io"
	"os"
)

func main() {
	// 1.打開一個檔案
	// 注意: 檔案不存在不會創建, 會報錯
	// 注意: 通過Open打開只能讀取, 不能寫入
	fp, err := os.Open("d:/lnj.txt")
	if err != nil{
		fmt.Println(err)
	}else{
		fmt.Println(fp)
	}

	// 2.關閉一個檔案
	defer func() {
		err = fp.Close()
		if err != nil {
			fmt.Println(err)
		}
	}()

	// 3.讀取指定指定位元組個資料
	// 注意點: \n也會被讀取進來
	//buf := make([]byte, 50)
	//count, err := fp.Read(buf)
	//if err != nil {
	//	fmt.Println(err)
	//}else{
	//	fmt.Println(count)
	//	fmt.Println(string(buf))
	//}

	// 4.讀取檔案中所有內容, 直到檔案末尾為止
	buf := make([]byte, 10)
	for{
		count, err := fp.Read(buf)
		// 注意: 這行代碼要放到判斷EOF之前, 否則會出現少讀一行情況
		fmt.Print(string(buf[:count]))
		if err == io.EOF {
			break
		}
	}
}
  • ReadBytes和ReadString函式(帶緩沖區去讀)
    • func (b *Reader) ReadBytes(delim byte) (line []byte, err error)
    • ReadBytes讀取直到第一次遇到delim位元組
    • func (b *Reader) ReadString(delim byte) (line string, err error)
    • ReadString讀取直到第一次遇到delim位元組
package main

import (
	"bufio"
	"fmt"
	"io"
	"os"
)

func main() {
	// 1.打開一個檔案
	// 注意: 檔案不存在不會創建, 會報錯
	// 注意: 通過Open打開只能讀取, 不能寫入
	fp, err := os.Open("d:/lnj.txt")
	if err != nil{
		fmt.Println(err)
	}else{
		fmt.Println(fp)
	}

	// 2.關閉一個檔案
	defer func() {
		err = fp.Close()
		if err != nil {
			fmt.Println(err)
		}
	}()

	// 3.讀取一行資料
	// 創建讀取緩沖區, 默認大小4096
	//r :=bufio.NewReader(fp)
	//buf, err := r.ReadBytes('\n')
	//buf, err := r.ReadString('\n')
	//if err != nil{
	//	fmt.Println(err)
	//}else{
	//	fmt.Println(string(buf))
	//}

	// 4.讀取檔案中所有內容, 直到檔案末尾為止
	r :=bufio.NewReader(fp)
	for{
		//buf, err := r.ReadBytes('\n')
		buf, err := r.ReadString('\n')
		fmt.Print(string(buf))
		if err == io.EOF{
			break
		}
	}
}
  • ReadFile函式
    • func ReadFile(filename string) ([]byte, error)
    • 從filename指定的檔案中讀取資料并回傳檔案的所有內容
    • 不適合大檔案讀取
package main

import (
	"fmt"
	"io/ioutil"
)

func main() {

	filePath := "d:/lnj.txt"
	buf, err := ioutil.ReadFile(filePath)
	if err !=nil {
		fmt.Println(err)
	}else{
		fmt.Println(string(buf))
	}
}

檔案創建和寫入

  • Create函式
    • func Create(name string) (file *File, err error)
    • Create采用模式0666(任何人都可讀寫,不可執行)創建一個名為name的檔案
    • 如果檔案存在會覆寫原有檔案
  • Write函式
    • func (f *File) Write(b []byte) (n int, err error)
    • 將指定位元組陣列寫入到檔案中
  • WriteString函式
    • func (f *File) WriteString(s string) (ret int, err error)
    • 將指定字串寫入到檔案中
package main

import (
	"fmt"
	"os"
)

func main() {

	// 1.創建一個檔案
	fp, err := os.Create("d:/lnj.txt")
	if err != nil{
		fmt.Println(err)
	}
	// 2.關閉打開的檔案
	defer func() {
		err := fp.Close()
		if err != nil {
			fmt.Println(err)
		}
	}()
	// 2.往檔案中寫入資料
	// 注意: Windows換行是\r\n
	bytes := []byte{'l','n','j','\r','\n'}
	fp.Write(bytes)
	
	fp.WriteString("www.it666.com\r\n")
	fp.WriteString("www.itzb.com\r\n")
	// 注意: Go語言采用UTF-8編碼, 一個中文占用3個位元組
	fp.WriteString("李南江")
}
  • OpenFile函式
    • func OpenFile(name string, flag int, perm FileMode) (file *File, err error)
    • 第一個引數: 打開的路徑
    • 第二個引數: 打開的模式
    const (
      O_RDONLY int = syscall.O_RDONLY // 只讀模式打開檔案
      O_WRONLY int = syscall.O_WRONLY // 只寫模式打開檔案
      O_RDWR   int = syscall.O_RDWR   // 讀寫模式打開檔案
      O_APPEND int = syscall.O_APPEND // 寫操作時將資料附加到檔案尾部
      O_CREATE int = syscall.O_CREAT  // 如果不存在將創建一個新檔案
      O_EXCL   int = syscall.O_EXCL   // 和O_CREATE配合使用,檔案必須不存在
      O_SYNC   int = syscall.O_SYNC   // 打開檔案用于同步I/O
      O_TRUNC  int = syscall.O_TRUNC  // 如果可能,打開時清空檔案
    )
    
    • 第三個引數: 指定權限
      • 0沒有任何權限
      • 1.執行權限(如果是可執行程式, 可以運行)
      • 2.寫權限
      • 3.寫權限和執行權限
      • 4.讀權限
      • 5.讀權限和執行權限
      • 6.讀權限和寫權限
      • 7.讀權限和寫權限以及執行權限
    const (
      // 單字符是被String方法用于格式化的屬性縮寫,
      ModeDir        FileMode = 1 << (32 - 1 - iota) // d: 目錄
      ModeAppend                                     // a: 只能寫入,且只能寫入到末尾
      ModeExclusive                                  // l: 用于執行
      ModeTemporary                                  // T: 臨時檔案(非備份檔案)
      ModeSymlink                                    // L: 符號鏈接(不是快捷方式檔案)
      ModeDevice                                     // D: 設備
      ModeNamedPipe                                  // p: 命名管道(FIFO)
      ModeSocket                                     // S: Unix域socket
      ModeSetuid                                     // u: 表示檔案具有其創建者用戶id權限
      ModeSetgid                                     // g: 表示檔案具有其創建者組id的權限
      ModeCharDevice                                 // c: 字符設備,需已設定ModeDevice
      ModeSticky                                     // t: 只有root/創建者能洗掉/移動檔案
      // 覆寫所有型別位(用于通過&獲取型別位),對普通檔案,所有這些位都不應被設定
      ModeType = ModeDir | ModeSymlink | ModeNamedPipe | ModeSocket | ModeDevice
      ModePerm FileMode = 0777 // 覆寫所有Unix權限位(用于通過&獲取型別位)
    )
    
  • 不帶緩沖區寫入
package main

import (
	"fmt"
	"os"
)

func main() {

	// 注意點: 第三個引數在Windows沒有效果
	// -rw-rw-rw- (666)   所有用戶都有檔案讀、寫權限,
	//-rwxrwxrwx (777)  所有用戶都有讀、寫、執行權限,
	// 1.打開檔案
	//fp, err := os.OpenFile("d:/lnj.txt", os.O_CREATE|os.O_RDWR, 0666)
	fp, err := os.OpenFile("d:/lnj.txt", os.O_CREATE|os.O_APPEND, 0666)
	if err != nil {
		fmt.Println(err)
	}
	// 2.關閉打開的檔案
	defer func() {
		err := fp.Close()
		if err != nil {
			fmt.Println(err)
		}
	}()

	// 注意點:
	// 如果O_RDWR模式打開, 被打開檔案已經有內容, 會從最前面開始覆寫
	// 如果O_APPEND模式打開, 被打開檔案已經有內容, 會從在最后追加
	// 3.往檔案中寫入資料
	bytes := []byte{'l','n','j','\r','\n'}
	fp.Write(bytes)
	fp.WriteString("www.it666.com\r\n")
}
  • 帶緩沖區寫入
package main

import (
	"bufio"
	"fmt"
	"os"
)

func main() {

	// 1.打開檔案
	fp, err := os.OpenFile("d:/lnj.txt", os.O_CREATE|os.O_APPEND, 0666)
	if err != nil {
		fmt.Println(err)
	}
	// 2.關閉打開的檔案
	defer func() {
		err := fp.Close()
		if err != nil {
			fmt.Println(err)
		}
	}()

	// 3.創建緩沖區
	w := bufio.NewWriter(fp)

	// 4.寫入資料到緩沖區
	bytes := []byte{'l','n','j','\r','\n'}
	w.Write(bytes)
	w.WriteString("www.it666.com\r\n")

	// 5.將緩沖區中的資料重繪到檔案
	w.Flush()
}
  • WriteFile函式
package main

import (
	"fmt"
	"io/ioutil"
)

func main() {

	// 1.寫入資料到指定檔案
	data := []byte{'l','n','j','\r','\n'}
	err := ioutil.WriteFile("d:/abc.txt", data, 0666)
	if err != nil {
		fmt.Println(err)
	}else{
		fmt.Println("寫入成功")
	}
}

判斷檔案是否存在

  • Stat函式
    • func Stat(name string) (fi FileInfo, err error)
  • 回傳值: FileInfo
type FileInfo interface {
    Name() string       // 檔案的名字(不含擴展名)
    Size() int64        // 普通檔案回傳值表示其大小;其他檔案的回傳值含義各系統不同
    Mode() FileMode     // 檔案的模式位
    ModTime() time.Time // 檔案的修改時間
    IsDir() bool        // 等價于Mode().IsDir()
    Sys() interface{}   // 底層資料來源(可以回傳nil)
}
  • 回傳值: error
    • 回傳值error等于nil,代表檔案存在
    • 回傳值error不等于nil, 可以進一步通過IsNotExist判斷, 如果回傳true代表檔案不存在
    • 回傳值error如果回傳其它錯誤, 則不確定檔案是否存在
package main

import (
	"fmt"
	"os"
)

func main() {

	info, err := os.Stat("d:/lnj.txt")
	if err == nil {
		fmt.Println("檔案存在")
		fmt.Println(info.Name())
	}else if os.IsNotExist(err) {
		fmt.Println("檔案不存在")
	}else{
		fmt.Println("不確定")
	}
}

練習

  • 將一個文本檔案拷貝到另外一個檔案中
    • 嘗試用上面學習的其它方法實作下
package main

import (
	"fmt"
	"io/ioutil"
)

func main() {

	// 1.讀取一個檔案
	buf, err := ioutil.ReadFile("d:/lnj.txt")
	if err != nil {
		fmt.Println(err)
		return
	}
	// 2.寫入讀取的資料到另一個檔案
	err =ioutil.WriteFile("d:/abc.txt", buf, 0666)
	if err != nil {
		fmt.Println(err)
		return
	}
	fmt.Println("拷貝完成")

}
  • 將一個圖片/視頻檔案拷貝到另一個檔案
package main

import (
	"bufio"
	"fmt"
	"io"
	"os"
)

func main() {

	// 1.定義拷貝檔案的路徑
	scrPath := "D:/a.png"
	destPath := "E:/b.png"
	// 2.打開被拷貝檔案
	fr, err := os.Open(scrPath)
	if err != nil {
		fmt.Println(err)
		return
	}
	// 3.關閉打開檔案
	defer func() {
		err := fr.Close()
		if err != nil{
			fmt.Println(err)
		}
	}()
	// 4.創建讀取緩沖區
	r := bufio.NewReader(fr)

	// 1.創建寫入檔案
	fw, err := os.Create(destPath)
	if err != nil {
		fmt.Println(err)
		return
	}
	// 2.關閉打開檔案
	defer func() {
		err := fw.Close()
		if err != nil{
			fmt.Println(err)
		}
	}()
	// 3.創建寫入緩沖區
	w := bufio.NewWriter(fw)
	// 4.利用系統copy函式完成拷貝
	count, err := io.Copy(w, r)
	if err != nil {
		fmt.Println(err)
		return
	}
	fmt.Println(count)
	fmt.Println("拷貝完成")
}
  • 自己查檔案實作遍歷檔案夾
    • 例如: 給一個檔案夾路徑, 獲取該檔案夾下所有檔案, 并將所有檔案路徑保存到切片中

并發編程基本概念

  • 學習并發編程之前我們需要腦補幾個基礎知識和思考一個問題
    • 什么是串行?
    • 什么是并行?
    • 什么是并發?
    • 什么是程式?
    • 什么是行程?
    • 什么是執行緒?
    • 什么是協程?

什么是串行?

  • 串行就是按順序執行, 就好比銀行只有1個視窗, 有3個人要辦事, 那么必須排隊, 只有前面的人辦完走人, 才能輪到你
  • 在計算機中, 同一時刻, 只能有一條指令, 在一個CPU上執行, 后面的指令必須等到前面指令執行完才能執行, 就是串行
    +

什么是并行?

  • 并行就是同時執行, 就好比銀行有3個視窗, 有3個人要辦事, 只需要到空視窗即可立即辦事.
  • 在計算機中, 同一時刻, 有多條指令, 在多個CPU上執行, 就是并行
  • 從以上分析不難看出, 并行的速度優于串行
    +

什么是并發?

  • 并發是偽并行, 就好比銀行只有1個視窗, 有3個人要辦事, 那么沒輪到后面的人時, 后面的人可以用拖鞋先排隊, 去吃個早餐,買個東西啥的, 感覺差不多要到自己時再回來辦事
  • 在計算機中, ***同一時刻, 只能有一條指令, 在一個CPU上執行, 但是CPU會快速的在多條指令之間輪詢執行***就是并發
  • 并行和并發的區別就好比古代的三妻四妾(名正言順, 光明正大)和現代三妻四妾(抽空幽會, 小三小四)
  • 總結:
    • 多執行緒程式在單核上運行, 就是并發
    • 多執行緒程式在多核上運行,就是并行

什么是程式?

  • 程式是指將編譯型語言撰寫好的代碼通過編譯工具編譯之后存盤在硬碟上的一個二進制檔案, 會占用磁盤空間, 但不會占用系統資源
    • 例如我們通過C++撰寫了一個聊天程式, 然后通過C++編譯器將撰寫好的代碼編譯成一個二進制的檔案, 那么這個二進制的檔案就是一個程式

什么是行程?

  • 行程是指程式在作業系統中的一次執行程序, 是系統進行資源分配和調度的基本單位
  • 例如:
    • 啟動記事本這個程式, 在系統中就會創建一個記事本行程
    • 再次啟動記事本這個程式, 又會在系統中創建一個記事本行程
  • 程式和行程的關系就好比劇本和演出的關系
    • 劇本對應程式, 演出對應行程. 同一個劇本可以在多個舞臺同時演出互不影響, 同一個程式可以在系統中開啟多個行程互不影響
  • 所以程式和行程的關系是1:N, 所以多個行程的空間是獨立的

什么是執行緒?

  • 執行緒是指行程中的一個執行實體, 是程式執行的最小單元, 它是比行程更小的能獨立運行的基本單位
  • 一個行程中至少有一個執行緒, 這個執行緒我們稱之為主執行緒
  • 一個行程中除了主執行緒以外, 我們還可以創建和銷毀多個執行緒
  • 例如:
    • 啟動迅雷這個程式, 系統會創建一個迅雷行程, 并且默認會有一個主執行緒, 用于執行迅雷默認的業務邏輯
    • 當我們利用迅雷下載多個任務的時候, 會發現多個任務都在同時下載, 此時為了能夠同時執行下載操作, 迅雷就會創建多個執行緒, 將不同的下載任務放到不同的執行緒中執行

什么是協程?

  • 協程是一種用戶態的輕量級執行緒,又稱微執行緒,英文名Coroutine
  • 與傳統的系統級別行程和執行緒相比, 協程最大的優勢在于"輕量級". 可以輕松創建上萬個不會導致系統資源衰竭. 而執行緒和行程通常很難超過1萬個.這也是協程稱之為"輕量級執行緒"的原因
  • 一個執行緒中可以有任意多個協程, 但某一時刻只能有一個協程在運行, 多個協程分享所在執行緒分配到的計算機資源
  • 在協程中, 呼叫一個任務就像呼叫一個函式一樣, 消耗系統資源極少, 但能達到行程、執行緒相同的并發效果

Go并發

  • Go在語言級別支持協程(多數語言在語法層面并不直接支持協程), 叫做goroutine.

  • 人們把Go語言稱之為21世紀的C語言. 第一是因為Go語言設計簡單, 第二是因為21世紀最重要的就是并行程式設計.而Go從語言層面就支持并發和并行

  • Go并發小案例

package main

import (
	"fmt"
	"time"
)

func sing()  {
	for i:=0; i< 10; i++{
		fmt.Println("我在唱歌")
		time.Sleep(time.Millisecond)
	}
}
func dance() {
	for i:=0; i< 10; i++{
		fmt.Println("我在跳舞---")
		time.Sleep(time.Millisecond)
	}
}

func main() {
	// 串行: 必須先唱完歌才能跳舞
	//sing()
	//dance()

	// 并行: 可以邊唱歌, 邊跳舞
	// 注意點: 主執行緒不能死, 否則程式就退出了
	go sing() // 開啟一個協程
	go dance() // 開啟一個協程
	for{
		;
	}
}
  • runtime包中常用的函式
    • Gosched:使當前go程放棄處理器,以讓其它go程運行
      package main
      import (
      	"fmt"
      	"runtime"
      )
      
      func sing()  {
      	for i:=0; i< 10; i++{
      		fmt.Println("我在唱歌")
      		// Gosched使當前go程放棄處理器,以讓其它go程運行,
      		// 它不會掛起當前go程,因此當前go程未來會恢復執行
      		runtime.Gosched()
      	}
      }
      func dance() {
      	for i:=0; i< 10; i++{
      		fmt.Println("我在跳舞---")
      		runtime.Gosched()
      	}
      }
      
      func main() {
      
      	go sing()
      	go dance()
      	for{
      		;
      	}
      }
    
    • Goexit: 終止呼叫它的go程, 其它go程不會受影響
      package main
      import (
      	"fmt"
      	"runtime"
      )
      
      func main() {
      	go func() {
      		fmt.Println("123")
      		// 退出當前協程
      		//runtime.Goexit()
      		// 退出當前函式
      		//return
      		test()
      		fmt.Println("456")
      	}()
      	for{
      		;
      	}
      }
      
      func test()  {
      	fmt.Println("abc")
      	// 只會結束當前函式, 協程中的其它代碼會繼續執行
      	//return
      	// 會結束整個協程, Goexit之后整個協程中的其它代碼不會執行
      	runtime.Goexit()
      	fmt.Println("def")
      }
    
    • NumCPU: 回傳本地機器的邏輯CPU個數
      package main
      import (
      	"fmt"
      	"runtime"
      )
      func main() {
      	num := runtime.NumCPU()
      	fmt.Println(num)
      }
    
    • GOMAXPROCS: 設定可同時執行的最大CPU數,并回傳先前的設定
      • Go語言1.8之前, 需要我們手動設定
      • Go語言1.8之后, 不需要我們手動設定
      func main() {
      	// 獲取帶來了CPU個數
      	num := runtime.NumCPU()
      	// 設定同時使用CPU個數
      	runtime.GOMAXPROCS(num)
      }
    

多執行緒同步問題

  • 互斥鎖
    • 互斥鎖的本質是當一個goroutine訪問的時候, 其它goroutine都不能訪問
    • 這樣就能實作資源同步, 但是在避免資源競爭的同時也降低了程式的并發性能. 程式由原來的并發執行變成了串行
  • 案例:
    • 有一個列印函式, 用于逐個列印字串中的字符, 有兩個人都開啟了goroutine去列印
    • 如果沒有添加互斥鎖, 那么兩個人都有機會輸出自己的內容
    • 如果添加了互斥鎖, 那么會先輸出某一個的, 輸出完畢之后再輸出另外一個人的
package main
import (
	"fmt"
	"sync"
	"time"
)
// 創建一把互斥鎖
var lock sync.Mutex

func printer(str string)  {
	// 讓先來的人拿到鎖, 把當前函式鎖住, 其它人都無法執行
	// 上廁所關門
	lock.Lock()
	for _, v := range str{
		fmt.Printf("%c", v)
		time.Sleep(time.Millisecond * 500)
	}
	// 先來的人執行完畢之后, 把鎖釋放掉, 讓其它人可以繼續使用當前函式
	// 上廁所開門
	lock.Unlock()
}
func person1()  {
	printer("hello")
}
func person2()  {
	printer("world")
}
func main() {
	go person1()
	go person2()
	for{
		;
	}
}

生產者消費者問題

  • 所謂的生產者消費者模型就是
    • 某個模塊(函式)負責生產資料, 這些資料由另一個模塊來負責處理
    • 一般生產者消費者模型包含三個部分"生產者"、“緩沖區”、“消費者”
  • 為什么生產者消費者模型要含三個部分? 直接生產和消費不行么?
  • 一個案例說明一切
    • 生產者好比現實生活中的某個人
    • 緩沖區好比現實生活中的郵箱
    • 消費者好比現實生活中的郵遞員
  • 如果只有生產者和消費者, 那么相當于只有寫信的人和郵遞員, 那么如果將來過去的郵遞員離職了, 你想郵寄信件必須想辦法結識新的郵遞員(消費者發生變化, 會直接影響生產者, 耦合性太強)
  • 如果在生產者和消費者之間添加一個緩沖區, 那么就好比有了郵箱, 以后郵寄信件不是找郵遞員, 只需把信件投遞到郵箱中即可, 寫信的人不需要關心郵遞員是誰(解耦)
  • 如果只有生產者和消費者, 那么每個人郵寄信件都需要直接找郵遞員(1對1關系), 如果有10個人要郵寄信件, 那么郵遞員只能依次找到每個人, 然后才能取件(效率低下)
  • 如果在生產者和消費者之間添加一個緩沖區, 那么所有的人只需要將信件投遞到郵箱即可, 郵遞員不用關心有多少人要郵寄信件, 也不用依次取件, 只需要找到郵箱從郵箱中統一取件即可(效率提高)
  • 如果只有生產者和消費者, 那么如果郵寄信件太多郵遞員無法一次拿走, 這個時候非常難辦
  • 如果在生產者和消費者之間添加一個緩沖區, 那么如果信件太多可以先拿走一部分, 剩下的繼續放到郵箱中下次再拿
  • ... ...

生產者和消費者資源競爭問題

  • 例如生產比較慢, 而消費比較快, 就會導致消費者消費到錯誤資料
package main

import (
	"fmt"
	"math/rand"
	"sync"
	"time"
)
// 創建一把互斥鎖
var lock = sync.Mutex{}

// 定義緩沖區
var sce []int = make([]int, 10)

// 定義生產者
func producer(){
	// 加鎖, 注意是lock就是我們的鎖, 全域公用一把鎖
	lock.Lock()
	rand.Seed(time.Now().UnixNano())
	for i:=0;i<10;i++{
		num := rand.Intn(100)
		sce[i] = num
		fmt.Println("生產者生產了: ", num)
		time.Sleep(time.Millisecond * 500)
	}
	// 解鎖
	lock.Unlock()
}
// 定義消費者
func consumer()  {
	// 加鎖, 注意和生產者中用的是同一把鎖
	// 如果生產者中已加過了, 則阻塞直到解鎖后再重新加鎖
	lock.Lock()
	for i:=0;i<10;i++{
		num := sce[i]
		fmt.Println("---消費者消費了", num)
	}
	lock.Unlock()
}

func main() {
	go producer()
	go consumer()
	for{
		;
	}
}
  • 思考: 那如果是一對多, 或者多對多的關系, 上述代碼有問題么?

管道(Channel)

  • 上述實作并發的代碼中為了保持主執行緒不掛掉, 我們都會在最后寫上一個死回圈或者寫上一個定時器來實作等待goroutine執行完畢
  • 上述實作并發的代碼中為了解決生產者消費者資源同步問題, 我們利用加鎖來解決, 但是這僅僅是一對一的情況, 如果是一對多或者多對多, 上述代碼還是會出現問題
  • 綜上所述, 企業開發中需要一種更牛X的技術來解決上述問題, 那就是管道(Channel)

  • Channel的本質是一個佇列
  • Channel是執行緒安全的, 也就是自帶鎖定功能
  • Channel宣告和初始化
    • 宣告: var 變數名chan 資料型別
    • 初始化: mych := make(chan 資料型別, 容量)
    • Channel和切片還有字典一樣, 必須make之后才能使用
    • Channel和切片還有字典一樣, 是參考型別
package main
import "fmt"
func main() {
	// 1.宣告一個管道
	var mych chan int
	// 2.初始化一個管道
	mych = make(chan int, 3)
	// 3.查看管道的長度和容量
	fmt.Println("長度是", len(mych), "容量是", cap(mych))
	// 4.像管道中寫入資料
	mych<- 666
	fmt.Println("長度是", len(mych), "容量是", cap(mych))
	// 5.取出管道中寫入的資料
	num := <-mych
	fmt.Println("num = ", num)
	fmt.Println("長度是", len(mych), "容量是", cap(mych))
}

  • 注意點:
    • 管道中只能存放宣告的資料型別, 不能存放其它資料型別
    • 管道中如果已經沒有資料, 再取就會報錯
    • 如果管道中資料已滿, 再寫入就會報錯
package main

import "fmt"

func main() {
	// 1.宣告一個管道
	var mych chan int
	// 2.初始化一個管道
	mych = make(chan int, 3)

	// 注意點: 管道中只能存放宣告的資料型別, 不能存放其它資料型別
	//mych<-3.14

	// 注意點: 管道中如果已經沒有資料, 
	// 并且檢測不到有其它協程再往管道中寫入資料, 那么再取就會報錯
	//num = <-mych
	//fmt.Println("num = ", num)

	// 注意點: 如果管道中資料已滿, 再寫入就會報錯
	mych<- 666
	mych<- 777
	mych<- 888
	mych<- 999
}

  • 管道的關閉和遍歷
package main

import "fmt"

func main() {
	// 1.創建一個管道
	mych := make(chan int, 3)
	// 2.往管道中存入資料
	mych<-666
	mych<-777
	mych<-888
	// 3.遍歷管道
	// 第一次遍歷i等于0, len = 3,
	// 第二次遍歷i等于1, len = 2
	// 第三次遍歷i等于2, len = 1
	//for i:=0; i<len(mych); i++{
	//	fmt.Println(<-mych) // 輸出結果不正確
	//}

	// 3.寫入完資料之后先關閉管道
	// 注意點: 管道關閉之后只能讀不能寫
	close(mych)
	//mych<- 999 // 報錯

	// 4.遍歷管道
	// 利用for range遍歷, 必須先關閉管道, 否則會報錯
	//for value := range mych{
	//	fmt.Println(value)
	//}

	// close主要用途:
	// 在企業開發中我們可能不確定管道有還沒有有資料, 所以我們可能一直獲取
	// 但是我們可以通過ok-idiom模式判斷管道是否關閉, 如果關倍訓回傳false給ok
	for{
		if num, ok:= <-mych; ok{
			fmt.Println(num)
		}else{
			break;
		}
	}
	fmt.Println("資料讀取完畢")
}

  • Channel阻塞現象
    • 單獨在主執行緒中操作管道, 寫滿了會報錯, 沒有資料去獲取也會報錯
    • 只要在協程中操作管道過, 寫滿了就會阻塞, 沒有就資料去獲取也會阻塞
package main
import (
	"fmt"
	"time"
)
// 創建一個管道
var myCh = make(chan int, 5)
func demo()  {
	var myCh = make(chan int, 5)
	//myCh<-111
	//myCh<-222
	//myCh<-333
	//myCh<-444
	//myCh<-555
	//fmt.Println("我是第六次添加之前代碼")
	//myCh<-666
	//fmt.Println("我是第六次添加之后代碼")

	fmt.Println("我是第六次直接獲取之前代碼")
	<-myCh
	fmt.Println("我是第六次直接獲取之后代碼")
}
func test()  {
	//myCh<-111
	//myCh<-222
	//myCh<-333
	//myCh<-444
	//myCh<-555
	//fmt.Println("我是第六次添加之前代碼")
	//myCh<-666
	//fmt.Println("我是第六次添加之后代碼")

	//fmt.Println("我是第六次直接獲取之前代碼")
	//<-myCh
	//fmt.Println("我是第六次直接獲取之后代碼")
}
func example()  {
	time.Sleep(time.Second * 2)
	myCh<-666
}
func main() {
	// 1.同一個go程中操作管道
	// 寫滿了會報錯
	//myCh<-111
	//myCh<-222
	//myCh<-333
	//myCh<-444
	//myCh<-555
	//myCh<-666

	// 沒有了去取也會報錯
	//<-myCh

	// 2.在協程中操作管道
	// 寫滿了不會報錯, 但是會阻塞
	//go test()

	// 沒有了去取也不會報錯, 也會阻塞
	//go test()

	//go demo()
	//go demo()
	
	// 3.只要在協程中操作了管道, 就會發生阻塞現象
	go example()
	fmt.Println("myCh之前代碼")
	<-myCh
	fmt.Println("myCh之后代碼")

	//for{
	//	;
	//}
}

  • 利用Channel實作生產者消費者
package main

import (
	"fmt"
	"math/rand"
	"time"
)
// 定義緩沖區
var myCh = make(chan int, 5)
var exitCh = make(chan bool, 1)

// 定義生產者
func producer(){
	rand.Seed(time.Now().UnixNano())
	for i:=0;i<10;i++{
		num := rand.Intn(100)
		fmt.Println("生產者生產了: ", num)
		// 往管道中寫入資料
		myCh<-num
		//time.Sleep(time.Millisecond * 500)
	}
	// 生產完畢之后關閉管道
	close(myCh)
	fmt.Println("生產者停止生產")
}
// 定義消費者
func consumer()  {
	// 不斷從管道中獲取資料, 直到管道關閉位置
	for{
		if num, ok := <-myCh; !ok{
			break
		}else{
			fmt.Println("---消費者消費了", num)
		}
	}
	fmt.Println("消費者停止消費")
	exitCh<-true
}

func main() {
	go producer()
	go consumer()
	fmt.Println("exitCh之前代碼")
	<-exitCh
	fmt.Println("exitCh之后代碼") 
}

  • 無緩沖Channel
package main
import "fmt"
var myCh1 = make(chan int, 5)
var myCh2 = make(chan int, 0)
func main() {
	// 有緩沖管道
	// 只寫入, 不讀取不會報錯
	//myCh1<-1
	//myCh1<-2
	//myCh1<-3
	//myCh1<-4
	//myCh1<-5
	//fmt.Println("len =",len(myCh1), "cap =", cap(myCh1))

	// 無緩沖管道
	// 只有兩端同時準備好才不會報錯
	go func() {
		fmt.Println(<-myCh2)
	}()
	// 只寫入, 不讀取會報錯
	myCh2<-1
	//fmt.Println("len =",len(myCh2), "cap =", cap(myCh2))
	// 寫入之后在同一個執行緒讀取也會報錯
	//fmt.Println(<-myCh2)
	// 在主程中先寫入, 在子程中后讀取也會報錯
	//go func() {
	//	fmt.Println(<-myCh2)
	//}()
}

  • 無緩沖Channel和有緩沖Channel
    • 有緩沖管道具備異步的能力(寫幾個讀一個或讀幾個)
    • 無緩沖管道具備同步的能力(寫一個讀一個)
package main
import (
	"fmt"
	"math/rand"
	"time"
)
// 定義緩沖區
//var myCh = make(chan int, 0)
var myCh = make(chan int)
var exitCh = make(chan bool, 1)

// 定義生產者
func producer(){
	rand.Seed(time.Now().UnixNano())
	for i:=0;i<10;i++{
		num := rand.Intn(100)
		fmt.Println("生產者生產了: ", num)
		// 往管道中寫入資料
		myCh<-num
		//time.Sleep(time.Millisecond * 500)
	}
	// 生產完畢之后關閉管道
	close(myCh)
	fmt.Println("生產者停止生產")
}
// 定義消費者
func consumer()  {
	// 不斷從管道中獲取資料, 直到管道關閉位置
	for{
		if num, ok := <-myCh; !ok{
			break
		}else{
			fmt.Println("---消費者消費了", num)
		}
	}
	fmt.Println("消費者停止消費")
	exitCh<-true
}

func main() {
	go producer()
	go consumer()
	fmt.Println("exitCh之前代碼")
	<-exitCh
	fmt.Println("exitCh之后代碼")
}

IO的延遲說明:
看到的輸出結果和我們想象的不太一樣, 是因為IO輸出非常消耗性能, 輸出之后還沒來得及賦值可能就跑去執行別的協程了


  • 單向管道和雙向管道
    • 默認情況下所有管道都是雙向了(可讀可寫)
    • 但是在企業開發中, 我們經常需要用到將一個管道作為引數傳遞
    • 在傳遞的程序中希望對方只能單向使用, 要么只能寫,要么只能讀
  • 雙向管道
    • var myCh chan int = make(chan int, 0)
  • 單向管道
    • var myCh chan<- int = make(chan<- int, 0)
    • var myCh <-chan int = make(<-chan int, 0)
  • 注意點:
    • 雙向管道可以自動轉換為任意一種單向管道
    • 單向管道不能轉換為雙向管道
package main

import "fmt"

func main() {
	// 1.定義一個雙向管道
	var myCh chan int = make(chan int, 5)

	// 2.將雙向管道轉換單向管道
	var myCh2 chan<- int
	myCh2 = myCh
	fmt.Println(myCh2)
	var myCh3 <-chan int
	myCh3 = myCh
	fmt.Println(myCh3)

	// 3.雙向管道,可讀可寫
	myCh<-1
	myCh<-2
	myCh<-3
	fmt.Println(<-myCh)
	
	// 3.只寫管道,只能寫, 不能讀
	//	myCh2<-666
	//	fmt.Println(<-myCh2)

	// 4.指讀管道, 只能讀,不能寫
	fmt.Println(<-myCh3)
	//myCh3<-666
	
	// 注意點: 管道之間賦值是地址傳遞, 以上三個管道底層指向相同容器
}
  • 單向管道作為函式引數
package main
import (
	"fmt"
	"math/rand"
	"time"
)
// 定義生產者
func producer(myCh chan<- int){
	rand.Seed(time.Now().UnixNano())
	for i:=0;i<10;i++{
		num := rand.Intn(100)
		fmt.Println("生產者生產了: ", num)
		// 往管道中寫入資料
		myCh<-num
		//time.Sleep(time.Millisecond * 500)
	}
	// 生產完畢之后關閉管道
	close(myCh)
	fmt.Println("生產者停止生產")
}
// 定義消費者
func consumer(myCh <-chan int)  {
	// 不斷從管道中獲取資料, 直到管道關閉位置
	for{
		if num, ok := <-myCh; !ok{
			break
		}else{
			fmt.Println

轉載請註明出處,本文鏈接:https://www.uj5u.com/qukuanlian/287449.html

標籤:區塊鏈

上一篇:POS共識機制竟然漏洞這么多 | 分析POS共識機制的原理帶來的思考

下一篇:swarm主網BZZ挖礦:錢包如何添加BZZ合約?如何查錢包余額?

標籤雲
其他(157675) Python(38076) JavaScript(25376) Java(17977) C(15215) 區塊鏈(8255) C#(7972) AI(7469) 爪哇(7425) MySQL(7132) html(6777) 基礎類(6313) sql(6102) 熊猫(6058) PHP(5869) 数组(5741) R(5409) Linux(5327) 反应(5209) 腳本語言(PerlPython)(5129) 非技術區(4971) Android(4554) 数据框(4311) css(4259) 节点.js(4032) C語言(3288) json(3245) 列表(3129) 扑(3119) C++語言(3117) 安卓(2998) 打字稿(2995) VBA(2789) Java相關(2746) 疑難問題(2699) 细绳(2522) 單片機工控(2479) iOS(2429) ASP.NET(2402) MongoDB(2323) 麻木的(2285) 正则表达式(2254) 字典(2211) 循环(2198) 迅速(2185) 擅长(2169) 镖(2155) 功能(1967) .NET技术(1958) Web開發(1951) python-3.x(1918) HtmlCss(1915) 弹簧靴(1913) C++(1909) xml(1889) PostgreSQL(1872) .NETCore(1853) 谷歌表格(1846) Unity3D(1843) for循环(1842)

熱門瀏覽
  • JAVA使用 web3j 進行token轉賬

    最近新學習了下區塊鏈這方面的知識,所學不多,給大家分享下。 # 1. 關于web3j web3j是一個高度模塊化,反應性,型別安全的Java和Android庫,用于與智能合約配合并與以太坊網路上的客戶端(節點)集成。 # 2. 準備作業 jdk版本1.8 引入maven <dependency> < ......

    uj5u.com 2020-09-10 03:03:06 more
  • 以太坊智能合約開發框架Truffle

    前言 部署智能合約有多種方式,命令列的瀏覽器的渠道都有,但往往跟我們程式員的風格不太相符,因為我們習慣了在IDE里寫了代碼然后打包運行看效果。 雖然現在IDE中已經存在了Solidity插件,可以撰寫智能合約,但是部署智能合約卻要另走他路,沒辦法進行一個快捷的部署與測驗。 如果團隊管理的區塊節點多、 ......

    uj5u.com 2020-09-10 03:03:12 more
  • 谷歌二次驗證碼成為區塊鏈專用安全碼,你怎么看?

    前言 谷歌身份驗證器,前些年大家都比較陌生,但隨著國內互聯網安全的加強,它越來越多地出現在大家的視野中。 比較廣泛接觸的人群是國際3A游戲愛好者,游戲盜號現象嚴重+國外賬號安全應用廣泛,這類游戲一般都會要求用戶系結名為“兩步驗證”、“雙重驗證”等,平臺一般都推薦用谷歌身份驗證器。 后來區塊鏈業務風靡 ......

    uj5u.com 2020-09-10 03:03:17 more
  • 密碼學DAY1

    目錄 ##1.1 密碼學基本概念 密碼在我們的生活中有著重要的作用,那么密碼究竟來自何方,為何會產生呢? 密碼學是網路安全、資訊安全、區塊鏈等產品的基礎,常見的非對稱加密、對稱加密、散列函式等,都屬于密碼學范疇。 密碼學有數千年的歷史,從最開始的替換法到如今的非對稱加密演算法,經歷了古典密碼學,近代密 ......

    uj5u.com 2020-09-10 03:03:50 more
  • 密碼學DAY1_02

    目錄 ##1.1 ASCII編碼 ASCII(American Standard Code for Information Interchange,美國資訊交換標準代碼)是基于拉丁字母的一套電腦編碼系統,主要用于顯示現代英語和其他西歐語言。它是現今最通用的單位元組編碼系統,并等同于國際標準ISO/IE ......

    uj5u.com 2020-09-10 03:04:50 more
  • 密碼學DAY2

    ##1.1 加密模式 加密模式:https://docs.oracle.com/javase/8/docs/api/javax/crypto/Cipher.html ECB ECB : Electronic codebook, 電子密碼本. 需要加密的訊息按照塊密碼的塊大小被分為數個塊,并對每個塊進 ......

    uj5u.com 2020-09-10 03:05:42 more
  • NTP時鐘服務器的特點(京準電子)

    NTP時鐘服務器的特點(京準電子) NTP時鐘服務器的特點(京準電子) 京準電子官V——ahjzsz 首先對時間同步進行了背景介紹,然后討論了不同的時間同步網路技術,最后指出了建立全球或區域時間同步網存在的問題。 一、概 述 在通信領域,“同步”概念是指頻率的同步,即網路各個節點的時鐘頻率和相位同步 ......

    uj5u.com 2020-09-10 03:05:47 more
  • 標準化考場時鐘同步系統推進智能化校園建設

    標準化考場時鐘同步系統推進智能化校園建設 標準化考場時鐘同步系統推進智能化校園建設 安徽京準電子科技官微——ahjzsz 一、背景概述隨著教育事業的快速發展,學校建設如雨后春筍,隨之而來的學校教育、管理、安全方面的問題成了學校管理人員面臨的最大的挑戰,這些問題同時也是學生家長所擔心的。為了讓學生有更 ......

    uj5u.com 2020-09-10 03:05:51 more
  • 位元幣入門

    引言 位元幣基本結構 位元幣基礎知識 1)哈希演算法 2)非對稱加密技術 3)數字簽名 4)MerkleTree 5)哪有位元幣,有的是UTXO 6)位元幣挖礦與共識 7)區塊驗證(共識) 總結 引言 上一篇我們已經知道了什么是區塊鏈,此篇說一下區塊鏈的第一個應用——位元幣。其實先有位元幣,后有的區塊 ......

    uj5u.com 2020-09-10 03:06:15 more
  • 北斗對時服務器(北斗對時設備)電力系統應用

    北斗對時服務器(北斗對時設備)電力系統應用 北斗對時服務器(北斗對時設備)電力系統應用 京準電子科技官微(ahjzsz) 中國北斗衛星導航系統(英文名稱:BeiDou Navigation Satellite System,簡稱BDS),因為是目前世界范圍內唯一可以大面積提供免費定位服務的系統,所以 ......

    uj5u.com 2020-09-10 03:06:20 more
最新发布
  • web3 產品介紹:metamask 錢包 使用最多的瀏覽器插件錢包

    Metamask錢包是一種基于區塊鏈技術的數字貨幣錢包,它允許用戶在安全、便捷的環境下管理自己的加密資產。Metamask錢包是以太坊生態系統中最流行的錢包之一,它具有易于使用、安全性高和功能強大等優點。 本文將詳細介紹Metamask錢包的功能和使用方法。 一、 Metamask錢包的功能 數字資 ......

    uj5u.com 2023-04-20 08:46:47 more
  • Hyperledger Fabric 使用 CouchDB 和復雜智能合約開發

    在上個實驗中,我們已經實作了簡單智能合約實作及客戶端開發,但該實驗中智能合約只有基礎的增刪改查功能,且其中的資料管理功能與傳統 MySQL 比相差甚遠。本文將在前面實驗的基礎上,將 Hyperledger Fabric 的默認資料庫支持 LevelDB 改為 CouchDB 模式,以實作更復雜的資料... ......

    uj5u.com 2023-04-16 07:28:31 more
  • .NET Core 波場鏈離線簽名、廣播交易(發送 TRX和USDT)筆記

    Get Started NuGet You can run the following command to install the Tron.Wallet.Net in your project. PM> Install-Package Tron.Wallet.Net 配置 public reco ......

    uj5u.com 2023-04-14 08:08:00 more
  • DKP 黑客分析——不正確的代幣對比率計算

    概述: 2023 年 2 月 8 日,針對 DKP 協議的閃電貸攻擊導致該協議的用戶損失了 8 萬美元,因為 execute() 函式取決于 USDT-DKP 對中兩種代幣的余額比率。 智能合約黑客概述: 攻擊者的交易:0x0c850f,0x2d31 攻擊者地址:0xF38 利用合同:0xf34ad ......

    uj5u.com 2023-04-07 07:46:09 more
  • Defi開發簡介

    Defi開發簡介 介紹 Defi是去中心化金融的縮寫, 是一項旨在利用區塊鏈技術和智能合約創建更加開放,可訪問和透明的金融體系的運動. 這與傳統金融形成鮮明對比,傳統金融通常由少數大型銀行和金融機構控制 在Defi的世界里,用戶可以直接從他們的電腦或移動設備上訪問廣泛的金融服務,而不需要像銀行或者信 ......

    uj5u.com 2023-04-05 08:01:34 more
  • solidity簡單的ERC20代幣實作

    // SPDX-License-Identifier: GPL-3.0 pragma solidity >=0.7.0 <0.9.0; import "hardhat/console.sol"; //ERC20 同質化代幣,每個代幣的本質或性質都是相同 //ETH 是原生代幣,它不是ERC20代幣, ......

    uj5u.com 2023-03-21 07:56:29 more
  • solidity 參考型別修飾符memory、calldata與storage 常量修飾符C

    在solidity語言中 參考型別修飾符(參考型別為存盤空間不固定的數值型別) memory、calldata與storage,它們只能修飾參考型別變數,比如字串、陣列、位元組等... memory 適用于方法傳參、返參或在方法體內使用,使用完就會清除掉,釋放記憶體 calldata 僅適用于方法傳參 ......

    uj5u.com 2023-03-08 07:57:54 more
  • solidity注解標簽

    在solidity語言中 注釋符為// 注解符為/* 內容*/ 或者 是 ///內容 注解中含有這幾個標簽給予我們使用 @title 一個應該描述合約/介面的標題 contract, library, interface @author 作者的名字 contract, library, interf ......

    uj5u.com 2023-03-08 07:57:49 more
  • 評價指標:相似度、GAS消耗

    【代碼注釋自動生成方法綜述】 這些評測指標主要來自機器翻譯和文本總結等研究領域,可以評估候選文本(即基于代碼注釋自動方法而生成)和參考文本(即基于手工方式而生成)的相似度. BLEU指標^[^?88^^?^]^:其全稱是bilingual evaluation understudy.該指標是最早用于 ......

    uj5u.com 2023-02-23 07:27:39 more
  • 基于NOSTR協議的“公有制”版本的Twitter,去中心化社交軟體Damus

    最近,一個幽靈,Web3的幽靈,在網路游蕩,它叫Damus,這玩意詮釋了什么叫做病毒式營銷,滑稽的是,一個Web3產品卻在Web2的產品鏈上瘋狂傳銷,各方大佬紛紛為其背書,到底發生了什么?Damus的葫蘆里,賣的是什么藥? 注冊和簡單實用 很少有什么產品在用戶注冊環節會有什么噱頭,但Damus確實出 ......

    uj5u.com 2023-02-05 06:48:39 more