GOPATH的作用
一个Go语言的工作空间可能(通常)是表示成这样的文件结构:
其中,该目录的根目录就是$GOPATH
-bin/
hello.exe # 可执行命令
outyet.exe # 可执行命令
-pkg/
windows_amd64/
github.com/golang/example/
stringutil.a # package对象
-src/
github.com/golang/example/
.git/ # Git仓库数据
hello/
hello.go # 源代码
outyet/
main.go # 源代码
main_test.go # 测试源代码
stringutil/
reverse.go # package源代码
reverse_test.go # 测试源代码
要编译上面的项目,可以在$GOPATH/src/github.com/golang/example/hello目录下,命令行输入
go build
或者
go install
而如果配置了GOPATH环境变量,则可以在任何地方打开cmd,输入
go build hello
或者
go install hello
这就体现了GOPATH的作用
第一个hello world的go程序
下面开始尝试编写我们的第一个go程序
在我们的工作空间下,或者不使用GOPATH进行编译则可以在任何地方,新建文件夹src,用来存放我们的源代码。
在src中,新建文件夹hello,存放我们的主程序源代码hello.go
在hello.go中输入
package main
import ("fmt")
func main() {
fmt.Printf("Hello World.\n")
}
这时候,我们的文件结构是这样的
src/
hello/
hello.go
接下来假设我们不使用GOPATH
,直接在src/hello
目录下输入命令。
我们有3种方法运行我们的hello.go
- 第一种方法
go run hello.go
会直接编译运行打印出Hello World.
go run
并不会在任何地方生成可执行文件,因此,使用go run
后我们的文件结构没有任何变化
- 第二种方法
go build
会在src/hello
目录下生成可执行文件hello.exe
接着在cmd中输入
hello
就可以打印出Hello World.
这时,我们的文件结构变为
src/
hello/
hello.exe
hello.go
- 第三种方法
go install
会在bin
目录下(如果没有bin
目录则会自动创建该目录),创建可执行文件hello.exe
接着,直接在当前目录下(src/hello/
)或者在bin/
目录下运行命令
hello
就会打印出Hello World.
在src/hello/
目录下运行hello
,如果当前目录下没有hello.exe
,就会自动寻找bin/
目录。
这时,我们的文件结构变为
bin/
hello.exe
src/
hello/
hello.go
到此,我们完成了第一个hello world程序
第二个hello world的go程序(初识pkg)
接着,我们尝试编写我们的第二个hello world程序。
首先,我们先来看看第一个程序的源代码,每一行具体是什么意思,第一个hello world程序源代码:
package main
import ("fmt")
func main() {
fmt.Printf("Hello World.\n")
}
第一行的package main
表明编译hello.go
会生成可执行文件。
第二行的import ("fmt")
表示引入包fmt
。
下面的main()
函数是生成的可执行文件运行时的入口函数。
下面我们尝试编写我们自己的一个包,然后在hello.go
把它引入并使用。
在src/
下新建文件夹world
,在src/world/
里面新建文件world.go
,输入
package world
import "fmt"
func World() {
fmt.Printf("hello, world\n")
}
这里,我们把world.go
打包成world
,我们把包的名字和目录的名字设为一样,这样可以避免后续导入包时引起歧义。在后续导入包时,import "world"
后接的"world"
是目录的名字,然后会自动导入子目录$GOPATH/src/world
中定义的包。而使用包的时候,则要使用包的名字,比如world.World()
中的world
指的是包的名字。而我们只要把目录名字和包的名字设为一样,就能统一在import
和具体使用包的时候使用同一个名字。
对于一个包,若变量名或函数名以大写开头,则意味着这个变量或函数会自动导出,后续导入包后就可以使用这些导出的变量或函数。而以小写开头的变量和函数则只能在包内互相访问和使用,同一个包的不同文件也可以访问这些小写开头的变量或函数。
接着我们修改src/hello/hello.go
为
package main
import ("world")
func main() {
world.World()
}
这时候,我们的文件结构变为
src/
hello/
hello.go
world/
world.go
要运行我们的程序,可以使用第一个hello world程序的三种方法。文件结构的改变也是相似的。
但是,我们使用上述三种方法运行了我们的程序,但是目录下并未生成pkg
文件夹,要使得world.go
能被编译出来,以便在后续编译hello.go
时不会重复编译world.go
,我们可以在src/world/
目录下,输入命令
go install
这样,我们就会在我们的工作空间下生成pkg\windows_amd64\world.a
我们的文件结构变为了(假设也进行了hello.go
的编译):
bin/
hello.exe
pkg/
wondows_amd64/
world.a
src/
hello/
hello.go
world/
world.go
变量和函数声明
变量声明
go语言中的基本变量包括
bool
string
int int8 int16 int32 int64
uint uint8 uint16 uint32 uint64 uintptr
byte // alias for uint8
rune // alias for int32
// represents a Unicode code point
float32 float64
complex64 complex128
go的变量声明与C/C++有很大区别,假设我们要声明2个int类型和1个bool类型的变量。在C/C++中,我们会这么做:
int x = 1, y = 2;
bool c;
而在go中,则要这样声明:
var x, y int = 1, 2
var c bool
这样声明的好处在于代码的可读性增强了,尤其是在声明函数的时候,至于为什么,可以详细看这篇解释Go的声明语法。
注意变量的声明如果没有显式给定初始值,则会使用默认初始值初始化变量,比如上述的变量x
和y
会相应初始为1
和2
,而c
则会初始化为false
。
也可以使用自动推导来声明一个变量,但只能在函数内部使用,因此不建议使用。比如
x, y, z := 1, "string", false
go语言的类型转换只能显式转换,比如
var i int = 42
var f float64 = float64(i)
var u uint = uint(f)
// or in a function block
i := 42
f := float64(i)
u := uint(f)
// if you try
// var i int = 42
// var f float64 = i
// you will get an error
go语言的常量声明需要使用const
代替var
,而且不可以使用:=
,常量声明可以在任意地方,函数内外均可,比如
package main
import "fmt"
const Pi = 3.14
func main() {
const World = "世界"
fmt.Println("Hello", World)
fmt.Println("Happy", Pi, "Day")
}
函数声明
一个简单的两个数相加的函数在go里面是这样声明的
func add(x int, y int) int {
return x + y
}
finc
表明这是一个函数,add
则表示函数的名字,(x int, y int)
表示函数的参数列表,也可以写成(x, y int)
,后面跟着的int
表示返回类型,如果有多个返回值则用括号括起来,比如func add(x, y int) (int, int) {...}
。
关于返回值还有一个特殊的规则,但是不建议使用,因为会降低代码的可读性。我们可以对返回值也命名,最后“裸返回”,就可以隐式地返回函数中出现的命名的返回值。如下面的程序最后会返回x
和y
func split(sum int) (x, y int) {
x = sum * 4 / 9
y = sum - x
return
}
为什么要这样声明
比如,x, y int
可以从左往右读成,变量x
和y
都是bool
类型。
又比如,f func(func(int,int) int, int) int
可以从左往右读成,变量f
是一个函数,这个函数参数为一个以2个int
作为参数的函数,以及2个int
,这个函数返回一个int。尽管读起来很奇怪,但是直观感受起来会比较简单。
而使用这样的声明规则,还可以简单实现函数闭包:
sum := func(a, b int) int { return a+b } (3, 4)
for
, if
, switch
go语言的for
和if
也与传统的C/C++有很大区别。
for
- 第一种情况,可以简单把go中的
for
理解为不带括号的C/C++的for
,比如
for i := 0; i < 10; i++ {
sum += i
}
// or
sum := 1
for ; sum < 10; {
sum += sum
}
- 第二种情况,
for
等同于没有括号的C/C++的while
,比如
sum := 1
for sum < 10 {
sum += sum
}
- 第三种情况,要写死循环,即对应C/C++中的
while(1)
,可以这样写
for {
// ...
}
if
与C/C++相比,go中的if
的条件除了没有括号外,与for
类似,在条件前可以加一个简单的初始化语句,但是要注意,这个初始化语句初始化的变量,作用域只有这个if
子句和后续的else
语句等,即作用域只是整一个if
块。比如
if v := somefunc(); v < lim {
return v
} else {
return lim + v
}
// you can not access v here
switch
与C/C++相比,go语言的switch
在执行完一个case
分支后,会自动跳出整个switch
,而不像C/C++会继续往下执行其他的case
。此外,go语言的switch
中,case
可以是函数而非常量。
// it is equal to
// if i != 0 {
// f()
// }
switch i {
case 0:
case f():
}
defer
defer的意思是推迟,在go语言中,使用defer
关键字可以让defer
后的语句延迟执行,具体延迟到该语句所在的语境结束后才执行。还是看个例子比较好懂:
func main() {
fmt.Printf("counting ")
for i := 0; i < 10; i++ {
defer fmt.Printf("%v ", i)
}
fmt.Printf("done ")
}
该程序的输出为:
counting done 9 8 7 6 5 4 3 2 1 0
可见,defer
后的语句,相当于被推进了一个栈,在main
函数结束前,栈内的语句又一条一条被弹出并执行。
指针
普通指针
go的指针和C/C++的类似,根据声明规则可以这样声明:
var p *int
即声明变量p,他是一个指针,指向一个int类型,注意指针的默认值都为nil
要给该指针赋值,则要用到取地址符&
i := 2
p = &i
fmt.Println(*p)
*p = 3 // this would change i to 3
结构体的指针
go的结构体和C/C++的类似,但和C/C++不同的是,C/C++中,指向结构体的指针访问结构体内的成员,需要这样:p->a
或者(*p).a
,而在go中,则可以直接p.a
这样访问结构体成员。
数组
go中,声明一个数组可以这样
var a [10]int
即声明变量a,它是一个大小为10的数组,类型是int。
数组切片
也可以使用:=
声明数组,数组还可以切片:
a := [4]int{1, 2, 3, 4}
var b []int = a[1:2] // b = {2, 3}
要特别注意的是,数组切片相当于数组的引用(指针),任何改变切片b
的操作,都会改变原来的数组a
。或者说,任何数组间的直接赋值,只是对指针的赋值,并没有深拷贝一个新的数组。
数组切片有两个变量,长度len
和容量cap
,分别表示数组切片本身的长度,和原数组的长度,比如
a := [6]int{2, 3, 5, 7, 11, 13}
b := a[:3] // len(b) = 3, cap(b) = 6
数组切片还可以用make
函数来创建,该函数第一个参数是类型,第二个参数是长度,第三个参数是容量,比如
b := make([]int, 0, 5) // len(b)=0, cap(b)=5