重要的话说3遍
使用gofmt
使用gofmt
使用gofmt
gofmt是golang提供的代码格式化工具,整个团队使用,就不需要做代码风格审查了
// ListDirectory returns the contents of dir.
func ListDirectory(dir string) ([]string, error)
// ListDirectory returns a channel over which
// directory entries will be published. When the list
// of entries is exhausted, the channel will be closed.
func ListDirectory(dir string) chan string
这两个函数显著的区别是,第一个例子读取目录到切片,然后将整个切片返回,否则如果有问题则返回一个错误。这是同步发生的,ListDirectory的调用者将被阻塞直到整个目录被读完。依赖于目录有多大,这个过程可能持续很长时间,也可能因为构建一个目录条目名称的切片,而分配大量的内存。
看第二个例子。这更一个更像Go,ListDirectory返回了一个传输目录条目的通道,当通道关闭时,表明没有更多目录条目了。由于通道信息发生在ListDirectory返回之后,ListDirectory内部可能开启了一个协程。
通道版本的ListDirectory还有两个进一步的问题:
func ListDirectory(dir string, fn func(string))
最佳实践:filepath.Walk
总结:如果你的函数开启了一个协程,那么你必须给调用者提供一个停止协程的途径。将异步执行函数的决策留给该函数的调用者通常更容易
package main
import (
"fmt"
"net/http"
_ "net/http/pprof"
)
func main() {
mux := http.NewServeMux()
mux.HandleFunc("/", func(resp http.ResponseWriter, req *http.Request) {
fmt.Fprintln(resp, "Hello, QCon!")
})
go http.ListenAndServe("127.0.0.1:8001", http.DefaultServeMux) // debug
http.ListenAndServe("0.0.0.0:8080", mux) // app traffic
}
上述代码优化后如下
func serveApp() {
mux := http.NewServeMux()
mux.HandleFunc("/", func(resp http.ResponseWriter, req *http.Request) {
fmt.Fprintln(resp, "Hello, QCon!")
})
http.ListenAndServe("0.0.0.0:8080", mux)
}
func serveDebug() {
http.ListenAndServe("127.0.0.1:8001", http.DefaultServeMux)
}
func main() {
go serveDebug()
serveApp()
}
通过将 serveApp 和 serveDebug 的功能放到到各自的函数中,我们把他们从 main.main 分离出来。我们照样遵循了上面的建议,把serveApp和serveDebug 的并发性留给了调用者
但是这个程序有一些可操作性上的问题。如果serveApp返回则main.main会返回并导致程序关闭,最终由您正在使用的任何进程管理器重新启动。
正如函数的并发性留给调用者一样,应用应该将状态监视、重启留给程序的唤起者。不要让你的应用程序担负重启自身的责任,这是一个最好从应用程序外部处理的过程。
func serveApp() {
mux := http.NewServeMux()
mux.HandleFunc("/", func(resp http.ResponseWriter, req *http.Request) {
fmt.Fprintln(resp, "Hello, QCon!")
})
if err := http.ListenAndServe("0.0.0.0:8080", mux); err != nil {
log.Fatal(err)
}
}
func serveDebug() {
if err := http.ListenAndServe("127.0.0.1:8001", http.DefaultServeMux); err != nil {
log.Fatal(err)
}
}
func main() {
go serveDebug()
go serveApp()
select {}
}
现在我们通过必要时调用 log.Fatal 来检查 serverApp 和 serveDebug 从 ListenAndServe 返回的错误。由于两个处理器都是在协程中运行,我们使用 select{} 来阻塞主协程。
这种方法存在许多问题:
如果 ListenAndServe 返回一个 nil,log.Fatal不会被调用,则对应的 HTTP 服务会停止,并且应用程序不会退出。
log.Fatal 会调用 os.Exit无条件终止进程,defer不会被调用,其他协程不会被通知关闭,应用程序会停止。这会使得为这些函数编写测试用例变得很困难。
只在 main.main 或 init 函数里使用 log.Fatal
们需要的是,把任何错误都传回协程的发起者,以便于我们弄清楚为什么协程会停止,并且可以干净地关闭进程。
func serveApp() error {
mux := http.NewServeMux()
mux.HandleFunc("/", func(resp http.ResponseWriter, req *http.Request) {
fmt.Fprintln(resp, "Hello, QCon!")
})
return http.ListenAndServe("0.0.0.0:8080", mux)
}
func serveDebug() error {
return http.ListenAndServe("127.0.0.1:8001", http.DefaultServeMux)
}
func main() {
done := make(chan error, 2)
go func() {
done <- serveDebug()
}()
go func() {
done <- serveApp()
}()
for i := 0; i < cap(done); i++ {
if err := <-done; err != nil {
fmt.Println("error: %v", err)
}
}
}
可以使用一个通道来收集协程返回的状态。通道的大小与我们要管理的协程数一致,从而使得向 done 通道发送状态时不会被阻塞,否则这将阻塞协程的关闭,导致泄漏。
由于没有办法安全地关闭 done 通道,我们不能使用 for range 循环通道知道所有协程都上报了信息,因此我们循环开启协程的次数,这也等于通道的容量。
现在我们有办法等待协程干净地退出,并且记录发生的日志。我们所需的仅仅是将一个协程的关闭信号,通知到其他协程而已。
其结果是,通知一个http.Server关闭这事被引入进来。所以我将这个逻辑转换为辅助函数。serve帮助我们持有一个地址和一个http.Handler,类似http.ListenAndServe 以及一个用于触发 Shutdown 方法的 stop 通道。
func serve(addr string, handler http.Handler, stop <-chan struct{}) error {
s := http.Server{
Addr: addr,
Handler: handler,
}
go func() {
<-stop // wait for stop signal
s.Shutdown(context.Background())
}()
return s.ListenAndServe()
}
func serveApp(stop <-chan struct{}) error {
mux := http.NewServeMux()
mux.HandleFunc("/", func(resp http.ResponseWriter, req *http.Request) {
fmt.Fprintln(resp, "Hello, QCon!")
})
return serve("0.0.0.0:8080", mux, stop)
}
func serveDebug(stop <-chan struct{}) error {
return serve("127.0.0.1:8001", http.DefaultServeMux, stop)
}
func main() {
done := make(chan error, 2)
stop := make(chan struct{})
go func() {
done <- serveDebug(stop)
}()
go func() {
done <- serveApp(stop)
}()
var stopped bool
for i := 0; i < cap(done); i++ {
if err := <-done; err != nil {
fmt.Println("error: %v", err)
}
if !stopped {
stopped = true
close(stop)
}
}
}
现在,每当我们从 done 通道接收到一个值,就关闭stop通道,从而导致所有等待在这个通道上的协程关闭http.Server。这将导致所有剩余的ListenAndServe协程返回。一旦我们启动的协程停止,main.main 便返回继而进程干净地停止了。
少量字符串拼接效率最高的是+使用也最简单,效率最差的是fmt.Sprint,但是每次+操作会导致内存重新分配,这时builder的优势就展现出来了。
在函数签名中不要混用可为 nil 和不可为 nil 的参数
http.ListenAndServe("0.0.0.0:8080", nil)
http.ListenAndServe("0.0.0.0:8080", http.DefaultServeMux)
两个方式都做完全一样的事情。
这种 nil的行为是病毒式的。在http包中同样有个http.Serve帮助类,您可以合理地想象 ListenAndServe 是这样建立的:
func ListenAndServe(addr string, handler Handler) error {
l, err := net.Listen("tcp", addr)
if err != nil {
return err
}
defer l.Close()
return Serve(l, handler)
}
因为ListenAndServe允许调用者为第二个参数传递nil,所以http.Serve也支持这种行为。事实上,http.Serve 是“当 handler 为 nil,则使用 DefaultServeMux”这个逻辑的一个实现。允许其中一个参数传入 nil 可能导致调用者以为他们可以给两个参数都传入 nil(Austin Luo:调用者可能想,既然第二个参数有默认实现,那第一个参数可能也有),但像这样调用:
http.Serve(nil, nil)
将导 panic 。
强制调用者应该传递至少一个参数。我们可以像这样组合常规参数和可变参数:
func anyPositive(first int, rest ...int) bool {
if first > 0 {
return true
}
for _, v := range rest {
if v > 0 {
return true
}
}
return false
}
func Max(a, b int) int
func CopyFile(to, from string) error
上述代码对于函数Max来说,参数的位置并不会影响到函数的结果,但是CopyFile就会对顺序有要求,那么这种情况下怎么写出可读性更高的代码呢?废话不多说,上代码:
type Source string
func (src Source) CopyTo(dest string) error {
return CopyFile(dest, string(src))
}
func main() {
var from Source = "presentation.md"
from.CopyTo("/tmp/backup")
}
这样 CopyFile 就总是可以被正确地调用——这也可以通过单元测试确定,也可以被设置为私有,进一步降低了误用的可能性。
比如:
type Config struct {
//
}
func WriteConfig(w io.Writer, config *Config)
将Config参数命名为config是多余的,我们知道它是个Config,函数签名上写得很清楚。
在这种情况建议考虑conf或者c——如果生命周期足够短的话。
如果在一个范围内有超过一个*Config,那命名为conf1、conf2的描述性就比original、updated更差,而且后者比前者更不容易出错。
1.只声明,不初始化时,使用var。避免将包命名为base、common、util;
针对优化utils或helpers这种包名,我的建议是分析它们是在哪里被使用,并且是否有可能把相关函数挪到调用者所在的包。即便这可能导致一些重复的帮助类代码,但这也比在两个包之间引入一个导入依赖来的更好
2.既声明,也显式地初始化时,使用:=。
特定类型除外
length := uint32(0x80)
以包“提供”的东西来命名,而不是以“包含”的东西来命名
每个 Go Package 事实上自身都是一个小的 Go 程序。
模块不向外部透露任何不必要的信息,也不依赖外部模块的实现。
一个好的 Go Package应该致力于较低的源码级耦合,这样,随着项目的增长,对一个包的更改不会级联影响其他代码库。那些“世界末日”似的重构让代码的更新优化变得极其困难,也让工作在这样的代码库上的开发者的生产效率极度地受限。
编写可维护的程序的一个关键方面是松耦合——更改一个包,应该把对没有直接依赖它的包的影响降到最低。
在 Go 中有两种很好的方法可以实现松散耦合:
1.使用接口来描述函数或方法所需的行为。
2.避免使用全局状态
全局变量对每个函数都是可见的,但开发者可能意识不到全局变量的存在(即隐匿的参数),即使意识到并使用了全局变量,也可能意识不到该变量可能在别处被修改,导致全局变量的使用不可靠,依赖该变量状态(值)的函数被打破。
如果您想减少全局变量带来的耦合,那么:
将相关变量作为字段移动到需要它们的结构上。
使用接口来减少行为与该行为的实现之间的耦合
一个 Java 包等效于一个独立的 .go 源文件;一个 Go 包等效于整个 Maven 模块或 .NET 程序集
从单一的 .go 文件开始,并且使用与包相同的名字。比如包http的第一个文件应该是 http.go,并且放到名为 http 的文件夹中。
随着包的逐渐增长,您可以根据职责拆分不同的部分到不同的文件。例如,将 Request 和 Response 类型拆分到 message.go 中,将 Client 类型拆分到 client.go 中,将 Server 类型拆分到 server.go 中。
如果您发现您的文件具有很相似的import声明时,考虑合并它们,否则确定两者的具体差异并优化重构它们。
不同的文件应该负责包的不同区域。messages.go 可能负责网络相关的 HTTP 请求和响应编组,http.go 可能包含低级网络处理逻辑,client.go 和 server.go 实现 HTTP 请求创建或路由的业务逻辑,等等。
对于 Go 工具集来讲,Go 包的层次结构是没有意义的。例如,net/http 并不是 net 的子或子包。
如果您创建了不包含任何 .go 文件的中间目录,则不适用此建议。
把代码放置于名为 internal/ 的目录或子目录即可。 go 命令发现导入的包中包含 internal 路径,它就会校验执行导入的包是否位于以 internal 的父目录为根的目录树中。
例如,包 …/a/b/c/internal/d/e/f 只能被根目录树 …/a/b/c 中的代码导入,不能被 …/a/b/g 或者其他任何库中的代码导入。
main 应当解析标识,打开数据库连接,初始化日志模块等等,然后将具体的执行交给其他高级对象。
条件代码块在进入函数时立即断言前置条件
假设没有明确提供显示初始化器,每个变量声明之后都会被自动初始化为零内存对应的值,这就是零值。零值与其类型有关:数值类型为0,指针为nil,切片、映射、管道等也同样(为nil)。
免责声明:
一切资料仅限用于学习和研究目的;不得将上述内容用于商业或者非法用途,否则,一切后果请用户自负。资料来自网络,版权争议与本人无关。您必须在下载后的24个小时之内,从您的电脑中彻底删除上述内容。如果您喜欢该程序,请支持正版,购买注册,得到更好的正版服务。如有侵权请邮件与我联系处理。
全网视频教程加微信a518958666获取
基于SpringCloud 微服务架构 广告系统设计与实现
系统学习docker
docker前后端分离实战
Docker+Kubernetes(k8s)微服务容器化实战
Go语言实战抽奖系统
Go语言开发分布式任务调度 轻松搞定高性能Crontab
20小时快速入门go语言
Java从零到企业级电商项目实战
SSM主流框架入门与综合项目实战
Socket网络编程进阶与实战
…
…
更多课程请加微信a518958666获取
╭══════════════════════════════════════════╮ ║
║ 说明:教程版权归原作者所有,本人只是负责搜集整理,本人 ║
║ 不承担任何技术及版权问题。本人分享的任何教程仅提 ║
║ 供学习参考,不得用于商业用途,请在下载后在24小时 ║
║ 内删除。 ║
║ ║
║ 1.请遵守中华人民共和国相关法律、条例 ║
║ 2.本人提供的各类视频教程仅供研究学习,本人不承担观看 ║
║ 本教程后造成的一切后果 ║
║ 3.本人不保证提供的教程十分安全或是完全可用,请下载后 ║
║ 自行检查 ║
║ 4.本人提供的教程均为网上搜集,如果该程序涉及 ║
║ 或侵害到您的版权请立即写信通知我们。 ║
║ 5.如不同意以上声明,请立即删除,不要使用,谢谢合作 ║
║ ║
╰═════════════════════════════════════════╯