Golang筆記02:函數、方法、泛型、接口學習筆記
一、進階學習
1.1、函數
go
中的函數使用func
關鍵字進行定義,go
程序的入口函數叫做:main
,并且必須是屬于main包
里面。
1.1.1、定義函數
(1)普通函數
go
中定義函數,需要使用func
關鍵字,同時要指定函數名稱,函數參數,函數返回值,函數體,這就是函數的五要素。語法格式:
func 函數名稱(參數1,參數2,...)(返回值類型1,返回值類型2,...) {// 函數體
}// 當只有一個返回值類型的時候,可以省略括號(),直接寫類型即可
go
語言中,函數允許返回多個返回值
,這個語法和其他的一些編程語言是有些區別的,比如:Java
語言中,只允許一個函數返回一個返回值。go
語言中,函數的特點:
- 允許返回多個返回值。
- 不允許函數重載。
// Method01 定義函數
func Method01() int {return 1 + 2
}// Method02 定義函數
func Method02(a int, b int) (int,int) {return a + b, a - b;
}
另外,如果函數參數的類型都是一致的,那么可以簡寫成下面這種方式:
// Method02 定義函數,參數類型相同,則可以簡寫
func Method02(a, b int) (int,int) {return a + b, a - b;
}
(2)函數字面量
函數字面量,是指將函數作為一個變量先定義出來,接著在需要使用的時候,通過func
重新定義一個函數,然后函數字面量指向func
函數。如下所示:
package mainimport "fmt"// 先定義一個函數字面量,函數體不定義
var method func(int, int) (int, int)// Method02 定義函數
func Method02(a int, b int) (int, int) {return a + b, a - b
}func main() {// 這里給函數字面量,指向具體的函數實現method := func(a int, b int) (int, int) {return a + b, a - b}// 調用函數add, sub := method(1, 2)fmt.Println(add, sub)
}
1.1.2、函數參數和返回值
(1)函數參數
go
語言中的函數參數,如果是相同數據類型的,則可以統一定義類型,不需要每個參數都寫出類型名稱。格式:
func 函數名稱(參數1,參數2,參數3 數據類型)(返回值1,返回值2...) {// 函數體
}
另外,go
中函數參數的傳遞是值傳遞
,也就是說,函數參數傳遞的時候,會拷貝實參的值。在函數體內修改函數參數的值,不會影響實參的值。
如果要定義可變參數,那么可以使用【...
】符號,并且可變參數只能夠在最后一個參數位置出現。
// 可變參數
func Method03(args ...int) {}
(2)函數返回值
go
語言中,函數的返回值允許定義多個,當只有一個返回值,則可以不寫括號,超過一個返回值的時候,則必須使用括號將所有返回值包裹起來。
// 只有一個返回值,可以省略返回值的括號
func demo() int {}
// 多返回值
func demo() (int,string){}
另外,go
中函數返回值也可以定義名稱,定義返回值名稱之后,那么這個參數就可以在函數體中直接使用,并且通過return
返回結果。
// Method02 定義函數
func Method02(a, b int) (add int,sub int) {// 可以使用函數返回值名稱add = a + bsub = a - b// 定義了返回值名稱,則return的時候,可以不寫// 等價于 return add, subreturn//return add, sub
}
1.1.3、匿名函數
匿名函數,顧名思義,就是沒有函數名稱的函數。匿名函數一般在函數內部使用,語法格式:
package mainimport "fmt"func main() {// 定義匿名函數,并且調用函數ans := func(a, b int) int {ans := a + breturn ans}(1, 2)fmt.Println(ans)
}
匿名函數也可以作為一個函數的參數,例如:
package mainimport "fmt"// 定義函數,并且函數接受一個函數作為參數
func demo(a, b int, call func(int, int) int) int {return call(a, b)
}func main() {// 調用函數ans := demo(1, 2, func(a, b int) int {return a * b})fmt.Println(ans)// 調用函數ans2 := demo(1, 2, func(a, b int) int {return a + b})fmt.Println(ans2)
}
1.1.4、閉包
go
語言中,函數有一個閉包的概念,閉包在一些編程語言中,又叫做:Lamda
表達式。閉包,可以在內部函數中訪問到外部函數的變量,即使外部函數已經執行完畢,內部函數仍然可以訪問到外部的變量,這個過程就叫做:閉包。
- 【
閉包=匿名函數+外部環境變量引用
】
下面給一個閉包的案例:
package mainimport "fmt"func main() {// 創建一個匿名函數,并且返回值是一個函數類型sum := func() func() int {a, b := 1, 1// 返回一個匿名函數,相當于回調函數,并且還修改外部函數 a、b 兩個變量的值return func() int {// 引用外部函數的變量a, b = b, a+breturn a}}// 調用函數,并且返回值是個回調函數getSum := sum()for i := 0; i < 10; i++ {// 調用回調函數,由于閉包的特性,回調函數中仍然可以使用 sum() 函數中的變量 a、bfmt.Println(getSum())}
}
閉包結構中,外部函數執行結束之后,內部函數就相當于一個回調函數一樣,仍然可以被繼續調用,并且還可以使用外部函數中的變量。另外,多次調用外部函數獲取到的回調函數,是互不影響的。
package mainimport "fmt"func demo() func() int {count := 0return func() int {count++return count}
}func main() {// 第一次調用外部函數f1 := demo()fmt.Println("調用f1()函數")fmt.Println(f1())fmt.Println(f1())fmt.Println(f1())// 第二次調用外部函數f2 := demo()fmt.Println("調用f2()函數")fmt.Println(f2())fmt.Println(f2())fmt.Println(f2())fmt.Println("再次調用 f1() 函數")// 再次調用 f1() 函數fmt.Println(f1())
}
上面案例中,就是演示的多次調用外部函數,獲取到的回調函數之間是互不影響的。運行結果如下所示:
調用f1()函數
1
2
3
調用f2()函數
1
2
3
再次調用 f1() 函數
4
從上面的執行結果可以看到,f1()
和f2()
之間的閉包結構是互不影響的。
1.1.5、延遲調用
go
語言中,提供了一個defer
關鍵字,可以用于聲明函數的延遲調用功能。defer
聲明的延遲函數,會在外部函數執行完成之前,調用延遲函數,一般用于釋放文件資源,關閉連接等操作。
注意:當一個函數中,存在多個
defer
定義的延遲函數,那么go
會根據后進先出的規則,依次調用延遲函數。
怎么理解執行完成之前,才會調用defer
延遲函數呢???
- 當聲明了
defer
函數的代碼塊內,執行到最后一行代碼語句,后面已經沒有可執行的語句的時候,但是函數還沒有結束執行,只有遇到函數的最后一個右花括號【}】,才算執行結束。 - 那么
defer
函數,就是在遇到右花括號【}】之前,會被調用的。
package mainimport "fmt"func deferDemo() {fmt.Println("3、執行延遲函數...")
}func main() {fmt.Println("1、執行main函數代碼...")// 采用延遲函數的方式,調用方法,將在 main 函數執行完成之前,調用 deferDemo() 函數defer deferDemo()fmt.Println("2、執行main函數最后一句代碼...")
}// 控制臺輸入結果
1、執行main函數代碼...
2、執行main函數最后一句代碼...
3、執行延遲函數...
從上面案例代碼中,可以看到,雖然defer
函數聲明在中間位置,但是從控制臺輸出的結果來看,defer
函數的內容確是最后打印出來的,這也就說明defer
函數是最后執行的。
注意了,當存在多個defer延遲函數的時候,Go語言會按照后進先出的順序,依次執行defer延遲函數。案例代碼:
package mainimport "fmt"func demo01() {fmt.Println("調用demo01()函數...")
}
func demo02() {fmt.Println("調用demo02()函數...")
}
func demo03() {fmt.Println("調用demo03()函數...")
}func main() {fmt.Println("開始執行main函數...")// 定義延遲函數defer demo02()defer demo01()defer demo03()fmt.Println("main函數執行完成...")
}// 執行結果
開始執行main函數...
main函數執行完成...
調用demo03()函數...
調用demo01()函數...
調用demo02()函數...
1.2、方法
go語言中,既有函數,又有方法,在其他的語言中,一般情況下,函數和方法都是同一個概念,但是在go語言里面,兩者是有點不同的。
go中的函數和方法,在定義形式上大體相同,只不過方法的定義,需要顯示的聲明方法的接收者,只有接收者才可以調用方法。方法定義格式:
// 自定義方法接收者的類型
type 接收者類型名稱 接收者實際數據類型func (方法接收者名稱 接收者類型名稱) 方法名稱(方法參數) 返回值類 {// 方法體
}
什么是方法接收者呢???要如何理解方法接收者這個概念???
- 方法的接收者,可以理解成是方法的調用者,它是規定哪一種數據類型的對象,可以調用這個方法。
go
語言中的方法接收者,就類似于java
語言中的this
關鍵字,例如:this.demo()
,這個this
就是方法的接收者,也可以理解成調用者。
方法的調用一般需要和自定義類型結合使用。
調用方法的語法格式:
變量名稱 := 值
變量名稱.方法名稱()
方法的調用和函數的調用有點區別,函數是直接在代碼中調用即可,不需要指定是由誰觸發的調用。而方法,則需要指定具體的調用對象,是由哪個變量對象觸發的方法調用。
package mainimport "fmt"// MyInt 先自定義一個類型
type MyInt intfunc (myInt MyInt) setValue(value int) {// 修改參數值myInt = MyInt(value)fmt.Println("方法中,修改的值=", myInt)
}func main() {var myInt MyInt = 1fmt.Println("修改之前的值=", myInt)// 調用方法myInt.setValue(2)fmt.Println("調用方法,修改之后的值=", myInt)
}// 執行結果
修改之前的值= 1
方法中,修改的值= 2
調用方法,修改之后的值= 1
從上面案例代碼中,可以看到,我們雖然調用了setValue()
方法去修改myInt
的變量,但是方法執行完成之后,myInt
的值仍然沒有變化,這是為什么呢???
這就涉及到一個知識點了,方法的接收者分為兩種情況:值接收者
和指針接收者
。
1.2.1、值接收者
go
中方法的值接收者,是指方法接收者是通過值傳遞到方法里面的,傳遞的是形參,修改形參是不會影響到實際參數的。這和Java
中的值傳遞的概念相同。
要想在方法里面,修改接收者的數據,那就需要通過指針接收者來實現。
1.2.2、指針接收者
方法指針接收者,這和Java
中的引用傳遞的概念相同,在一個方法中,對引用傳遞
的參數進行修改,實際上是對這個引用地址指向的數據進行了修改,會影響實際的參數。
go
中的指針接收者作用就和Java
引用傳遞作用相同,通過指針接收者修改數據,會將實際參數的值也一起更新了。
package mainimport "fmt"// MyInt 先自定義一個類型
type MyInt int// 方法接收者采用指針類型
func (myInt *MyInt) setValue(value int) {// 修改參數值*myInt = MyInt(value)fmt.Println("方法中,修改的值=", *myInt)
}func main() {var myInt MyInt = 1fmt.Println("修改之前的值=", myInt)// 調用方法,雖然 myInt 這里是值類型,但是 Go 會將其編譯之后,就相當于是 (&myInt).setValue(2)myInt.setValue(2)fmt.Println("調用方法,修改之后的值=", myInt)
}// 執行結果
修改之前的值= 1
方法中,修改的值= 2
調用方法,修改之后的值= 2
1.3、接口介紹
Go
中的接口分為兩大類:基本接口
和通用接口
。
- 基本接口:接口內部是一組方法的集合。
- 通用接口:接口內部是一組類型的集合。
接口,是一組規范的集合,也就是說,接口只會規定實現功能的規范,但是具體的功能邏輯是怎么實現的,接口不負責,具體的功能實現交給具體的實現類。
在Go
中沒有類與繼承的概念,而是通過結構體來實現類的功能,那么在接口實現上,也是通過結構體來實現的。你可以怎么理解,創建一個struct
結構體,就相當于是Java
中創建了一個Class
類。
1.3.1、基本接口
Go
中定義接口需要使用interface
關鍵字,這個關鍵字用于標識是接口類型。
(1)定義接口
基本接口的語法格式,如下所示:
// 基本接口的定義
type 接口名稱 interface {// 定義方法方法名稱(參數類型) 返回值類型方法名稱(參數類型) 返回值類型方法名稱(參數類型) 返回值類型
}
在定義基本接口的時候,接口內部只能夠是一組方法的集合,不能存在其他的類型集合。針對接口中的方法,方法參數可以不用寫參數名稱,只需要指定類型即可,因為接口不具備實現邏輯,也就不會使用方法參數,所以寫不寫參數名稱都沒關系,但是類型是必須要規定的。
package mainimport "fmt"// BaseInterface 定義基本接口
type BaseInterface interface {// Say 定義兩個方法Say(string) stringWalk()
}func main() {// 初始化接口var baseInterface BaseInterfacefmt.Println(baseInterface)
}
上面代碼,就是定義了一個基本接口,并且在main
函數中,初始化了接口,但是這個接口還沒有具體實現,只是初始化了。
(2)接口實現
接口定義好了之后,那么要如何實現這個接口中的方法呢???Go
語言中沒有提供類似Java
中的implements
關鍵字,那么要怎么樣才能實現接口呢???
在Go
語言中,接口的實現方式很簡單,只需要一種類型(結構體或者自定義類型
)將接口中的所有方法都實現了,那么我們就說,這個類型實現了某某接口。
package mainimport "fmt"// BaseInterface 定義基本接口
type BaseInterface interface {// Say 定義兩個方法Say(string) stringWalk()
}// CustomType 定義類型,實現接口
type CustomType struct{}// Say CustomType 實現 BaseInterface 接口的方法
func (customType CustomType) Say(s string) string {fmt.Println("CustomType實現接口:saying..." + s)return s
}// Walk CustomType 實現 BaseInterface 接口的方法
func (customType CustomType) Walk() {fmt.Println("CustomType實現接口:walking...")
}func main() {
}
上面案例代碼中,自定義了CustomType
結構體類型,然后這個結構體定義了兩個方法,方法和接口中定義的方法名稱一致,那么這種情況下,CustomType
類型就是實現了BaseInterface
接口了。
Go
語言中接口的實現都是隱式的,不像Java
中的接口實現那樣,還需要使用implements
關鍵字才能夠實現接口。
(3)使用接口
實現接口之后,就需要在相應的地方,使用接口實現來完成業務功能啦。使用接口很簡單,其實就是通過接口的實現類,調用對應的接口方法即可。
package mainimport "fmt"// BaseInterface 定義基本接口
type BaseInterface interface {// Say 定義兩個方法Say(string) stringWalk()
}// CustomType 定義類型,實現接口
type CustomType struct{}// Say CustomType 實現 BaseInterface 接口的方法
func (customType CustomType) Say(s string) string {fmt.Println("CustomType實現接口:saying..." + s)return s
}// Walk CustomType 實現 BaseInterface 接口的方法
func (customType CustomType) Walk() {fmt.Println("CustomType實現接口:walking...")
}func main() {// 定義接口實現類customType := CustomType{}// 調用接口方法say := customType.Say("Hello World")fmt.Println("返回值:" + say)customType.Walk()
}
從上面案例代碼中,可以發現一個問題,在使用接口的時候,我們的代碼里面沒有直接出現BaseInterface
這個接口,而是定義了一個CustomType
結構體的變量,然后通過結構體變量去調用了結構體實現的Say
和Walk
兩個方法。
這是因為Go
語言中,某個類型(結構體或者自定義類型
)實現某個接口之后,對應的類型就已經滿足接口中定義的方法規范。
(4)空接口
Go
中提供了一個空接口,空接口就是接口中沒有定義任何的方法,里面是空的。空接口相當于Java
中的Object
對象,是所有類型的父類型,Go
中的空接口就是所有類型的父類型。
// 定義空接口變量
interfaceVar interface{}
空接口可以作為一個方法的參數,這個參數叫做:空接口變量
。空接口變量可以接收任意的數據類型,從而就可以實現同一個方法可以處理多種類型的參數。
package mainimport "fmt"func demo(interfaceVar interface{}) {switch v := interfaceVar.(type) {case int:fmt.Println("int 類型", v)case string:fmt.Println("string 類型", v)default:fmt.Println("都不滿足的類型")}
}func main() {// 定義int類型myInt := 10demo(myInt)// 定義string類型myStr := "hello world"demo(myStr)
}
(5)類型切換和斷言
Go
語言中,提供了一種特殊的語法,叫做:類型切換
和類型斷言
。語法格式:
// 類型切換和類型斷言
v := interfaceVar.(T)// interfaceVar 表示接口變量
// T 表示數據類型
// v 是一個變量,具體來說是一個動態類型變量,它的類型會根據 interfaceVar.(T) 檢測出來的類型變化
// 當 v 的類型和 case 類型匹配時候,那么 v 變量就會被賦值對應類型的數據值
上面表達式用在switch
結構里面,能夠動態的檢查接口變量interfaceVar
的具體類型,并且將變量的值賦值給變量v
。
類型切換和類型斷言的代碼執行規則,我理解大概是這樣的:
1、首先進行類型切換,通過 interfaceVar.(T),獲取到具體的類型
2、將獲取到具體類型賦值給 v 變量
3、v 變量的類型再和 case 中的類型進行匹配
4、如果 v 能夠匹配上 case 的類型,則將對應類型的值賦值給 v 變量
案例代碼:
package mainimport "fmt"func demo(interfaceVar interface{}) {switch v := interfaceVar.(type) {case int:fmt.Println("int 類型", v)case string:fmt.Println("string 類型", v)default:fmt.Println("都不滿足的類型")}
}func main() {// 定義int類型myInt := 10demo(myInt)// 定義string類型myStr := "hello world"demo(myStr)// 定義 float 類型myFloat := 3.14demo(myFloat)
}
1.3.2、通用接口
go中的通用接口,需要和泛型結合使用,這樣才可以定義一個通用的接口,讓所有的數據類型都適用,這就是通用接口。后續介紹泛型時候,在一起介紹通用接口。
1.4、泛型
Go
語言在1.18
版本中引入泛型的概念。泛型定義的語法格式如下所示:
// 泛型定義
[泛型參數 約束類型1 | 約束類型2...]// 舉個例子:
func sum[T int | float32](a,b T) T {// 方法體
}
類型約束
是指:當前泛型可以接收哪些數據類型,如果不是這里面的類型,那么就無法使用對應的方法、函數之類的。
上面就是定義泛型時候的語法格式,那要如何使用呢???
在使用泛型的時候,可以有兩種方式:
- 第一種方式:使用時候指定具體的數據類型。
- 第二種方式:不指定類型,讓編譯期自動推斷類型。
package mainimport "fmt"// 定義泛型方法
func sum[T int | float32](a, b T) T {return a + b
}func main() {// 計算 int 類型ans := sum(1, 2)fmt.Println(ans)// 主動指定類型ans2 := sum[float32](3.14, 2.86)fmt.Println(ans2)
}
泛型結構的使用注意事項:
- 泛型不能用在基本類型上。
- 泛型不能進行類型斷言。
- 匿名結構中,不允許使用泛型。
- 匿名函數不支持自定義泛型。
- 方法不能使用泛型。
為什么方法上面不能使用泛型呢???
我是這么理解的,因為
Go
中不允許方法重載,所以如果方法上面使用了泛型,那不就是相當于Go
中會存在兩個相同名稱的方法了嗎?這就和Go
中不允許方法重載的定義相違背了,所以也就不允許方法使用泛型了。
1.5、類型
Go
語言中的類型,是一種靜態強類型
,什么是靜態呢???
靜態強類型
靜態是指:Go
中的數據類型一旦定義出來之后,在編譯期期間就已經確定了,后續運行程序的時候,就不能夠改變類型了。
強類型是指:當我們程序員在寫代碼的時候,如果改變了數據類型,那么編譯期就會馬上提示程序員,語法錯了,不能修改類型。
var a int = 1
// 編譯不通過,因為前后類型不一致
a = "2"
類型后置
Go
語言中,所有的數據類型都是寫在名稱后面的,這是因為寫在變量名稱后面,能夠讓程序的可讀性更強,類型太多的時候不至于看著混亂。
var 變量名稱 數據類型
類型聲明
Go
語言中,使用type
關鍵字聲明類型,自定義類型也是使用type
關鍵字定義的。
// 聲明類型
type 類型名稱 數據類型
類型轉換
Go
語言中,沒有類型Java
語言中的隱式類型轉換的功能,Go
語言只有顯式類型轉換,也就是說,必須讓程序員在代碼中主動的進行類型轉換。
package mainimport "fmt"func main() {var f float64 = 3.14// 強制類型轉換var f2 int = int(f)fmt.Println(f2)
}
以上就是Go
語言中函數、方法、泛型、接口相關的學習筆記內容。