购买
下载掌阅APP,畅读海量书库
立即打开
畅读海量书库
扫码下载掌阅APP

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语言中一种并发安全的数据类型。 tqzPN2nvOqLMMZ+hXZwTlxb3/ZHIMzAzEc+5CHA7mDpKge0MQOSfeM160W+wDlfT

点击中间区域
呼出菜单
上一章
目录
下一章
×