前言
这是我学习Go语法的笔记。由于有C和Python的基础,上手Go很快。笔记很粗糙,好在自己够用。
此篇包括了Go相关的:函数,闭包,类型命名,项目管理等。涉及关键字type defer
。
函数
无参无返回值函数
func myfun() {
fmt.Println("this is a func")
}
被调函数放在调用函数的前后都允许,不需要像C那样放在后面需要前置声明。
有参无返回值函数
func myfun(str string) {
fmt.Println(str)
}
// 形参同类型时
func myfun(str1, str2 string) {
// ...
}
// 形参不同类型时
func myfun(num int, str string) {
// ...
}
需要在参数列表中指明参数的类型。与C语言的区别是:形参名在前,类型在后。
不定长参数函数
func myfun(str ...string) {
// ...
}
在函数类型前加美式省略号(...
)可实现不定长参数传参。
func myfun(str ...string) {
for _, s := range str {
fmt.Printf("%s\n", s)
}
}
func main() {
myfun("hello", "guan", "ying")
}
// 输出
hello
guan
ying
需要注意的是:当参数列表中有多个参数时,不定长参数必须在固定参数的后面。
func myfun(num int, str ...string) {/* ... */} // 对
func myfun(str ...string, num int) {/* ... */} // 错
当我们需要把一个函数中的不定长参数传给另一个函数时,也需要用到...
:
func herfun(num ...int) {
for _, n := range num {
fmt.Println(n)
}
}
func myfun(num ...int) {
herfun(num...)
// 如果只需要传最后两个参数,可以
// herfun(num[1:]...)
}
func main() {
myfun(1, 2., 3)
}
有返回值函数
一般来说,让人习惯的用法是:
func myfun() int {/* ... */} // 表示return了一个int类型
func myfun(str string) (byte, byte, byte) {/* ... */} // 表示return了三个byte类型
此时返回的格式与Python格式相同。如第一个函数可以return 512
;第二个函数可以return 'z', 't', 'y'
。像下面这样:
func myfun(str string) (byte, byte, byte) {
return str[0], str[1], str[2]
}
func main() {
str := "zty"
z, t, y := myfun(str)
fmt.Printf("%c, %c, %c", z, t, y)
}
但是,这并不是官方推荐的返回方式。官方更建议一种酷炫、也更一目了然的方法——返回列表中写明返回的变量的名字:
func myfun(str string) (z byte, t byte, y byte) {
z, t, y = str[0], str[1], str[2]
return
}
看仔细!在myfun()函数中,我们并没有声明变量z、t、y,但它们确实、已然可用。而return的时候,return
就够了。
自然与参数列表一样,当返回值类型相同时我们可以简化代码:
func myfun(str string) (z , t, y byte) {/* ... */}
type
变量类型
关键字type
可以为数据类型重命名,让类型有了语义。
func main() {
type alpha byte // 类型alpha的底层数据类型是byte
var z alpha = 'z'
fmt.Printf("%c 的类型是 %T", z, z)
}
// 输出
z 的类型是 main.alpha
如果我们用alpha
来声明一个变量,我们很显然可以知道这个变量企图存放一个字母。这里需要说明一下,Python中获取一个变量的类型通过type()
函数;而Go语言是通过格式占位符%T
来实现。
通过type
命名的数据类型默认有一个方法,可以显式地转换变量的类型:
func main() {
type positive string
type negative string
var pWords positive = "beautiful"
var nWords negative = "ugly"
fmt.Printf("%s是%T类型的词语\n", pWords, pWords)
fmt.Printf("%s是%T类型的词语\n", nWords, nWords)
// 输出:
// beautiful是main.positive类型的词语
// ugly是main.negative类型的词语
fmt.Printf("%s是%T类型的词语\n", nWords, positive(nWords)) // 显式地转换类型
// 输出:
// ugly是main.positive类型的词语
}
这样的转换并非万能,只在允许的情况下使用。比如字符串类型就不能转整数型。所以基于这两个类型之上的类型的变量就不可以相互显式转换:
type (
str string
num int
)
var greet str = "hello"
fmt.Printf("%d\n", num(greet)) // 错误的使用方式
报错:cannot convert greet (type str) to type num
。
函数类型
Go语言也允许对一个函数类型命名,通过这种方式可以实现多态。具体使用方式如下:
func add(a int, b int) int {
return a + b
}
func sub(a int, b int) int {
return a - b
}
func main() {
type ComputeFunc func(int, int) int // 格式:type 类型名 函数类型
var compfun ComputeFunc
compfun = add // 此时compfun()当于add()函数
resultAdd := compfun(10, 20)
fmt.Printf("result = %d\n", resultAdd)
// 输出:result = 30
compfun = sub // 此时compfun()相当于sub()函数
resultSub := compfun(10, 20)
fmt.Printf("result = %d\n", resultSub)
// 输出:result = -10
}
匿名函数与闭包
Go语言的匿名函数与JS的相似。不得不说,功能强大。然而Python的匿名函数虽然有些弱鸡,但胜在优雅。我这种外貌协会更偏爱Python风。匿名函数像下面这样:
func main() {
resutl := func (a int, b int) int {
return a + b
} (10, 20)
fmt.Printf("%d\n", resutl)
}
上述代码的坏处是匿名函数会立即执行,有时候我们期望它能够在我们需要的时候执行——虽然我们早早就定义好了。解决方式是丢弃匿名函数最后的“小尾巴”:
func main() {
f := func (a int, b int) int {
return a + b
}
fmt.Printf("我还不想执行1\n")
fmt.Printf("我还不想执行2\n")
fmt.Printf("现在想执行了,那就执行吧!\n")
result := f(12, 21) // 执行匿名函数
fmt.Printf("%d\n", result)
}
// 输出:
我还不想执行1
我还不想执行2
现在想执行了,那就执行吧!
33
所以匿名的两种格式:
f := func () int {} // 不会立即执行
f() // 此时才执行
// ---------分割线--------------
func () int {} () // 立即执行
Python的匿名函数与闭包大多时候是一种概念,只是关键字lambda
的存在让其看起来不一样。而Go语言里二者的“外貌”比较统一。区别这个函数是不是闭包的要点在于:这个函数是否使用了与它同一作用域的变量或常量。
func main() {
alpha := 'z'
num := 512
func() {
alpha = 't'
num = 1024
}()
// alpha 与 num 值已经发生了改变
fmt.Printf("alpha = %c, num = %d\n", alpha, num)
// 输出:alpha = t, num = 1024
}
Python处理上面状况的时候存在一些问题。Py2不允许闭包函数直接修改外界变量的值,而Py3中,当你企图修改外界变量时需要用nonlocal
关键字说明:
def main():
alpha = "z"
num = 512
def package():
nonlocal alpha, num # 对变量进行说明
alpha = "t"
num = 1024
package()
print("alpha = %s, num = %d" % (alpha, num))
# 输出:alpha = t, num = 1024
main()
闭包的另一个特点是不会被它的作用域限制,只要闭包还在使用,它引用的变量就会一直存在:
func myfun() func() int {
var x int
return func() int {
x++
return x * x
}
}
func main() {
f := myfun()
fmt.Println(f()) // 输出:1
fmt.Println(f()) // 输出:4
fmt.Println(f()) // 输出:9
// 尽管myfun()的生命周期已经结束
// 但储存其闭包的内存空间还在
}
defer
常规使用
特点:(1)延迟调用(2)无论程序发生什么错误,总会执行发生错误前的defer修饰的语句(3)多个defer语句的执行顺序是先进后出。
一、延迟调用:
package main
import "fmt"
func main() {
fmt.Println("start ...")
defer fmt.Println("middle ...")
fmt.Println("end ...")
}
// 输出:
start ...
end ... // 注意执行顺序
middle ...
二、无论程序发生什么错误,总会执行发生错误前的defer修饰的语句:
func mod(x int) (result int) {
result = 100 / x
return
}
func main() {
defer fmt.Println("start ... ")
result := mod(0)
fmt.Println(result)
defer fmt.Println("end ... ")
}
// 输出:
start ...
panic: runtime error: integer divide by zero
// 没有执行 `fmt.Println("end ... ")` 语句
三、多个defer语句的执行顺序是先进后出
func main() {
defer fmt.Println("start ... ")
defer fmt.Println("end ... ")
}
// 输出:
end ...
start ...
// 注意执行顺序
与匿名函数的使用
无传参示例:
func main() {
a, b := 10, 20
defer func() {
fmt.Printf("a = %d, b = %d", a, b) // 输出:a = 100, b = 200
} ()
a, b = 100, 200 // 第二次赋值
}
根据输出值可推断匿名函数是在第二次赋值之后才被调用的。
传参示例:
func main() {
a, b := 10, 20
defer func(a int, b int) {
fmt.Printf("a = %d, b = %d", a, b) // 输出:a = 10, b = 20
} (a, b)
a, b = 100, 200
}
可见,对匿名函数传参时,匿名函数会绑定当时当刻的变量的值(闭包),而不是需要时才获取。这样做可以保护变量不受外界污染。在Python中,想达到此目的会有些麻烦,需要两层嵌套:
def main():
a, b = 10, 20
def outer(a, b):
def inner():
print("a = %d, b = %d" % (a, b))
return inner
f = outer(a, b)
a, b = 100, 200
f()
main()
# 输出:
a = 10, b = 20
作用域
花括号可以隔离变量:
func main() {
{
num := 1024
}
fmt.Println(num)
}
// 输出(程序报错):
undefined: num
这跟C语言是如出一辙的。
导包
Go语言中导入包有四种操作。
普通导入
package main
import "fmt"
func main() {
fmt.Println("this is a test")
}
通过这种方式导包后需要注意两点:第一点,如果需要用到这个包里的变量或函数,需要加上包名,如上面代码所示fmt.
;第二点,必须用到fmt包中的变量或者函数至少一次,否则编译器报错。
点操作
package main
import . "fmt"
func main() {
Println("this is a test")
}
类似Python中的from os import *
。此时使用fmt包中的内容需要省略包名。
重命名
package main
import mypen "fmt"
func main() {
mypen.Println("this is a test")
}
类似Python中的import os as xxx
。
_下划线操作
package main
import _ "fmt"
func main() {
}
Go语言中要求,导入的包、定义的变量必须被使用,否则不能通过编译器,此时下划线替我们解决了导入包必须被使用的烦恼。如上示代码,即便导入了fmt包,但可以不使用,编译器不会报错。更多时候这样做的目的是为了自动调用导入包的init()
函数。
项目管理
实际编程时我们需要将代码写在多个.go的源文件中,这时候需要知道Go语言是如何进行项目管理的(比Python稍稍复杂)。
要求一:代码必须放在src目录下。
要求二:src的父目录必须添加到GOPATH中(可用命令go env
进行查看)。
要求三:一个程序必须有一个main包。
同目录下的多个源文件
同目录下的多个源文件要求必须属于同一个包,也就说package xxx
中的xxx应该是相同的。
此时在目录D:\MyCode\GoCode\ImportPkg\src下创建两个文件:main.go和test.go。由于一个程序必须有一个main包,而test.go与main.go在同一目录,所以两个文件都应该属于main包。
// main.go
package main
func main() {
greet() // 直接使用test.go中的greet()函数
}
// test.go
package main
import "fmt"
func greet() {
fmt.Println("hello world.")
}
终端使用命令:go run .
或go build .
编译整个包的文件。
同一个包里的函数可以直接使用。不需要导入,也不要求首字母大写。
不同目录下的多个源文件
不同目录的源文件可以属于不同包。同时,如果需要使用别的目录下的源文件中的函数,必须导入所属包。且使用的函数必须首字母大写。
// main.go
package main
import "demo"
func main() {
demo.Greet()
}
// demo/test.go
package demo
import "fmt"
func Greet() {
fmt.Println("hello world.")
}
目录结构如下:
D:\MyCode\GoCode\ImportPkg\src:
|——main.go
|——demo
|——test.go
go install的使用
在src的同级目录中创建bin目录,并添加到系统变量中,此时在src目录下执行命令go install
,会在bin目录下生成可执行文件。
看到网上说同时还会生成pkg目录以及该目录下的相关文件,但我的没有,暂时存疑。
还不快抢沙发