Go 学习笔记(15)— 函数(01)[函数定义、函数特点、多值返回、实参形参、变长参数,函数作为参数调用]
1. 函數定義
Go 語言最少有個 main() 函數。函數聲明告訴了編譯器函數的名稱,返回類型和參數。
func funcName(parameter_list)(result_list) {function_body
}
函數定義解析:
-
func:定義函數關鍵字;
-
funcName:函數名遵循標識符的命名規則,首字母大寫其它包可見,首字母小寫只能本包可見;
-
parameter_list:參數列表,使用 () 包裹,參數就像一個占位符,當函數被調用時,你可以將值傳遞給參數,這個值被稱為實際參數。參數列表指定的是參數類型、順序、及參數個數。參數是可選的,也就是說函數也可以不包含參數;
-
result_list:返回類型,函數返回一列值。result_list 是該列值的數據類型。有些功能不需要返回值,這種情況下 result_list不是必須的;
-
function_body:函數體使用
{}包裹,{必須位于函數定義行的行尾;
2. 函數特點
- 函數可以沒有輸入參數,也可以沒有返回值,默認返回值為 0;
func A() {// do something...
}// or
func A() (int) {// do something...return 1
}
- 多個相鄰的相同類型參數可以使用簡寫模式;
func add(a int, b int) {...
}
簡寫為:
func add(a, b int) {...
}
以下兩個聲明是等價的:
func f(i, j, k int, s, t string) { /* ... */ }
func f(i int, j int, k int, s string, t string) { /* ... */ }
- 支持有名的返回值,參數名就相當于函數體內最外層的局部變量,命名返回值變量會被初始化為類型零值,最后的 return 可以不帶參數名直接返回;
命名返回參數可看做與形參類似的局部變量,最后由 return 隱式返回。
// sum 相當于函數內的局部變量,被初始化為 0
func add(a, b int) (sum int) {sum = a + breturn // return sum 的簡寫模式// sum := a + b 則相當于新聲明一個 sum 變量名,原有的 sum 變量被覆蓋// return sum 需要顯式地調用 return sum
}
- 不支持默認值參數;
每一次函數調用都必須按照聲明順序為所有參數提供實參(參數值)。在函數調用時, Go 語言沒有默認參數值,也沒有任何方法可以通過參數名指定形參,因此形參和返回值的變量名對于函數調用者而言沒有意義。
-
不支持函數重載;
-
函數作為值賦值給變量;
package mainimport ("fmt"
)func fire() {fmt.Println("fire")
}func main() {var f func()// 將變量 f 聲明為 func() 類型,此時 f 就被俗稱為“回調函數”,此時 f 的值為 nil。f = fire // 將 fire() 函數作為值,賦給函數變量 f,此時 f 的值為 fire() 函數f() // 使用函數變量 f 進行函數調用,實際調用的是 fire() 函數。
}
- 不支持命名函數嵌套,但支持匿名函數的嵌套;
func add(a , b int) (sum int) {anonymous:= func(x , y int) int {return x + y} return anonymous(a , b)
}
- 函數是第一類對象,可作為參數傳遞。建議將復雜簽名定義為函數類型,以便于閱讀。
package mainimport "fmt"func test(fn func() int) int {return fn()
}// FormatFunc 定義函數類型。
type FormatFunc func(s string, x, y int) stringfunc format(fn FormatFunc, s string, x, y int) string {return fn(s, x, y)
}
func main() {s1 := test(func() int { return 100 }) // 直接將匿名函數當參數。s2 := format(func(s string, x, y int) string {return fmt.Sprintf(s, x, y)}, "%d, %d", 10, 20)println(s1, s2)
}
- 命名返回參數允許 defer 延遲調用通過閉包讀取和修改。
package mainfunc add(x, y int) (z int) {defer func() {z += 100}()z = x + yreturn
}func main() {println(add(1, 2)) // 輸出: 103
}
- 顯式 return 返回前,會先修改命名返回參數。
package mainfunc add(x, y int) (z int) {defer func() {println(z) // 輸出: 203}()z = x + yreturn z + 200 // 執行順序: (z = z + 200) -> (call defer) -> (ret)
}func main() {println(add(1, 2)) // 輸出: 203
}
- 在函數中,實參通過值傳遞的方式進行傳遞,因此函數的形參是實參的拷貝,對形參進行修改不會影響實參,但是,如果實參包括引用類型,如指針、
slice、map、function、channel等類型,實參可能會由于函數的間接引用被修改。
3. 多值返回
Go 支持多值返回,定義多值返回的返回參數列表時要使用 () 包含,支持命名參數的返回。
3.1 不帶命名參數的返回值
如果返回值是同一種類型,則用括號將多個返回值類型括起來,用逗號分隔每個返回值的類型。使用 return 語句返回時,值列表的順序需要與函數聲明的返回值類型一致,示例代碼如下:
func swap(a, b int) (int, int) {return b, a
}
如果多值返回有錯誤類型,一般將錯誤類型作為最后一個返回值。
不能用容器對象接收多返回值。只能用多個變量或 “_” 忽略。
package mainfunc test() (int, int) {return 1, 2
}func main() {// s := make([]int, 2)// s = test() // Error: multiple-value test() in single-value contextx, _ := test()println(x)
}
多返回值可直接作為其他函數調用實參。
package mainfunc test() (int, int) {return 1, 2
}func add(x, y int) int {return x + y
}func sum(n ...int) int { // 不定參數var x intfor _, i := range n {x += i}return x
}func main() {println(add(test()))println(sum(test()))
}
3.2 帶命名參數的返回值
Go 語言支持對返回值進行命名,這樣返回值就和參數一樣擁有參數變量名和類型。
命名的返回值變量的默認值為類型的默認值,即數值為 0,字符串為空字符串,布爾為 false、指針為 nil 等。
下面代碼中的函數擁有兩個整型返回值,函數聲明時將返回值命名為 a 和 b,因此可以在函數體中直接對函數返回值進行賦值,在命名的返回值方式的函數體中,在函數結束前需要顯式地使用 return 語句進行返回,代碼如下:
package mainimport ("fmt"
)func main() {x, y := namedRetValues()fmt.Println(x, y) // "5"
}func namedRetValues() (a, b int) { // 對兩個整型返回值進行命名,分別為 a 和 ba = 1 // 命名返回值的變量與這個函數的局部變量的效果一致,可以對返回值進行賦值和值獲取。b = 2// 當函數使用命名返回值時,可以在 return 中不填寫返回值列表,如果填寫也是可行的,return // 等價 return a, b
}
4. 實參形參
Go 函數實參到形參的傳遞永遠是值傳遞,除非參數傳遞的是指針值得拷貝,實參是一個指針變量,傳遞給形參的是這個指針變量的副本,二者指向同一地址, 本質上參數傳遞仍是值拷貝。
package mainimport "fmt"func main() {a := 10addOne(a)fmt.Println("main a is ", a)fmt.Println("main a address is ", &a)addPointer(&a) // 實參給形參傳遞時仍然是值拷貝,傳遞的是 a 的地址fmt.Println("main a is ", a)}func addOne(a int) int {a = a + 1fmt.Println("addOne a is ", a)return a
}func addPointer(a *int) {fmt.Println("addPointer a address is ", a)*a = *a + 1fmt.Println("addPointer a address is ", a)fmt.Println("addPointer a is ", *a)return
}
輸出結果:
addOne a is 11
main a is 10
main a address is 0xc000016068
addPointer a address is 0xc000016068
addPointer a address is 0xc000016068
addPointer a is 11
main a is 11
5. 不定參數
不定參數也叫作可變參數,是指函數傳入的參數個數是可變的,Go 函數支持不定數目的形式參數,不定參數聲明使用 param ...type 的語法格式。
5.1 類型相同的不定參數
為了做到這點,首先需要將函數定義為可以接受可變參數的類型:
package mainimport ("fmt"
)
// 函數 myfunc() 接受不定數量的參數,這些參數的類型全部是 int,
func myfunc(args ...int) {for _, arg := range args {fmt.Println(arg)}
}
func main() {myfunc(1, 2, 3)myfunc(10, 20, 30)}
從內部實現機理上來說,類型...type本質上是一個數組切片,也就是[]type,通過 reflect.ValueOf(args).Kind() 可以看到其類型為 slice ,這也是為什么上面的參數 args 可以用 for 循環來獲得每個傳入的參數。
假如沒有...type這樣的語法糖,開發者將不得不這么寫:
func myfunc2(args []int) {for _, arg := range args {fmt.Println(arg)}
}
從函數的實現角度來看,這沒有任何影響,該怎么寫就怎么寫,但從調用方來說,情形則完全不同:
myfunc2([]int{1, 3, 7, 13})
大家會發現,我們不得不加上[]int{}來構造一個數組切片實例,但是有了...type這個語法糖,我們就不用自己來處理了。
函數的不定參數特點:
-
所有的不定參數類型必須是相同的;
-
不定參數必須是函數的最后一個參數;
-
不定參數在函數體內相當于切片,對切片的操作同樣適合對不定參數的操作;
-
切片可以作為參數傳遞給不定參數,切片名后要加上
...; -
形參為不定參數的函數和形參為切片的函數類型是不相同的;
可變參數變量是一個包含所有參數的切片,如果要將這個含有可變參數的變量傳遞給下一個可變參數函數,可以在傳遞時給可變參數變量后面添加...,這樣就可以將切片中的元素進行傳遞,而不是傳遞可變參數變量本身。
package mainimport "fmt"func main() {n := []int{1, 2, 3, 4, 5}result := sumOne(n...) // 切片元素作為參數傳遞給函數的不定參數,需要在切片名后加上 ...fmt.Println("result is ", result)ret := sumTwo(n) // 傳遞切片自身fmt.Println("ret is ", ret)fmt.Printf("sumOne type is %T\n", sumOne) // sumOne type is func(...int) intfmt.Printf("sumTwo type is %T\n", sumTwo) // sumTwo type is func([]int) int}func sumOne(a ...int) (ret int) {for _, v := range a { // 不定參數相當于切片,可以使用 range 訪問ret += v}return
}func sumTwo(a []int) (ret int) {for _, v := range a {ret += v}return
}
5.2 類型不同的不定參數
用 interface{} 傳遞任意類型數據是 Go 語言的慣例用法,使用 interface{} 仍然是類型安全的,下面通過示例來了解一下如何分配傳入 interface{} 類型的數據。
package main
import "fmt"
func MyPrintf(args ...interface{}) {for _, arg := range args {switch arg.(type) {case int:fmt.Println(arg, "is an int value.")case string:fmt.Println(arg, "is a string value.")case int64:fmt.Println(arg, "is an int64 value.")default:fmt.Println(arg, "is an unknown type.")}}
}
func main() {var v1 int = 1var v2 int64 = 234var v3 string = "hello"var v4 float32 = 1.234MyPrintf(v1, v2, v3, v4)
}
輸出結果:
1 is an int value.
234 is an int64 value.
hello is a string value.
1.234 is an unknown type.
如果在創建函數時,可變參數存在多種數據類型,則可以將可變參數類型設置成 interface{} ,示例代碼如下:
package main
import ("fmt""reflect"
)
func demo(a string, i int, other ...interface{}) {fmt.Println("first:", a)fmt.Println("other", i)fmt.Println("variable parameter:", other, "variable parameter type:", reflect.ValueOf(other).Kind())for index, val := range other {fmt.Println("variable parameter index:", index, ",variable parameter value:", val, ",variable parameter type:", reflect.ValueOf(val).Kind())}
}
func main() {demo("hello", 10, 100, "b", 3.1415926)
}
輸出結果:
first: hello
other 10
variable parameter: [100 b 3.1415926] variable parameter type: slice
variable parameter index: 0 ,variable parameter value: 100 ,variable parameter type: int
variable parameter index: 1 ,variable parameter value: b ,variable parameter type: string
variable parameter index: 2 ,variable parameter value: 3.1415926 ,variable parameter type: float64
5.3 獲取可變參數類型
package mainimport ("bytes""fmt"
)func printTypeValue(slist ...interface{}) string {// 字節緩沖作為快速字符串連接var b bytes.Buffer// 遍歷參數for _, s := range slist {// 將interface{}類型格式化為字符串str := fmt.Sprintf("%v", s)// 類型的字符串描述var typeString string// 對s進行類型斷言switch s.(type) {case bool: // 當s為布爾類型時typeString = "bool"case string: // 當s為字符串類型時typeString = "string"case int: // 當s為整形類型時typeString = "int"}// 寫值字符串前綴b.WriteString("value: ")// 寫入值b.WriteString(str)// 寫類型前綴b.WriteString(" type: ")// 寫類型字符串b.WriteString(typeString)// 寫入換行符,一個輸入變量一行b.WriteString("\n")}return b.String()
}func main() {// 將不同類型的變量通過printTypeValue打印出來fmt.Println(printTypeValue(100, "str", true))
}
輸出結果:
value: 100 type: int
value: str type: string
value: true type: bool
6. 函數調用
函數在定義后,可以通過調用的方式,讓當前代碼跳轉到被調用的函數中進行執行,調用前的函數局部變量都會被保存起來不會丟失,被調用的函數運行結束后,恢復到調用函數的下一行繼續執行代碼,之前的局部變量也能繼續訪問。
函數內的局部變量只能在函數體中使用,函數調用結束后,這些局部變量都會被釋放并且失效。
Go語言的函數調用格式如下:
返回值變量列表 = 函數名(參數列表)
下面是對各個部分的說明:
- 函數名:需要調用的函數名。
- 參數列表:參數變量以逗號分隔,尾部無須以分號結尾。
- 返回值變量列表:多個返回值使用逗號分隔。
實參通過值的方式傳遞,因此函數的形參是實參的拷貝。對形參進行修改不會影響實參。但是,如果實參包括引用類型,如指針, slice (切片)、 map 、 function 、 channel 等類型,實參可能會由于函數的間接引用被修改。
7. 函數作為參數調用
package main//定義一個函數類型,兩個 int 參數,一個 int 返回值
type math func(int, int) int//定義一個函數 add,這個函數兩個 int 參數一個 int 返回值,與 math 類型相符
func add(i int, j int) int {return i + j
}//再定義一個 multiply,這個函數同樣符合 math 類型
func multiply(i, j int) int {return i * j
}//foo 函數,需要一個 math 類型的參數,用 math 類型的函數計算第 2 和第 3 個參數數字,并返回計算結果
//稍后在 main 中我們將 add 函數和 multiply 分別作為參數傳遞給它
func foo(m math, n1, n2 int) int {return m(1, 2)
}func main() {//傳遞 add 函數和兩個數字,計算相加結果n := foo(add, 1, 2)println(n)//傳遞 multply 和兩個數字,計算相乘結果n = foo(multiply, 1, 2)println(n)
}
輸出結果
3
2
示例代碼
package mainimport "fmt"func funcA(f func() string, s1, s2 string) string {ret := f()return ret + s1 + s2
}func main() {fu := func() string {return "hello,world"}ret := funcA(fu, " 你好", " 世界")fmt.Println(ret)
}
總結
以上是生活随笔為你收集整理的Go 学习笔记(15)— 函数(01)[函数定义、函数特点、多值返回、实参形参、变长参数,函数作为参数调用]的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 云南车票多少钱啊?
- 下一篇: 微信网名男生霸气头像