1. 程式人生 > 其它 >Go語言學習14-函式(超級重點)

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、函式內部的變數,外部是訪問不到的。

程式碼塊作用域

高階函式

函式也是一種型別,它可以作為引數,也可以作為返回值