分类目录归档:Golang

使用 fresh 来提升你的 Golang 开发效率

使用 fresh 来提升你的 Golang 开发效率

Golang 作为一个编译型语言,在编写程序时,一个不太方便的点便是每次修改完代码,都需要重新编译才能测试效果。虽然你可以使用 go run main.go 命令来运行一个 go 文件,但由于项目往往文件比较多、修改时还是需要手动输入命令比较麻烦,所以给 Golang 的开发过程带来了不少的问题。

fresh 就是一个帮助你执行一些重复命令的命令行工具,有了 fresh ,你就可以不用自己手动执行 go run main.go,它在检测到文件发上了变化后,会自动帮你中断掉当前进程,并重新执行命令,帮你实现 live-reload 的效果。

fresh 的执行效果

https://github.com/gravityblast/fresh

安装

安装比较简单,执行 如下命令后,你就可以在任何地方执行 fresh 命令了。

go get github.com/pilu/fresh

执行

当你当前目录有 main.go 文件时,直接执行 fresh 就会自动执行 main.go 文件。不过,如果你想要自定义的话,也可以通过配置文件来完成。创建一个 sample.conf 文件,贴入如下配置,并执行 fresh -c sample.conf 就可以让 fresh 按照你的配置来执行命令。

root:              .
tmp_path:          ./tmp
build_name:        runner-build
build_log:         runner-build-errors.log
valid_ext:         .go, .tpl, .tmpl, .html
no_rebuild_ext:    .tpl, .tmpl, .html
ignored:           assets, tmp
build_delay:       600
colors:            1
log_color_main:    cyan
log_color_build:   yellow
log_color_runner:  green
log_color_watcher: magenta
log_color_app:<a href="https://github.com/gravityblast/fresh#usage"></a>
三个有用的 Golang 辅助网站

三个有用的 Golang 辅助网站

在我数十年的编程生涯里,我大部分时间写的都是动态类型语言,所以我对于类型并不太感冒。不过,当我在写 Golang 的时候,确实遇到了一些过去写 PHP、 Node.js、前端不同的问题。

一个最令我困扰的点便是:对于 JSON 的处理,需要先定义结构体来做转换。对于一些简单的 JSON 结构,我还可以简单的完成定义。但如果是一些比较复杂的结构体,我就放弃了手动定义,而是选择借助一些辅助工具来完成

比如我最常用的便是各种 JSON to Go 的网站,贴一个 JSON 进去,然后自动生成对应的 Golang 的 Struct,接下来我只需要复制右侧生成的 Struct 到我的代码里,就可以完成 JSON 的定义,使用 Golang 自带的 JSON 库解析即可。

截图的网站是 https://mholt.github.io/json-to-go/

另外一个可以实现 JSON 转换成 Go Struct 的网站是 https://transform.tools/json-to-go,它提供了更加全面的转换路径。不过有些时候选择太多也会让我有点困惑。

另外一个令我困扰的也是类似的格式转换,不过是 YAML 的。思路相同,手写要配置的 YAML 文件,并生成对应的 Struct: https://zhwt.github.io/yaml-to-go/。在设计风格上,和上面的 JSON2Struct 类似,是基于上面的项目改造过来的。

除此之外,如果你在定义 YAML 的时候, 对于部分数据如何定义不太明确,可以考虑使用 JSON2Yaml 的工具来实现将你需要的数据转换为 YAML 定义。

在 Go 当中嵌入父目录中的文件

在 Go 当中嵌入父目录中的文件

