2.2.1 “hello world”程序的代码说明
笔者使用的集成开发环境是Goland。首先创建工程golang-1,并使用Go Module进行管理,可使用如下命令初始化项目。
$ go mod init goang-1
go: creating new go.mod: module golang-1
代码结构如下。
golang-1/intro-golang/helloworld/v1
$ tree .
.
├── main.go
└── mytask
├── mystruct.go
└── taskprocess.go
1 directory, 3 files
1.源码文件mystruct.go
下面展示的是源码文件mystruct.go中的内容,此代码所在的位置是golang-1/intro-golang/ helloworld/v1/mytask/mystruct.go。
package mytask
import "time"
const (
LogFilePath = "./my.log"
)
//定义了结构体MyTask
type MyTask struct {
InPath string //读取的路径
OutPath string //写入的路径
ReadChannel chan string //HandleLog从ReadChannel通道读取数据
WriteChannel chan *EveryoneDoIT //HandleLog处理完后,将数据传入WriteChannel通道
}
type EveryoneDoIT struct {
User, DoSth string // 用户,干什么事情
TimeLocal time.Time //本地时间
SpendTime float64 //花费的时间
}
代码说明如下。
(1)Go语言以包作为管理单位,在每个源码文件的顶部都必须先声明包。同一个包下可以有多个源码文件,文件中的函数、常量和结构体都可以被直接调用。在上述源码文件中,以mytask作为包名,写法为package mytask。
(2)在调用函数之前,必须先使用关键字import导入要使用的软件包。上述源码文件使用了与时间相关的函数,因此需要使用代码import "time"导入标准库time。
(3)在上述源码文件中可以看到典型的代码布局,从上到下依次为package语句、import语句和实际的程序代码。
(4)代码const (...)是Go语言中定义常量的方法。
(5)定义结构体的语法是“type结构体名字struct”,这里结构体的名字是MyTask。Go语言中没有“类”,它使用“结构体+方法”来代替面向对象语言中“类”的概念。
(6)结构体中的InPath string、OutPath string是该结构体定义的一些基本类型属性。请注意,在Go语言中,以大写字母开头的变量或者方法才可以在其他包中被引用。
(7)代码ReadChannel chan string表示Go语言中的通道数据类型。
(8)代码type EveryoneDoIT struct定义了另外一个结构体,用于封装处理后的原始数据。
2.源码文件main.go
下面展示的是源码文件main.go中的内容,此代码所在的位置是golang-1/intro-golang/helloworld/ v1/main.go。
package main
import (
"time"
"golang-1/intro-golang/helloworld/v1/mytask"
)
func main() {
//初始化一个MyTask实例
myTask := &mytask.MyTask{
InPath: mytask.LogFilePath,
OutPath: "",
ReadChannel: make(chan string),
WriteChannel: make(chan *mytask.EveryoneDoIT),
}
//循环,以下三个环节每次都会并发执行
for{
go myTask.ReadFromFile()
go myTask.HandleLog()
go myTask.WriteToDB()
//阻塞
time.Sleep(time.Second)
}
}
代码说明如下。
(1)这里根据mystruct.go中的定义初始化了MyTask对象。main.go文件属于main包。要访问mytask包中的结构体MyTask,必须在main.go文件中导入mytask包。此项目使用了包管理工具Go Module,开头的路径golang-1用于初始化Go Moudle的模块名字,被导入的软件包将被标记为import "golang-1/intro-golang/helloworld/v1/mytask"。
(2)main包中的main函数是项目的入口函数。每个需要运行的Go程序都要有一个main包,因为程序要从main.main函数开始。当然,其文件名不一定叫main.go,只要保证包名是main即可。
(3)在main函数中,我们执行了三个方法go ReadFromFile、go HandleLog和go WriteToDB。注意前面的关键字go,使用该关键字意味着开启了一个协程。在Go语言中,协程的执行采用的是抢占机制。
(4)最后注意time.Sleep函数。因为main函数是一个主协程,所以当main函数执行结束时程序就退出了,它不会等其他三个方法的协程执行完,在上述代码中使用time.Sleep函数是为了让主协程等待其他协程。另外,为了让这三个协程不停地执行任务,这里使用了for循环。当然,还有更好的方法控制协程的生命周期,这将在后面的章节中讲解。
3.源码文件taskprocess.go
下面展示的是源码文件taskprocess.go中的内容,此源码所在的位置是golang-1/intro-golang/ helloworld/v1/mytask/taskprocess.go。
package mytask
import (
"fmt"
"math/rand"
"strconv"
"strings"
"time"
)
func init() {
rand.Seed(time.Now().UnixNano())
}
//读取数据
//将模拟生成的数据通过ReadChannel传递给Process goroutine
func (my *MyTask) ReadFromFile() {
//模拟创建数据,格式为"老板-/Bug-881.763s"
users := []string{"前端工程师", "后端工程师", "架构师", "老板"}
user := users[rand.Intn(len(users))]
doSths := []string{"/Bug", "/code", "/markdown", "/ppt", "/search"}
doSth := doSths[rand.Intn(len(doSths))]
spendTime := rand.Float64() * 1000
content := fmt.Sprintf("%s-%s-%.3f\n", user, doSth, spendTime)
my.ReadChannel <- content
}
//将从ReadChannel处获取的数据以"-"分割,
//并将分割后的数据重新组装为一个EveryoneDoIT对象,
//再把EveryoneDoIT对象通过WriteChannel传递给Write goroutine
func (my *MyTask) HandleLog() {
msg := &EveryoneDoIT{}
ret:=strings.Split(<-my.ReadChannel,"-")
msg.User=ret[0]
msg.DoSth=ret[1]
f,_:= strconv.ParseFloat(ret[2],64)
msg.SpendTime=f
now := time.Now()
loc, _ := time.LoadLocation("Asia/Shanghai")
dateTime, err := time.ParseInLocation("02/Jan/2006:15:04:05",
now.Format("02/Jan/2006:15:04:05"), loc)
if err!=nil{
panic(err)
}
msg.TimeLocal=dateTime
my.WriteChannel <- *msg
}
//写入方法,这里仅做演示,将写入数据库的逻辑简化为从WriteChannel处
//获取上一步传递过来的EveryoneDoIT,并输出
func (my *MyTask) WriteToDB() {
fmt.Println(<-my.WriteChannel)
}
代码说明如下。
(1)注意上述源码中第一行的包名依然是mytask,因为在同一级目录下只能有一个包名。
(2)业务逻辑都在taskprocess.go源码文件中。
(3)为什么在main.go的main函数中,我们可以直接以“myTask.函数名”的方式来调用ReadFromFile、HandleLog和WriteToDB函数呢?这是因为这些函数前面添加了代码(my *MyTask),这表示这些函数都是*MyTask的函数。通常情况下,我们应该使用带指针的“*MyTask.函数名”来调用这些函数,但这里有一个语法糖会自动进行转换,所以我们可以直接使用不带指针的“mytask.函数名”来调用。
(4)ReadFromFile函数用于模拟生成格式为“老板-/Bug-881.763s”的数据,然后通过通道将生成的数据传递到go HandleLog协程中。对于并发,Go语言的设计哲学是:不要通过共享内存来通信,而应该通过通信来共享内存。因此,这里使用通道进行数据传输。
(5)ReadFromFile函数用于模拟将读取的数据通过通道my.ReadChannel传递给go HandleLog协程进行处理。在HandleLog函数中,从通道my.ReadChannel中接收传输过来的数据,然后进行一定的处理,处理完以后又将新数据封装到对象EveryoneDoIT中,并通过通道my.WriteChannel传递给go WriteToDB协程。
(6)WriteToDB函数打印并输出从通道WriteChannel中传递过来的数据。至此,数据的处理流程结束。
(7)上述3个函数在main.main函数中是以协程的方式启动的,所以执行时没有固定的先后顺序。
(8)注意代码“dateTime, err := time.ParseInLocation”。因为Go语言允许函数返回多个值,所以在使用Go语言的标准库或者第三方库时,常常会遇到第一个为返回值,第二个为错误值的多返回值形式。对于这种情况,第一步是先判断返回的错误值是否为nil,只有返回的err为nil时,才会执行下一步逻辑。
(9)如果返回的err不为nil,则说明存在错误的执行逻辑。这也是Go语言的特色语法,与Java中的异常抛出相似。
注意:通道是Go语言中一种并发安全的数据类型。