开发环境搭建
随便学学,平时用的都是WIndows10的操作系统,就在这下面搞了。
下载安装
下载go安装程序,下载地址:https://golang.org/dl/ (被墙了,打不开)
墙内下载地址http://www.golangtc.com/download
我下的是这个:go1.9.2.windows-amd64.msi
安装就下一步就好了,装完之后验证一下。默认已经把Go的路径加到环境变量中去了,所以可以直接在cmd中输入go命令执行:
环境变量
装完之后,我装到了D:\Go\,已经帮我们添加好了2个环境变量:
- GOROOT: D:\Go\
- Path: D:\Go\bin
- GOPATH: 这个环境变量也要,但是需要手动设置
GOPATH也是要的,设置工作区路径,不过如果没有设置的话,有一个默认位置(差不多就是系统的家目录)。执行帮助命令 go help gopath
,我这截了一段说明:
If the environment variable is unset, GOPATH defaults
to a subdirectory named "go" in the user's home directory
($HOME/go on Unix, %USERPROFILE%\go on Windows),
unless that directory holds a Go distribution.
Run "go env GOPATH" to see the current GOPATH.
暂时不设置也没影响,反正也有默认的,后面写 Hellow World 的时候再看设置在哪个目录
开发工具
开发工具先没有装,不过这里先记一下工具的名称:
Visual Studio Code (简称 VS Code / VSC) 是一款免费开源的现代化轻量级代码编辑器,支持几乎所有主流的开发语言的语法高亮、智能代码补全、自定义热键、括号匹配、代码片段、代码对比 Diff、GIT 等特性,支持插件扩展,并针对网页开发和云端应用开发做了优化。软件跨平台支持 Win、Mac 以及 Linux。
这是一个通用的代码编辑器,装完之后要再装一下go的扩展:
- 点开,菜单-查看>>扩展
- 搜索“go”,找到“Rich Go language support for Visual Studio Code”,作者“lukehoban”,安装
- 装好之后用的时候
调试工具
Delve是一个非常棒的golang调试工具,支持多种调试方式,直接运行调试,或者attach到一个正在运行中的golang程序,进行调试。
安装Delve非常简单,直接运行 go get 即可:
go get -u github.com/derekparker/delve/cmd/dlv
先记一下,我并没有装
写代码
在第一次写代码前,还要先把go的工作区设置好。
设置工作区
工作区是存放go源码文件的目录。一般情况,Go源码文件都需要存放到工作区中,不过命令源码文档不是必须的。
每个工作区的目录结构都类似,有3个文件夹:
- src/: 用于存放源码以代码包为组织形式
- pkg/: 用于存放归档文件(名称以.a为后缀的文件)
- bin/: 用于存放当前工作区中的GO程序的可执行文件
找一个合适的位置,作为工作区。比如:E:\Go。这时就可以去把GOPATH环境变量设置为 “E:\Go” 。
按上面说的,还要工作区中还要创建3个文件夹,暂时只需要src,另外两个暂时不用。
Hello World
到这里需要一些仪式感,就是写一个 Hellow World 。在src目录下创建文件 “hellow.go” 。如果用了VS Code,会提示你装一些扩展,点下Install All,这样会自动把一些go的插件给装好。
下面是程序的源码:
package main
import(
"fmt"
)
func main() {
fmt.Println("Hello World")
}
第一部分,声明这是一个包。可执行文件的包名必须是“main”。
第二部分,导入包。所有的库,包括自己的库,用之前都需要导入。fmt是包名,里面有一个方法可以向终端打印信息。
第三部分,定义了一个函数。必须要定义一个main函数,这是个入口函数。
最后在cmd命令行里运行一下:
go run hello.go
做加法
这次写一个有参数有返回值的函数:
package main
import(
"fmt"
)
func add(a int, b int) int {
var sum int
sum = a + b
return sum
}
func main(){
var c int
c = add(11, 22)
fmt.Println("11 + 22 =", c)
}
go是一个强类型语言,所有变量都要设置类型。
add函数,写参数的时候,参数后面要用类型。并且由于add函数有返回值,返回值的类型也要在函数名后面定义好。
声明变量用var,后面是变量名,在后面是变量的类型。
最后如果你的变量定义之后,没有被应用,也是会报错的。这样强制你去掉冗余的代码,这个变量既然没有用到,自然就不需要出现,比如把最后一句fmt.Println(c)去掉或者换成打印一个固定的字符串就会报错。
golang语言特性
- 垃圾回收
- 内存自动回收,不需要开发人员管理内存
- 开发人员专注业务实现,减轻负担
- 只需要new分配内存,不需要释放
- 天然并发
- 从语言层面支持并发,非常简单
- goroute,轻量级线程,创建成千上万个goroute成为可能
- 基于CSP(Communicating Sequential Process)模型实现
- channel
- 管道,类似Linux中的pipe
- 多个goroute之间通过channel进行通信
- 支持任何类型
- 多返回值
- 一个函数返回多个值
并发代码演示
先写一个程序goroute_print.go,很简单,就是传入参数稍微计算一下,然后打印结果:
package main
import (
"fmt"
)
func goroute_print(a int) {
b := a + 10000
fmt.Println(b)
}
这里用了 “:=” ,是声明并赋值,并且系统自动推断类型,不需要var关键字。
然后再写一个程序goroute_for.go,里面是一个for循环,起若干个线程执行:
package main
import(
"time"
)
func main(){
for i := 0; i < 1000; i++ {
go goroute_print(i)
}
// sleep1秒,保证上面的子线程运行结束
time.Sleep(time.Second * 5)
}
在这个主程序要里调用另外一个文件里的程序。不过这2个文件都是同一个包,main包,开头package声明的。这里在前面加上了go调用,就是起了一个goroute。(不加go调用,就是在主线程里调用函数,如果调用前加上go,就是起了一个子线程调用这个函数)
主线程必须要等待子线程运行结束才能退出,否则主线程一旦结束,其他子线程也就没有了。这里简单粗暴的先用sleep来进行等待。导入了一个time包。参数 time.Second 就是1秒,如果不是等1秒而是几秒的话,就乘以几。
这里还有注释,注释的用法js一样。单行注释以 // 开头,多行注释以 /* 开始,以 */ 结尾。
执行的话在cmd命令行执行下面的命令:
go run goroute_for.go goroute_print.go
这个例子分在两个文件里了,所以run后面把两个文件都要写上。顺序是无所谓的。
管道代码示例
这里用了make()函数,是用来分配内存的。第一个参数 chan int
,表示要创建的是chan(管道),类型是int:
package main
import(
"fmt"
)
func main(){
pipe := make(chan int, 3) // 创建一个管道,存的数据类型是int,容量是3
pipe <- 11 // 往管道里存一个数
fmt.Println(11, len(pipe)) // 打印长度
pipe <- 22
fmt.Println(22, len(pipe))
var t1 int
t1 =<- pipe // 从管道里取出一个数,赋值给t1
fmt.Println(t1, len(pipe))
t1 =<- pipe
fmt.Println(t1, len(pipe))
}
似乎就是个队列,先进先出,默认满了也会阻塞。之所以叫管道不叫队列,可能是它能支持多个goroute之间通过channel(管道)进行通信。暂时不需要这么深入。
通过管道在goroute之间传递参数
有两种方式可以实现。把管道定义为全局变量,或者把管道作为函数的参数传递。代码中使用全局变量不是好的做法,所以推荐用传参的方法。不过两种实现方法都看一下。
全局变量的代码示例:
package main
import(
"fmt"
)
var pipe chan int // 在全局声明pipe变量,下面两个方法里都会用到
func add(a int, b int){
sum := a + b
pipe <- sum
}
func main(){
pipe = make(chan int, 1)
go add(1, 2)
sum :=<- pipe
fmt.Println("sum =", sum)
}
传递参数的代码示例:
package main
import(
"fmt"
)
func add(a int, b int, c chan int){
sum := a + b
c <- sum
}
func main(){
var pipe chan int // 在main函数里声明pipe
pipe = make(chan int, 1)
go add(3, 4, pipe)
sum :=<- pipe
fmt.Println("sum =", sum)
}
多返回值的代码示例
package main
import(
"fmt"
)
func calc(a int, b int)(int, int){
c1 := a + b
c2 := (a + b)/2
return c1, c2
}
func main(){
sum , avg := calc(100, 200)
fmt.Println("sum =", sum, "avg =", avg)
}
如果多返回值里只想取其中部分的值,可以把剩下的值传给占位符(“_”):
package main
import(
"fmt"
)
func calc(a int, b int)(int, int){
c1 := a + b
c2 := (a + b)/2
return c1, c2
}
func main(){
sum , _ := calc(100, 200)
fmt.Println("sum =", sum)
fmt.Println(_) // 打印下划线会报错
}
尝试打印下划线的报错信息如下:
>go run calc.go
# command-line-arguments
.\calc.go:17:16: cannot use _ as value
代码格式化(gofmt)
go默认提供一个代码格式化命令 gofmt
,会按照Go语言代码规范格式化文件中的代码。把之前的hello.go的代码搞乱一点,简单一点就改下缩进好了,然后运行下面的命令:
gofmt hello.go
最终还是能再命令行里看到整洁的代码
还截图的效果,缩进是8个字符的位置。
不过原文件里还是乱糟糟的,可以加上-w参数,改写原文件:
gofmt -w hello.go
实际写到文件里,缩进用的不是空格是Tab,具体效果是几个字符就看文件的设置了。
另外换行是 \n
,这个windows的记事本下会用问题,记事本的换行是 \r\n
。
编译Go源文件
创建Go工作目录:H:\Go\src\go_dev\day1\hello\。把之前写的示例hello.go放到hello目录下。再建个文件夹,其他示例的源文件放到examples目录下。这里src文件夹的名字是不能变的,其他文件夹的名字包括上级的都随意。
上面的演示都没有对go的原文件进行编译,编译要用 go build 命令:
H:\Go>go build go_dev\day1\hello
can't load package: package go_dev/day1/hello: cannot find package "go_dev/day1/hello" in any of:
D:\Go\src\go_dev\day1\hello (from $GOROOT)
C:\Users\Steed\go\src\go_dev\day1\hello (from $GOPATH)
H:\Go>
这里并没有编译成功,原因是找不到我们的源文件。之前因为没设置环境变量GOPATH,所以默认没有设置GOPATH就是把用户家目录作为GOPATH。这个GOPATH就是工作目录。
先去把GOPATH的环境变量设置好,我这里 “H:\Go\“ 就是我的工作目录,设置成:“H:\Go\“ 。建议设置用户变量,当然系统变量也没问题。
完成之后要新开一个cmd,也有可能有要注销当前用户,下次登录才能完全生效。然后可以验证一下:
C:\Users\Steed>go env GOPATH
H:\Go\
然后再编译一下,这次随便找个目录执行命令:
J:\tmp>go build go_dev\day1\hello
J:\tmp>hello.exe
Hello World
命令执行后会在当前目录下生成hello.exe的可执行文件,上面也直接执行了一下。文件名以及生成文件的路径都是可以用参数指定的。
前面只所以不把所有的示例都放一起,是因为其他示例也有main入口函数,存在多个入口函数编译会报错。
其实可以编译单个文件,但是路径要写全(绝对路径或相对路径),单文件编译似乎不受环境变量影响:
J:\tmp>go build H:\Go\src\go_dev\day1\hello\hello.go
复杂一点的程序一般都不是单个文件的,所以一个文件夹里就是一个小项目,只有一个入口函数。这样编译不会有问题。另外如果使用开发工具的话,这种情况开发工具直接也会提示错误的。
最后,下面这么搞比较方便:
H:\Go\src>go build go_dev\day1\hello
H:\Go\src>go build go_dev\day1\hello\hello.go
选好cmd的启动路径,之后源文件的路径用Tab就能自动补全出来了。
包的概念
代码不能单独存在,任何代码都属于一个包。所以第一行代码,一定写的是package。
- 把相同功能的代码放到一个目录,称之为包。
- 包可以被其他包调用。
- main包是用来生成可执行文件的,没个程序只有一个main包
- 包的主要用途是提高代码的可复用性。
使用多个包的代码示例
新建个文件夹package_exampl,写一个由多个包多个文件构成的代码,具体如下:
// go_dev/day1/package_example/calc/add.go
package calc
func Add(x int, y int) int {
return x + y
}
// go_dev/day1/package_example/calc/sub.go
package calc
func Sub(x int, y int) int {
return x - y
}
// go_dev/day1/package_example/main/main.go
package main
import(
// "calc"
"go_dev/day1/package_example/calc"
"fmt"
)
func main(){
sum := calc.Add(100, 200)
sub := calc.Sub(10, 5)
fmt.Println(sum, sub)
}
calc包里的Add和Sub函数都是首字母大写的。在Go语言里首字母大写有特殊的意义,就是全局变量。如果都小写的话,在main函数里这两个函数,由于作用域的问题,都是不可见的。
结论:就是说一个变量(包括函数),如果要被外部的包调用,必须要首字母大写。不过变量不推荐这么做,因为这样这个变量就是全局变量,而全局变量是不推荐使用的方法(别的语言里也同样是不推荐使用全局变量的)。
main函数里导入包的时候,calc包的路径要写全,不能只写 “calc” ,下面会具体分析。
编译过程
先把import改成这样,一步一步排错:
import(
"calc"
// "go_dev/day1/package_example/calc"
"fmt"
)
多个包的编译
H:\Go\src>go build go_dev\day1\package_example
can't load package: package go_dev/day1/package_example: no Go files in H:\Go\src\go_dev\day1\package_example
这里报错,因为文件夹下确实没有包,包的源文件在下一级的目录里:
H:\Go\src>go build go_dev\day1\package_example\main
go_dev\day1\package_example\main\main.go:4:5: cannot find package "calc" in any of:
D:\Go\src\calc (from $GOROOT)
H:\Go\src\calc (from $GOPATH)
继续报错,这是找不到calc包,并且这里也看到了默认查找包的路径。现在包的路径应该怎么写全就清除了,把import的内容改回来,之后就能顺利的编译执行了:
H:\Go\src>go build go_dev\day1\package_example\main
H:\Go\src>main.exe
300 5
指定路径和文件名
一般编译后生成的可执行文件是放在工作目录下的bin目录下的。还是用go build命令,加上-o参数,windows系统下得加上扩展名exe之后才能执行:
H:\Go\src>go build -o ../bin/test.exe go_dev\day1\package_example\main
H:\Go\src>cd ../bin
H:\Go\bin>test
300 5
H:\Go\bin>
线程和管道的代码示例
程序分成2部分。一部分是业务逻辑,放在goroute目录下。还有一部分是函数的入口,放在main目录下。文件结构:
H:\GO\SRC\GO_DEV\DAY1\GOROUTE_EXAMPLE
├─goroute
└─main
业务逻辑就是,获取3个变量,2个数和1个管道,把2个数的和计算后,存到管道里。
主函数里则是,单独起一个线程goroute进行计算,之后从管道里获取到计算结果,打印出来。
这个例子里用到了线程goroute,以及管道用于goroute之间的通信。之前已经演示过了,这里就是把代码分成2个包再写一遍:
// goroute/add.go
package goroute
func Add(a int, b int, c chan int){
sum := a + b
c <- sum
}
// goroute/main.go
package main
import(
"go_dev/day1/goroute_example/goroute"
"fmt"
)
func main(){
var pipe chan int
pipe = make(chan int, 1)
go goroute.Add(100, 200, pipe)
res := <- pipe
fmt.Println(res)
}
官网 Go 指南
都是中文版的。
Go 语言之旅:https://tour.go-zh.org/welcome/1
Go 编程语言:https://go-zh.org/
Go 语言之旅可以作为入门教程,并且可以Web在线写代码,提交执行在Web上显示结果。
课后作业
使用fmt分别打印字符串、二进制、十进制、十六进制、浮点数。
有疑问加站长微信联系(非本文作者)