自 Go 1.16 版本开始,Go 提供了将二进制文件打包进入到 Binary 文件当中的机制:`//go:embed。不过,我看到的示例大多数都是嵌入当前文件夹下的子文件夹的示例。并没有嵌入父一级文件夹的示例。于是,我便开始研究起来。

为什么会需要嵌入父目录中的文件?

这是因为不同的项目的构建规则不同。一些小型项目可能只有一个 main.go 或同级目录下几个其他的 go source 文件即可,但对于更大型的项目,合理的项目拆分是有助于帮助提升项目的可维护性的。

以我为例,我的目录结构如下:biz 目录下是我的核心逻辑,也是我日常写代码的地方;conf 则放置了各种配置,比如各种 API Key,ral 则是网络访问层,比如我比较喜欢用 gin 来做底层的网络访问层;script 放置了开发所需要的各种脚本文件;static 则放置了前端所需要的 JS / CSS 文件。在开发一些偏内容向的页面时,我会习惯使用 Server Side Render,所以我在 biz 目录下还有一个 template 目录。而主要的逻辑则放在 handler。

├── biz
│   ├── dal
│   ├── handler
│   ├── logic
│   ├── service
│   └── template
├── conf
├── ral
│   └── gin
├── script
└── static
    └── public

在日常开发时,一个非常常见的操作是在 handler 里处理基本的逻辑,并将 template 中的模板渲染出来,并返回给用户,这个时候就需要在 handler 里使用上一级目录的文件了。

错误的尝试1:使用 .. 来引入

作为文件系统的常规配置,我自然知道可以通过 .. 来代表上一级目录,因此我试着使用如下的语法来引入文件

//go:embed ../template/*

结果报了个错误 invalid pattern syntax。看得出来,是因为我的语法出现了问题。

通过搜索,我找到了这个 Github issue,才得知,由于避免跨模块的访问,go embed 阻止了 .. 语法的使用

Because embed.Files implements fs.FS, it cannot provide access to files with names beginning with .., so files in parent directories are also disallowed entirely, even when the parent directory named by .. does happen to be in the same module.

https://go.googlesource.com/proposal/+/master/design/draft-embed.md

错误的尝试2: 使用 go:generate 来执行复制

在上面的 issue 当中,我注意到还有另外一个方式,可以实现类似的效果。可以借助 go:generate来实现在执行 go generate 方法时,复制文件到本地,这样就可以实现在 Build 二进制文件之前,把文件复制到本地,即规避了跨模块的问题,也避免了将 template 文件放在本地,污染代码目录的问题。

//go:generate cp -r ../../assets ./local-asset-dir
//go:embed local-asset-dir
var assetFs embed.FS

这个方法在运行的时候没有效果,不过在其他人可能是有效果的(我猜是因为我开发时很少直接用 go generate,都是直接 go run 了)。不过,这个方法也有个问题,因为是写在代码本身当中的,很有可能后续因为某些原因,这些代码被误删除或修改了,导致整个系统的运行不正常。而散落在各文件的描述也会使得修复十分复杂。

正确的做法

在看这个 issue 的最后,我注意到了正确的用法,在另外一个 issue 当中,开发者 tomasf 提到了,我们应该在 asset 目录下创建一个 embed.go 来完成 embedFs 的建立,并在其他文件直接引入这个文件,完成定义。

You can create a embed.go file in your assets directory and make the assets available as it’s own package to the rest of your program.

tomasf

这个说法一出,豁然开朗!尝试一下,果然有效。我在 template 目录下创建了一个 embed.go 文件,并添加了如下代码。

package template

import "embed"

//go:embed *
var TemplateFs embed.FS

并在另外一个文件当中使用template.TemplateFs.ReadFile("index.tmpl") 来完成模板文件的引用。这样既不违背 golang 的跨模块,也不会使得代码不可维护,非常好。

参考阅读

  • https://go.googlesource.com/proposal/+/master/design/draft-embed.md
  • https://github.com/golang/go/issues/46056
  • https://github.com/golang/go/issues/41191#issuecomment-686621090
  • https://blog.carlmjohnson.net/post/2016-11-27-how-to-use-go-generate/
  • https://blog.carlmjohnson.net/post/2021/how-to-use-go-embed/
如何成为 Golang 贡献者

如何成为 Golang 贡献者

是的,我成为了 Golang contributor

我的第一个 contribution

在听完了 Go 夜聊的第 2 期播客后,我突然觉得,嗯,我是应该加入一个社区,而不是总是以一个创建者的身份去创建开源项目。可以有一个新的视角。

于是,我选择了加入我最常写的三种语言其一的 Golang。

然后接下来的问题就是,应该如何做贡献?

我查阅了一些资料,完成了此次贡献。并将其记录在此,希望可以帮到你。

贡献流程

0. 系统依赖

给 Golang 做贡献需要一些基本配置,这里不再赘述,只写明要求:

  • 安装了 Golang
  • 安装了 Git

1. 找到你要贡献的问题

实际上,既然要贡献,首先你要解决的问题是,你想贡献什么问题?绝大多数的贡献都是基于某一个特定的问题进行的。

2. 签署 Golang CLA,配置基本信息

golang 的贡献是基于 Git 进行的,因此,你在贡献前,需要确保你的各项基本信息是复合要求的。比如,你要在网站上签署 CLA(Contributor License Agreement);你的 Git Username 和 Git Email 应该符合和你签署 CLA 的信息是符合的。

签署完成后,可以使用你的 Google 账号登陆 https://go-review.googlesource.com/login/,注册一个账号。后续的 Code Review 也会在这里进行。

3. 配置 Password 、Git 等信息

需要注意的是,目前 Golang 无法从中国大陆提交贡献,所以你应该在一台海外服务器上进行贡献。以确保可以提交。想了解具体情况,可以查看附录中的 GitHub issue #20065

Golang 在提交时,是通过 Git 提交的,不过和一般我们常用的 HTTP 账号密码鉴权 or Key 鉴权不同,Golang 采用的是 GitCookies 进行提交的。因此,你需要配置 Git Cookies。

访问 go Git repositories – Git at Google (googlesource.com),登陆后,点击右上角的 Generate Password

点击这里

在新的页面中,复制下方蓝色框体中的 bash 代码,并粘贴在你的 Shell 中粘贴并执行,即可配置好 Git Cookies

蓝框里本来是 Shell 命令,被我 F12 给改了

4. 安装 Code Review 工具

在服务器上执行如下命令,安装代码提交工具

go get -u golang.org/x/review/git-codereview

安装完成后,执行如下命令验证

git-codereview help

5. Clone 代码并提交贡献

接下来的就很简单了,和我们日常贡献代码很接近了

5.1 clone 代码并执行测试

首先你需要 clone 代码,并执行测试,确保你 clone 的代码本身是没问题的(不然可能会出现你改了一大堆,发现问题不是你自己的)

git clone https://go.googlesource.com/go
cd go/src
./all.bash # 执行测试,如果看到 ALL TESTS PASSED 则说明代码没问题。

5.2 修改代码

修改代码按照我们一般的分支协作的方式来进行即可

git checkout -b feat/xxxx
# 修改文件...
git add [files...]
git codereview change # 为你的贡献创建一个 commit

5.3 提交代码

完成代码贡献后,你就可以准备提交你的代码了。不过,你还是要跑一次测试,确保你的修改没有导致某些地方坏掉。

./all.bash # 执行测试,如果看到 ALL TESTS PASSED 则说明代码没问题。
 git codereview mail  #提交你的代码

6. 进行 Code Review

提交完成后,你就会在 https://go-review.googlesource.com/ 上有一个新的 Change 的记录,然后系统会自动帮你分配 Maintainer 进行 Review,这个阶段如果 Reviewer 没有给你提出需要修改的话,你只需要等待你的代码被合并即可。

需要注意的是,Golang 对于代码的合并有要求,需要 2 个 +2 才能合并到代码库中。因此,你的代码可能会很快被某个 Maintainer Review,但很久才被另外一个 Maintainer Review (其实也没多久,只是相比于第一个)

当两个都合并完成后,左上角的标识就会变成 Merged ,此时就说明你的代码贡献成功啦!

一些注意的点

1. 关于中国区无法提交的问题

如果你使用自己的本地电脑进行提交,你会发现,你会收到一个报错

Access Denied (not available from your location)

这是因为 Golang 官方禁止了中国区的提交,将 golang 源码对于中国区进行了只读状态的设置。

这并不是 Golang 不欢迎中国开发者贡献,只是对于 GFW 封禁 golang.org 的一种抵抗。

Maintainer 的解释

你只需要将贡献环境从你的本地电脑,换成任何一个海外的服务器,就可以正常提交。

来源:access to Gerrit denied because of location · Issue #20065 · golang/go (github.com)

2. Commit Message 格式

golang 的 commet message 有自己的格式,你在提交的时候可以以这个格式进行提交,这样后续就不需要 Reviewer 帮你改了。

[模块] 修改内容
详细信息
Github link

给大家举个例子,大家就知道是什么样的了。首先,下面这个是我自己的 commit message

typo: fix reponse to response in src/net/http/header.go
Change-Id: I238bc90c4f273c352ef924989a44c6e927839128

Reviewer 帮我调整后的格式

net/http: fix typo in header.go
Change-Id: Ia6df881badf9a704c7f56967404d37e230b88a09
Reviewed-on: https://go-review.googlesource.com/c/go/+/343969
Reviewed-by: Damien Neil <dneil@google.com>
Reviewed-by: Ian Lance Taylor <iant@golang.org>
Trust: Damien Neil <dneil@google.com>

更加简单明了。

Change ID、Reviewed on、Reviewed by 是系统自动添加的,你不需要关注。

来源:Contribution Guide – The Go Programming Language (google.cn)

相关链接

如何修复 Beego 运行测试时的报错 undefined: web.Trace

如何修复 Beego 运行测试时的报错 undefined: web.Trace

Beego 近来已经更新了 2.X,因此,你现在可以使用更新版本的 Beego 来完成你的快速开发。

不过,beego 2.x 在使用 bee 工具初始化一个新的项目时,默认的项目中有一个 bug,当你在项目根目录下执行 go test ./... 时,会提示 tests 目录中有一个错误。而你并没有修改任何的源文件。

如果单独执行 tests 目录,就会报错如下内容

tests/default_test.go:28:2: undefined: web.Trace

你会发现这段代码指向了 default_test.go 中的这行代码。

beego.Trace("testing", "TestBeego", "Code[%d]\n%s", w.Code, w.Body.String())

这段代码中的 Trace 找不到导致无法正常使用。

我通过一些搜索,找到了答案。在 GitHub 中,beego 的 repo 下有一个 issue 和一个 pull request 与这个 bug 相关。结论是这个方法已经被移动到 log 文件夹,因此需要调整一下输出的来源。

在代码中引入 "github.com/beego/beego/v2/core/logs" 并将 beego.Trace 替换为 logs.Trace ,即可解决这个问题。

修改后的代码如下:

package test
import (
    "net/http"
    "net/http/httptest"
    "testing"
    "runtime"
    "path/filepath"
    _ "backend-server/routers"
    "github.com/beego/beego/v2/core/logs"
    beego "github.com/beego/beego/v2/server/web"
    . "github.com/smartystreets/goconvey/convey"
)
func init() {
    _, file, _, _ := runtime.Caller(0)
    apppath, _ := filepath.Abs(filepath.Dir(filepath.Join(file, ".." + string(filepath.Separator))))
    beego.TestBeegoInit(apppath)
}
// TestBeego is a sample to run an endpoint test
func TestBeego(t *testing.T) {
    r, _ := http.NewRequest("GET", "/", nil)
    w := httptest.NewRecorder()
    beego.BeeApp.Handlers.ServeHTTP(w, r)
    logs.Trace("testing", "TestBeego", "Code[%d]\n%s", w.Code, w.Body.String())
    Convey("Subject: Test Station Endpoint\n", t, func() {
            Convey("Status Code Should Be 200", func() {
                    So(w.Code, ShouldEqual, 200)
            })
            Convey("The Result Should Not Be Empty", func() {
                    So(w.Body.Len(), ShouldBeGreaterThan, 0)
            })
    })
}

参考阅读

Golang 中如何为 XML 加入 CDATA 支持

需求

最近在参与 WavPub 的开发,在开发的过程中,需要调整 XML 的结构,因此,需要为一些字段加入 CDATA 的支持。

问题

在阅读了 eduncan911/podcast 中的代码后发现,这个包在生成 XML 的时候,使用的是 Golang 核心库中的 encoding/xml 包,而这个包在使用的时候有一个问题,你可以给其字段加入 ,cdata 来完成加入 cdata 的标签,但问题在于,他的实现是,在你的字段外部加标签,而不是内部加标签。举个例子来说,就是,如果你定义了字段为 xml:”category,cdata” 你得到的会是

<![CDATA[ somecode ]]>

而非我们想要的

<category> <![CDATA[ xxx ]]> </category>

实现

想要解决这个问题,就需要你在你的字段中实现一层包裹,在其自动生成的 CDATA 外层加入一层 XML ,这样就可以实现我们想要的效果,比如说我上面的效果可以通过定义一个新的 Description 的 Struct 来实现

package podcast
import "encoding/xml"
// Description represents text inputs.
type Description struct {
	XMLName xml.Name `xml:"description"`
	Text    string   `xml:",cdata"`
}

然后,再在需要的地方,加入相应的引用就好

type Podcast struct {
	XMLName        xml.Name `xml:"channel"`
	...
	Description    *Description
        ...
}

Reference

https://pkg.go.dev/encoding/xml?tab=doc

https://play.golang.org/p/xRn6fe0ilj

Golang 返回随机值

需求

在某些场景下,需要根据给定值,返回一批特定的结果,在这种情况下,需要返回切片中的某一个特定的值。

实现

package main
import (
	"fmt"
	"math/rand"
	"time"
)
func main() {
	userAgentSlice := []string{
		"Podcasts/1430.46+CFNetwork/1125.2+Darwin/19.4.0",
		"Spotify/1.0",
		"PocketCasts/1.0+(Pocket+Casts+Feed+Parser;++http://pocketcasts.com/)",
		"iTMS",
		"AirPodcasts/1440.4+CFNetwork/1126+Darwin/19.5.0",
		"Tentacles,+Like+iTunes",
	}
	rand.Seed(time.Now().Unix())
	fmt.Println(userAgentSlice[rand.Intn(len(userAgentSlice))])
}