Go語言學習14-函式(超級重點)
Go語言中支援函式、匿名函式和閉包,並且函式在Go語言中屬於“一等公民”。
0x00 return關鍵字
return的作用1:結束當前函式,跳出整個函式。所以連後面的It's over!
都沒有輸出出來。
package main
import "fmt"
func main() {
for i := 0; i <= 10; i++ {
fmt.Println(i)
if i == 6 {
return
}
}
fmt.Println("It's over!")
}
0x01 函式引入理解
為什麼要使用函式?
假設我現在需要一個求和的功能,那麼就是下面這段程式碼。如果我還想求和其他的數,一遍一遍寫就太麻煩了。
提高程式碼的複用,減少程式碼的冗餘,程式碼的維護性也提高了。說白了,下次用這個功能,直接呼叫這個函式即可。
例子檢視
package main
import "fmt"
func main() {
var num1 int = 100
var num2 int = 200
var sum int = 0
sum += num1
sum += num2
fmt.Println(sum)
}
0x02 函式詳解
簡單的函式
可以看到下段程式碼,我們在main()裡面引用了f1函式,可以幫助我們快速打印出很多行程式碼。但是思考一下,如果我要讓它打印出不同的結果怎麼辦?
func f1() { fmt.Println("111111111111") fmt.Println("111111111111") fmt.Println("111111111111") fmt.Println("111111111111") } func main() { f1() }
傳參,實現定製化的操作
func f2(x string) {
fmt.Println("Hello!", x)
}
func main() {
f2("北京")
f2("上海")
}
帶返回值的
思考下面函式會輸出語句麼?不會,是因為f3(100,200)
僅僅是呼叫函式。呼叫函式和輸出語句沒有任何關係。如果想要讓其輸出,加一個輸出即可。
func f3(x int, y int) (sum int, sub int) {
sum = x + y
sub = x - y
return
}
func main() {
f3(100, 200)
}
引數型別簡寫
x和y都是整型,所以直接輸出即可
//引數型別簡寫
func f3(x, y int) (sum int, sub int) {
sum = x + y
sub = x - y
return
}
多個引數變種
還記得...吧,...代表若干個,如果型別一樣,可以用...代替,注意,返回值不支援這麼寫!
//可變引數(多個引數)
func f4(x int, y ...string) {
fmt.Println(x, y)
}
函式中不支援再新增一個函式,匿名函式可以
func f2()(x int){
defer func(){
x++
}()
return 5
}
高階函式:函式作為形參或者返回值
函式既然是一種資料型別,因此在Go中,函式可以作為形參,並且呼叫,把函式本身當作一種資料型別。
可以看到雖然a和b都是函式,但是他們的函式型別是不一樣的。
func f1() {
fmt.Println("Hellow shahe")
}
func f2(x int) int {
// fmt.Println("hellow")
return x
}
func main() {
a := f1
b := f2
fmt.Printf("%T\n %T", a, b)
}
一般情況下我們在函式裡面定義引數型別如切片,就是[]rune
func fa(slice []rune){
fmt.Println('a')
}
如果不滿足下面的型別,那就不能夠傳入進去引數
函式還可以作為返回值
func f5(x func() int) func(int, int) int {
return ff
}
func ff(a, b int) int {
return a + b
}
func test(num int) {
fmt.Println(num)
}
func test02(num1 int, num2 float32, testFunc func(int)) {
fmt.Println("----test02")
}
func main() {
a := test
fmt.Printf("a的型別是:%T, test函式的型別是%T \n", a, test)
a(10)
//呼叫test02函式
test02(10, 3.14, test) //因為能夠輸出----test02,所以證明能夠成功將test傳入test02進行使用
test02(10, 3.14, a)
}
01 基本語法
func 函式名(形參列表)(返回值型別列表){
執行語句...
return + 返回值列表
}
例子:使用函式來定義兩數相加
func sum(x int, y int) int {
return x + y //返回x+y後的結果,跳出函式
}
func main() {
a := 10
b := 20
c := sum(a, b) //函式的呼叫,a就是x,b就是y a和b使用sum函式進行處理後的結果即return出來的結果,相當於sum(a,b)用return來替換了
fmt.Println(c)
}
1、函式名:
- 遵循識別符號命名規範:見名知意addNum,駝峰命名addNum
- 首字母不能是數字
- 首字母大寫該函式可以被本包檔案和其他包檔案使用(類似public)
- 首字母小寫只能被本包檔案使用,其它包檔案不能使用(類似private)
2、形參列表與引數列表
形式引數列表:個數可以是0、1、n個 作用:接收外來的資料,後續處理。
實際引數列表:實際傳入的資料
3、返回值列表:函式的返回值型別應該寫在這個列表中
返回0個:
返回值1個:
返回值多個:
補充閱讀理解:
- 函式名:見名知意。由字母、數字、下劃線組成。但函式名的第一個字母不能是數字。在同一個包內,函式名也稱不能重名(包的概念詳見後文)。
- 形參列表:類似於一個佔位,引數由引數變數和引數變數的型別(Go語言為強型別,必須定義)組成,多個引數之間使用
,
分隔。 - 返回值:返回值由返回值變數和其變數型別組成,也可以只寫返回值的型別,多個返回值必須用
()
包裹,並用,
分隔。 - 函式體:實現指定功能的程式碼塊。
02 通過例題進行記憶體分析
首先程式執行會進入到main函式,因為main()是入口函式,第一個需要進行執行的函式,優先順序高。
在main裡面定義了兩個變數,正常輸出num1,num2=10,20沒毛病;
到了呼叫exchangeNumb函式的時候,交換兩個數的數值,再回到main函式中,按理說應該換了數值呀,為什麼沒有換??
package main
import "fmt"
func exchangeNum(num1 int, num2 int) {
var t int
t = num1
num1 = num2
num2 = t
}
func main() {
var num1 int = 10
var num2 int = 20
fmt.Printf("交換前的兩個數:num1=%v,num2=%v\n", num1, num2)
exchangeNum(num1, num2)
fmt.Printf("交換後的兩個數:num1=%v,num2=%v\n", num1, num2)
}
記憶體分析:
當我們執行Go語言時,會向記憶體申請一塊空間,供Go語言執行起來的程式來用。
隨後進行邏輯劃分,就是分成三個部分。棧、堆、程式碼區。
基本情況下,棧是用來存放基本資料型別的,如int、string、bool等;堆是用來存放引用資料型別、複雜資料型別的;程式碼區就是用來存放程式碼。(再次強調一下,這是一般情況,特殊情況可能堆疊存放的資料會變化)
1、執行程式碼時,首先是入口main函式,一旦執行main函式,就會在棧裡面獨自創建出一塊區域讓函式來存放函式自身的變數等,這塊區域被稱作為棧幀。
func main() {
2、在main函式中執行宣告變數語句,即宣告num1,num2;隨後在終端輸出第一句話
var num1 int = 10
var num2 int = 20
fmt.Printf("交換前的兩個數:num1=%v,num2=%v\n", num1, num2)
3、隨後呼叫函式exchangeNum,記憶體中就會建立exchangeNum棧幀。
exchangeNum(num1, num2)
4、隨後進行exchangeNum函式中的第一行語句,開始宣告變數num1,num2,並從main函式中繼承數值,說白了就是拷貝一份資料過去。
func exchangeNum(num1 int, num2 int) {
5、隨後進入函式體,聲明瞭t函式,開闢相應的記憶體空間
var t int
6、隨後num1的值被傳入到t,t此時被賦值
t = num1 //t = 10
7、再之後,num1的值被替換成20
num1 = num2 //num1 = 20
8、再之後,num2的值會變成10,之後結束函式執行。
num2 = t //num2 = 10
}
9、當函式執行結束後,會消除掉棧幀,即exchangeNum函式會被銷燬。所以exchangeNum函式僅僅只是完成了自身形參的轉換罷了,對main函式內部的變數沒有任何影響。再之後進行列印,還是這兩個數值。
03 函式不支援過載
過載:函式名相同,形參列表相同。可以看到報錯了,不支援函式重新宣告(redeclared)。不過可以使用匿名函式。
func exchangeNum(num1 int, num2 int) {
var t int
t = num1
num1 = num2
num2 = t
}
func exchangeNum(num1 int) {
var t int
t = num1
num1 = num2
num2 = t
}
04 函式支援可變引數(如果你希望函式帶有可變數量的引數)
可變引數是什麼意思?就是能夠變化的引數,一般來說我們可能會去傳多個引數,有時候傳0,有時候傳2,等等,但是吧,不穩定,我們希望這些引數是可以變化的。
什麼東西都沒有返還,什麼東西都沒有使用,所以是支援可變引數的。但是問題來了,怎麼處理裡面的引數?函式內部處理可變引數的時候,將可變引數當作切片來處理。
package main
//定義一個函式,函式的引數為:可變引數 ...引數的數量可變
func test(args ...int) { //args英文是多個引數的意思,不是關鍵詞,args...代表可以
//傳入任意多個數量的int型別的資料 傳入0個,1個...n個
}
func main() {
test() //傳0個
test(1) //傳1個
test(1, 2, 3, 4, 5, 45, 2, 3, 23, 12, 312, 3, 23, 1, 1) //傳n個
}
遍歷切片就好說了:
func test(args ...int) { //args英文是多個引數的意思,不是關鍵詞,args...代表可以
//傳入任意多個數量的int型別的資料 傳入0個,1個...n個
for i, v := range args {
fmt.Println(i, v)
}
}
func main() {
test()
test(1)
test(1, 2, 3, 4, 5, 45, 2, 3, 23, 12, 312, 3, 23, 1, 1)
}
05 值拷貝
基本資料型別和陣列預設都是值傳遞的,即進行值拷貝。在函式內修改,不會影響到原來的值。
06 函式內變數修改函式外變數
以值傳遞方式的資料型別,如果希望在函式內能夠修改函式外的變數,可以傳入變數的地址&,函式內以指標的方式操作變數,從效果來看類似引用傳遞。
package main
import "fmt"
func test(num *int) { //5、test函式新建指標變數接收num指標
*num = 30 //6、num指標對應的記憶體值改為30,函式結束,刪除棧幀
}
func main() { //1、進入main函式
var num int = 10 //2、宣告變數num=10
fmt.Println(&num) //3、列印num變數的指標
test(&num) //4、將num變數的指標傳入test函式
fmt.Println(num) //7、輸出num值
}
07 函式也是一種資料型別
在Go語言中,函式也是一種資料型別,可以賦值給一個變數,則該變數就是一個函式型別的變量了,通過該變數可以對函式呼叫。
上面這句話後面的部分,一句一句解釋:
func test(a int) {
fmt.Println(a)
}
func main() {
b := test
//函式也是一種資料型別,可以賦值給一個變數,則該變數就是一個函式型別的變量了
fmt.Printf("test函式的型別為:%T\nb的型別為:%T", test, b)
//通過該變數可以對函式呼叫
b(10)
}
08 函式作為形參
函式既然是一種資料型別,因此在Go中,函式可以作為形參,並且呼叫,把函式本身當作一種資料型別。
可以看到雖然a和b都是函式,但是他們的函式型別是不一樣的。
func f1() {
fmt.Println("Hellow shahe")
}
func f2(x int) int {
// fmt.Println("hellow")
return x
}
func main() {
a := f1
b := f2
fmt.Printf("%T\n %T", a, b)
}
一般情況下我們在函式裡面定義引數型別如切片,就是[]rune
func fa(slice []rune){
fmt.Println('a')
}
如果不滿足下面的型別,那就不能夠傳入進去引數。
函式還可以作為返回值
func f5(x func() int) func(int, int) int {
return ff
}
func ff(a, b int) int {
return a + b
}
func test(num int) {
fmt.Println(num)
}
func test02(num1 int, num2 float32, testFunc func(int)) {
fmt.Println("----test02")
}
func main() {
a := test
fmt.Printf("a的型別是:%T, test函式的型別是%T \n", a, test)
a(10)
//呼叫test02函式
test02(10, 3.14, test) //因為能夠輸出----test02,所以證明能夠成功將test傳入test02進行使用
test02(10, 3.14, a)
}
09 Go支援自定義資料型別
為了簡化資料型別定義,Go支援自定義資料型別。
- 基本語法:type 自定義資料型別名 資料型別
- 可以理解為:相當於起了一個別名
- 例如:type mylnt int ----->這時mylnt就等價int來使用了
- 例如:type mySum func(int,int) int-------------------------------------->這時mySum就等價一個函式型別func(int,int) int
func main() {
type myInt int
var num1 myInt = 30
fmt.Println("num1", num1)
var num2 int = 30
num2 = num1 //雖然是別名,但是在go中編譯是別的時候
//仍然認為兩者的資料型別不是一樣的
}
那麼怎麼才能讓其輸出正確?給num1
強制轉換一下就好了。
func main() {
type myInt int
var num1 myInt = 30
fmt.Println("num1", num1)
var num2 int = 30
num2 = int(num1)
fmt.Println("num2:", num2)
}
10 支援對函式返回值命名
做一個改動:
特點
函式和函式之間是並列關係,不影響
0x03 匿名函式
定義
匿名函式就是沒有名字的函式,很簡單。就完了。在函式內部使用。我們都知道函式內部不能宣告其他函式,但是匿名函式可以。
func(x int, y int) int {
ret := x + y
return ret
}
匿名函式的呼叫
第一種寫法:定義在外面,前面加個變數,就可以定義名字了。
var f1 = func(x int, y int) int {
ret := x + y
return ret
}
func main() {
fuck := f1(10, 20)
fmt.Println(fuck)
}
第二種寫法:在函式裡面進行定義和呼叫
func main() {
a()
//函式內部呼叫匿名函式,相當於用一個變數宣告匿名函式,隨後呼叫
f1 := func() {
fmt.Println("helkad")
}
f1()
}
第三種寫法:立即呼叫匿名函式
func main(){
//如果只是呼叫一次,可以簡寫稱立即執行函式
func() {
fmt.Println("立即執行匿名函式")
}() //這裡就是立即執行
}
第四種寫法:立即呼叫帶有引數的匿名函式
func main() {
a()
//函式內部呼叫匿名函式,相當於用一個變數宣告匿名函式,隨後呼叫
f1 := func() {
fmt.Println("helkad")
}
f1()
//如果只是呼叫一次,可以簡寫稱立即執行函式
func() {
fmt.Println("立即執行匿名函式")
}() //這裡就是立即執行
//立即呼叫帶有引數的匿名函式
func(x int, y int) {
ret := x + y
fmt.Println(ret)
}(10, 20)
}
0x04 init函式
【1】定義
init函式:初始化函式,可以用來進行一些初始化的操作。每一個原始檔都可以包含一個init函式,該函式會在main函式執行前,被Go執行框架呼叫。
func main() {
fmt.Println("main will be the 1st!")
}
func init() {
fmt.Println("init will be the first!")
}
【2】全域性變數定義,init函式,main函式的執行流程?
1、全域性變數定義 2、init函式 3、main函式
var x int = test()
func test() int {
fmt.Println("test函式被呼叫!")
return 10
}
func main() {
fmt.Println("main will be the 1st!")
}
func init() {
fmt.Println("init will be the first!")
}
【3】多個原始檔都有init函式的時候,如何執行?
main.go
package main
import (
"Study_GO/studygo/day06/init/testutils"
"fmt"
)
var x int = test()
func test() int {
fmt.Println("test函式被呼叫!")
return 10
}
func main() {
fmt.Println("main will be the 1st!")
fmt.Println("Name=", testutils.Name, "Gender=", testutils.Gender, "Age=", testutils.Age)
}
func init() {
fmt.Println("main中的init被執行了")
}
testutils.go
package testutils
import "fmt"
var Name string
var Age int
var Gender string
func init() {
fmt.Println("test中的init函式被執行")
Name = "你好"
Age = 18
Gender = "boy"
}
所以,順序是:1、外部的包中init 2、main中的init 3、main函式。為什麼呢?因為匯入包的時候,就會呼叫包中的init函式
0x05 閉包
【1】什麼是閉包?
閉包就是一個函式與其相關的引用環境組合的一個整體。
func getsum() func(int) int {
var sum int = 0
return func(num int) int {
sum = sum + num
return sum
}
}
//閉包:返回的匿名函式+匿名函式以外的變數num
func main() {
f := getsum()
fmt.Println(f(1))
fmt.Println(f(1))
fmt.Println(f(1))
fmt.Println(f(1))
}
感受:匿名函式中引用的那個變數會一直儲存在記憶體中,可以一直使用
【3】閉包的本質:
閉包本質依舊是一個匿名函式,只是這個函式引入外界的變數/引數
匿名函式+引用的變數/引數 = 閉包
【4】特點:
(1)返回的是一個匿名函式,但是這個匿名函式引用到函式外的變數/引數,因此這個匿名函式就和變數/引數形成一個整體,構成閉包。
(2)閉包中使用的變數/引數會一直儲存在記憶體中,所以會一直使用------------->意味著閉包不可濫用(對記憶體消耗很大!)
【5】不適用閉包可以嘛?可以是可以,但是很麻煩,我們需要每次將結果都傳一遍引數才可以,這是十分狗屎的。
func main() {
fmt.Println("==============我是一條優美的分割線===============")
// fuck := sum()
fmt.Println(sum(0, 1))
fmt.Println(sum(1, 1))
fmt.Println(sum(2, 1))
}
//不使用閉包來實現一個累加的效果可以嗎?
func sum(shu1, shu2 int) int {
shu1 = shu1 + shu2
return shu1
}
【6】總結
1、不使用閉包的時候:我想保留的值,不可以反覆使用
2、閉包應用場景:閉包可以保留上次引用的某個值,我們傳入一次就可以反覆使用了。即實現一個累加的場景。
0x06 函式再總結
函式的定義
引數的格式
無引數的函式
有引數的函式
引數型別簡寫
可變引數
返回值的格式
有返回值
多返回值
命名返回值
變數的作用域
全域性作用域
函式作用域
查詢變數的順序:
1、先在函式內部尋找變數,找不到往外找。
2、函式內部的變數,外部是訪問不到的。
程式碼塊作用域
高階函式
函式也是一種型別,它可以作為引數,也可以作為返回值