4.4 实现聊天室:项目组织和基础代码框架
一个项目,目录结构如何组织,各个语言似乎有自己的一套约定成俗的东西,比如了解 Java 的应该知道,Java Web 几乎是固定的目录组织方式。Go 语言经过这几年的发展,慢慢的也会有自己的一些目录结构组织方式。
4.4.1 聊天室项目的组织方式
本书的聊天室项目不复杂,所以项目的组织结构也比较简单,目录结构如下:(读者在本地创建类似的目录结构,方便跟着动手实现)
├── README.md
├── cmd
│ ├── chatroom
│ └── main.go
├── go.mod
├── go.sum
├── logic
│ ├── broadcast.go
│ ├── message.go
│ └── user.go
├── server
│ ├── handle.go
│ ├── home.go
│ └── websocket.go
└── template
└── home.html
相关目录说明如下:
- cmd:该目录几乎是 Go 圈约定俗成的,Go 官方以及开源界推荐的方式,用于存放 main.main;
- logic:用于存放项目核心业务逻辑代码,和 service 目录是类似的作用;
- server:存放 server 相关代码,虽然这是 WebSocket 项目,但也可以看成是 Web 项目,因此可以理解成存放类似 controller 的代码;
- template:存放静态模板文件;
关于 main.main,即包含 main 包 和 main 函数的文件(一般是 main.go)放在哪里,目前一般有两种做法:
1)放在项目根目录下。这样放有一个好处,那就是可以方便的通过 go get 进行安装。比如 github.com/polaris1119/golangclub ,按这样的方式安装:
$ go get github.com/polaris1119/golangclub
成功后在 $GOBIN
(未设置时取 $GOPATH[0]/bin
)目录下会找到 golangclub 可执行文件。但如果你的项目不止一个可执行文件,也就是会存在多个 main.go,这种方式显然没法满足需求。
2)创建一个 cmd 目录,专门放置 main.main,有些可能会直接将 main.go 放在 cmd 下,但这又回到了上面的方式,而且还没上面的方式方便。一般建议项目存在多个可执行文件时,在 cmd 下创建对应的目录。因为前面章节的需要,在项目 chatroom 中,cmd 下有了三个目录:tcp、websocket 和 chatroom。对于这种方式,通过 go get 可以这样安装:
$ go get -v github.com/go-programming-tour-book/chatroom/cmd/...
为了演示方便,我们的 tcp 和 websocket 同时包含了 server 和 client,相当于一个目录下有两个 main.main,所以用这种方式安装会报错,错误信息类似这样:
../../../../go/pkg/mod/github.com/go-programming-tour-book/chatroom@v0.0.0-20200412113309-9f22642e72e5/cmd/tcp/server.go:16:6: main redeclared in this block
previous declaration at ../../../../go/pkg/mod/github.com/go-programming-tour-book/chatroom@v0.0.0-20200412113309-9f22642e72e5/cmd/tcp/client.go:13:6
# github.com/go-programming-tour-book/chatroom/cmd/websocket
previous declaration at ../../../../go/pkg/mod/github.com/go-programming-tour-book/chatroom@v0.0.0-20200412113309-9f22642e72e5/cmd/websocket/client.go:12:6
所以,我们这个聊天室项目,可以用下面这种方式安装:
$ go get -v github.com/go-programming-tour-book/chatroom/cmd/chatroom
4.4.2 基础代码框架
接下来看看具体的代码实现。
1、main.go 的代码如下:
var (
addr = ":2022"
banner = `
____ _____
| | | /\ |
| |____| / \ |
| | | /----\ |
|____| |/ \ |
Go 语言编程之旅 —— 一起用 Go 做项目:ChatRoom,start on:%s
`
)
func main() {
fmt.Printf(banner+"\n", addr)
server.RegisterHandle()
log.Fatal(http.ListenAndServe(addr, nil))
}
该项目直接使用标准库 net/http 来启动 HTTP 服务,Handle 的注册统一在 server 包中进行。
大家以后项目中,可以试试 banner 的打印,感觉挺酷的。
2、server.RegisterHandle
在 server/handle.go 中,加上如下代码:
func RegisterHandle() {
inferRootDir()
// 广播消息处理
go logic.Broadcaster.Start()
http.HandleFunc("/", homeHandleFunc)
http.HandleFunc("/ws", WebSocketHandleFunc)
}
该函数内的四行代码,前两行其实并非是 Handle 的注册。
一般来说,项目中会需要读文件,比如读模板文件、读配置文件、数据文件等。为了能够准确的找到文件所在路径,在程序中应该尽早推断出项目的根目录,之后读其他文件,通过该根目录拼接绝对路径读取。inferRootDir 函数就是负责推断出项目根目录。具体看看推断的逻辑:
var rootDir string
// inferRootDir 推断出项目根目录
func inferRootDir() {
cwd, err := os.Getwd()
if err != nil {
panic(err)
}
var infer func(d string) string
infer = func(d string) string {
// 这里要确保项目根目录下存在 template 目录
if exists(d + "/template") {
return d
}
return infer(filepath.Dir(d))
}
rootDir = infer(cwd)
}
func exists(filename string) bool {
_, err := os.Stat(filename)
return err == nil || os.IsExist(err)
}
- 通过 os.Getwd() 获取当前工作目录;
- infer 被递归调用,判断目录 d 下面是否存在 template 目录(只要是项目根目录下存在的目录即可,并非一定是 template);
- 如果 d 中不存在,则在其上级目录递归查找;
在项目中任意一个目录执行编译然后运行或直接 go run,该函数都能正确找到项目的根目录。
go logic.Broadcaster.Start()
启动一个 goroutine 进行广播消息的处理,具体内容后文再讲。
最后两行代码:
http.HandleFunc("/", homeHandleFunc)
http.HandleFunc("/ws", WebSocketHandleFunc)
用于注册 “/” 和 “/ws” 两个路由,其中 “/” 代表首页,"/ws” 用来服务 WebSocket 长连接。
至此咱们代码的基础框架或者说项目启动涉及到的流程就基本完成了。下节将讲解聊天室的核心处理流程。