Jekyll2023-07-25T14:50:06+08:00https://bastengao.com/feed.xmlBastenGao’s Blogbastengao 的博客,主要专注于技术开发、Web、Ruby、Rails、Java 等。bastengao在 Go 中使用 exec 包 Cmd.StdoutPipe() 注意事项2023-07-05T00:00:00+08:002023-07-05T00:00:00+08:00https://bastengao.com/blog/2023/07/go-exec-pipe<p>在 Go 中执行一个外部命令,然后读取第一行输出代码一般是这样的。</p>
<pre><code class="language-go">cmd := exec.Command("prog")
stdout, err := cmd.StdoutPipe()
if err != nil {
log.Fatal(err)
}
err = cmd.Start()
if err != nil {
log.Fatal(err)
}
scanner := bufio.NewScanner(stdout) // 1 读取完第一行
if scanner.Scan() {
fmt.Println(scanner.Text())
}
err = cmd.Wait() // 2 等待命令结束
log.Printf("Command finished with error: %v", err)
</code></pre>
<p>上面代码感觉没什么问题,执行后会发现,外部命令都已经执行完毕了,代码会卡在 Wait 这里。
重新看文档后才明白问题所在,Wait 函数不仅会等待外部命令退出,还会等待之前打开的输出管道复制完成。</p>
<blockquote>
<p>func (c *Cmd) Wait() error</p>
<p>Wait waits for the command to exit and <strong>waits</strong> for any copying to stdin or copying from stdout or stderr to complete.</p>
</blockquote>
<p>所以正确的用法是如果使用了 StdoutPipe 那么就要把它读完。</p>
<pre><code class="language-diff">scanner := bufio.NewScanner(stdout) // 1 读取完第一行
if scanner.Scan() {
fmt.Println(scanner.Text())
}
+io.Copy(io.Discard, stdout)
</code></pre>bastengao在 Go 中执行一个外部命令,然后读取第一行输出代码一般是这样的。使用 AWS Global Accelerator 加速 EKS ingress 访问2023-05-04T00:00:00+08:002023-05-04T00:00:00+08:00https://bastengao.com/blog/2023/05/aws-ga-eks-ingress<p>先说下效果,使用 Global Accelerator 后,访问在 AWS us-west-2 的 ALB 基本都在 200ms 内,本地网络是西安电信。</p>
<p>场景:使用 https 方式通过 GA 访问 ingress 暴露的 API 接口。</p>
<p>下面说说我如何做的。首先添加一个 API ingress, 证书可以使用通配符或者限定域名都可以。
例如最终使用的 api.example.org 访问接口,那么就配置 <code>*.example.org</code> 或者 <code>api.example.org</code> 域名的证书。
<code>rules</code> 那里不配置 <code>host</code>。</p>
<pre><code class="language-yaml">apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
namespace: dev
name: api
annotations:
alb.ingress.kubernetes.io/scheme: internet-facing
alb.ingress.kubernetes.io/certificate-arn: arn:aws:acm:us-west-2:xxxx:certificate/xxxx
spec:
ingressClassName: alb
rules:
- http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: api
port:
number: 80
</code></pre>
<p>第二步,添加 Global Accelerator。加速器类型选标准,默认 ipv4。
侦听器端口选 443 (因为使用 https), 协议选 TCP。
端点组区域选择 us-west-2,端点选择上面 ingress 对应的 ALB 就好了。</p>
<p>第三步,配置 <code>api.example.org</code> CNAME 到 GA 分配的域名。稍等一段时间就可以测试访问了。</p>bastengao先说下效果,使用 Global Accelerator 后,访问在 AWS us-west-2 的 ALB 基本都在 200ms 内,本地网络是西安电信。iris 使用小技巧2022-04-30T00:00:00+08:002022-04-30T00:00:00+08:00https://bastengao.com/blog/2022/04/iris-mvc-practice<h2 id="内嵌模板">内嵌模板</h2>
<p>Go build 出的二进制文件是不包括静态资源的,例如 html。
所以 iris 结合使用 go-bindata 把静态资源打包到二进制文件,并注册给 view engine 使用。大概代码如下:</p>
<pre><code class="language-go">tmpl := iris.HTML("./views", ".html")
tmpl.Binary(Asset, AssetNames)
</code></pre>
<p>自从 Go 1.6 有了官方的内嵌资源功能后 <a href="https://pkg.go.dev/embed">embed</a>,其他的一些做静态资源打包的就被大一统了。
结合 iris(v12.2.0-beta1) 用,就非常简单了。示例如下:</p>
<pre><code class="language-go">//go:embed views/*
var fs embed.FS
// iris.HTML 支持目录或者 http.FileSystem
tmpl = iris.HTML(http.FS(fs), ".html")
</code></pre>
<h2 id="mvc-getbyxxx">MVC GetByXxx</h2>
<p>官方 <a href="https://github.com/kataras/iris/wiki/MVC">MVC wiki</a> 里列举了 GetBy 和 GetXxxBy, 但是少说明了一个 GetByXxx, 效果如下。</p>
<pre><code class="language-go">mvc.New(app.Party("/users")).Handle(new(UsersController))
// Method: GET
// Resource: /users/{userID:int64}/addresses
func (* UsersController) GetByAddresses(userID int64) string {
return "addresses"
}
</code></pre>
<h2 id="mvc-路由命名">MVC 路由命名</h2>
<p>iris 支持<a href="https://github.com/kataras/iris/wiki/Routing-reverse-lookups">路由命名</a>,并可以通过路由名称来获取路径。这种就可以方便的实现跳转或者页面引用路径,而不必到处写死路径。
常规注册路由可以很方便的拿到 <code>router.Route</code> 从何进行命名,像下面这样.</p>
<pre><code class="language-go">app.Get("/", h).Name = "home"
</code></pre>
<p>但是如果使用 MVC, 路由注册是动态的,没法直接拿到 <code>Route</code> 对象,需要通过 <code>AfterActivation</code> 回调来获取路由,从而进行命名。如下面这样:</p>
<pre><code class="language-go">type MyController struct {}
func (c *MyController) AfterActivation(b mvc.AfterActivation) {
c.GetRoutes("Get")[0].Name = "home"
}
func (c *MyController) Get() string {
return "home"
}
</code></pre>
<h2 id="mvc-重定向">MVC 重定向</h2>
<p>通过设置 mvc.Response 里的 Path 就可以实现跳转了。</p>
<pre><code class="language-go">func (c *MyController) Get() mvc.Result {
return mvc.Response{
Path: "/path/redirect/to",
}
}
</code></pre>
<p>以上是基于 iris v12.2.0-beta1 版本的技巧,供大家参考。</p>bastengao内嵌模板Go slice 不同使用方式性能测试2021-11-10T00:00:00+08:002021-11-10T00:00:00+08:00https://bastengao.com/blog/2021/11/go-slice-performance<p>写 Go 必然要经常用到 slice, 大家一般初始化 slice 无非是下面几种方式.</p>
<ol>
<li><code>var s []int</code></li>
<li><code>s := []int{}</code></li>
<li><code>s := make([]int, 0)</code> (这种和上面效果一样,但不推荐,推荐理由)</li>
<li><code>s := make([]int, n)</code></li>
</ol>
<p>如果是构造一个空的 slice 推荐使用第一种写法,
推荐理由参考 <a href="https://github.com/uber-go/guide/blob/master/style.md#local-variable-declarations">Uber Go Style Guide</a>
和 <a href="https://github.com/golang/go/wiki/CodeReviewComments#declaring-empty-slices">Go Code Review Comments</a>. 初始化完后,让我们来使用 slice, 实现 0-9 十个数都加到 slice.</p>
<pre><code class="language-go">var s []int
for i := 0; i < 10; i++ {
s = append(s, i)
}
</code></pre>
<pre><code class="language-go">s := make([]int, 10)
for i := 0; i < 10; i++ {
s[i] = i
}
</code></pre>
<p>盲猜应该是第二种写法效率高并且省内存,因为 slice 空间初始化的时候就一次分配好了。不过还是要用实际测试数据说话,上 <a href="https://gist.github.com/bastengao/397a3f3e8c0d30217b32d87afebb36ff">benchmark</a>.</p>
<pre><code class="language-go">package main
import "testing"
func BenchmarkInitLen(b *testing.B) {
n := b.N
for i := 0; i < n; i++ {
a := make([]int64, n)
for j := 0; j < n; j++ {
a[j] = int64(j)
}
}
}
func BenchmarkInitCap(b *testing.B) {
n := b.N
for i := 0; i < n; i++ {
a := make([]int64, 0, n)
for j := 0; j < n; j++ {
a = append(a, int64(j))
}
}
}
func BenchmarkZero(b *testing.B) {
n := b.N
for i := 0; i < n; i++ {
var a []int64
for j := 0; j < n; j++ {
a = append(a, int64(j))
}
}
}
func BenchmarkInitZeroLen(b *testing.B) {
n := b.N
for i := 0; i < n; i++ {
a := make([]int64, 0)
for j := 0; j < n; j++ {
a = append(a, int64(j))
}
}
}
</code></pre>
<p>测试设备信息:</p>
<pre><code>MacBook Pro (13-inch, M1, 2020)
OS: macOS Monterey
CPU: Apple M1
Mem: 16 GB
Go: 1.7.3
</code></pre>
<p>测试结果:</p>
<pre><code>BenchmarkInitLen
BenchmarkInitLen-8 131744 68970 ns/op 1056771 B/op 1 allocs/op
BenchmarkInitCap
BenchmarkInitCap-8 189924 96863 ns/op 1523716 B/op 1 allocs/op
BenchmarkZero
BenchmarkZero-8 49658 120226 ns/op 2303233 B/op 27 allocs/op
BenchmarkInitZeroLen
BenchmarkInitZeroLen-8 49298 120323 ns/op 2303233 B/op 27 allocs/op
</code></pre>
<p>可以看到 BenchmarkInitLen 测试结果是最好的,执行速度和内存非配都是最优的,执行耗时是最差的 57.3%,内存使用是最差的 45.9%。
所以当我们知道 slice 明确长度的,推荐使用 <code>s := make([]int, len)</code> 来初始化,然后通过索引来赋值。</p>
<p>上篇博客还在三月,转眼就要到年末了,记得上次写博客还是上次,真是时光荏苒,写一篇比较水的博客充个数,要不然 2021 年就只有一篇博客了。</p>bastengao写 Go 必然要经常用到 slice, 大家一般初始化 slice 无非是下面几种方式.iris view 作为更好的 Go HTML 模板使用2021-03-26T00:00:00+08:002021-03-26T00:00:00+08:00https://bastengao.com/blog/2021/03/iris-html-template<p>目前的一些 web framework 做 API 很方便,但是做 template 渲染都是简单粗暴,直接输出模板,提供自定义模板函数外,其他额外功能很少。
但真正开发 HTML 还是很需要很多东西,例如 layout 来组织 HTML 整体结构减少冗余,还需要 partial render 来解决 HTML 片段复用。</p>
<p>例如下面典型 HTML,基本每个页面都需要这些东西,如果每个页面都复制完整 HTML 简直是灾难。</p>
<pre><code class="language-html"><!DOCTYPE html>
<html>
<head>
<link>
<script>
...
</head>
<body>
<header>
...
</header>
content here
<footer>
...
</footer>
</body>
</html>
</code></pre>
<p><img src="https://bastengao.com/images/2021-03-26/layout.svg" alt="layout" /></p>
<p>熟悉 Rails 的同学都知道 Rails 有一套<a href="https://guides.rubyonrails.org/layouts_and_rendering.html">完整机制</a>来解决这些问题。<a href="https://github.com/kataras/iris">iris</a> 能够比较好的解决这些问题,文档参考<a href="https://github.com/kataras/iris/wiki/View">Wiki</a> 和 <a href="https://pkg.go.dev/github.com/kataras/iris/view">GoDoc</a>,官方提供了<a href="https://github.com/kataras/iris/tree/master/_examples/view/layout/html">layout 例子</a>。例子主要如下:</p>
<p>views/layouts/maim.html</p>
<pre><code><!DOCTYPE html>
<head>
<title>{{.Title}}</title>
</head>
<body>
{{ yield }}
<footer>
{{ render "partials/footer.html" }}
</footer>
</body>
</html>
</code></pre>
<p>views/index.html</p>
<pre><code class="language-html"><h1>Index Body</h1>
<h3>Message: {{.Message}}</h3>
</code></pre>
<p>main.go</p>
<pre><code class="language-go">package main
import "github.com/kataras/iris/v12"
func main() {
app := iris.New()
engine := iris.HTML("./views", ".html")
// 也可以通过这里设置默认 layout 就不用每次手动指定
// engine.Layout("layouts/main.html")
app.RegisterView(engine)
app.Get("/", index)
app.Listen(":8080")
}
func index(ctx iris.Context) {
data := iris.Map{
"Title": "Page Title",
"FooterText": "Footer contents",
"Message": "Main contents",
}
ctx.ViewLayout("layouts/main.html")
ctx.View("index.html", data)
}
</code></pre>
<p><code>yield</code> 在 layout 模板用来渲染页面动态内容,例如 <code>views/index.html</code> 的内容。<code>render</code> 可以渲染其他模板,例如 <code>partials/footer.html</code>。
还有 <code>urlpath</code> 模板函数,方便做反向路由用,例如 <code><a href="{{ urlpath "home" }}">Home</a></code>, 这样改路由就不用大费周章的全局搜索了。</p>
<p>不过 <a href="https://pkg.go.dev/github.com/kataras/iris/view">iris view</a> 也可以单独使用,view module 抽象了 <code>Engine</code> 接口, 接口定义如下。</p>
<pre><code class="language-go">type Engine interface {
// Load should load the templates from a physical system directory or by an embedded one (assets/go-bindata).
Load() error
// ExecuteWriter should execute a template by its filename with an optional layout and bindingData.
ExecuteWriter(w io.Writer, filename string, layout string, bindingData interface{}) error
// Ext should return the final file extension which this view engine is responsible to render.
Ext() string
}
</code></pre>
<p><a href="https://github.com/bastengao/iris-view-example">iris-view-example</a> 是 <code>net/http</code> + iris view 的例子,供大家参考。</p>bastengao目前的一些 web framework 做 API 很方便,但是做 template 渲染都是简单粗暴,直接输出模板,提供自定义模板函数外,其他额外功能很少。 但真正开发 HTML 还是很需要很多东西,例如 layout 来组织 HTML 整体结构减少冗余,还需要 partial render 来解决 HTML 片段复用。machinery 同步获取任务结果的小坑2020-08-03T00:00:00+08:002020-08-03T00:00:00+08:00https://bastengao.com/blog/2020/08/machinery-result<p>下面是 <a href="https://github.com/RichardKnop/machinery">machinery</a> 获取任务执行结果的<a href="https://github.com/RichardKnop/machinery#keeping-results">示例代码</a></p>
<pre><code class="language-go">results, err := asyncResult.Get(time.Duration(time.Millisecond * 5))
if err != nil {
// getting result of a task failed
// do something with the error
}
for _, result := range results {
fmt.Println(result.Interface())
}
</code></pre>
<p>上面 <code>Get(sleepDuration)</code> 会阻塞调用一直尝试到获取任务结果为止。当任务量巨大的时候,这会导致大量的 redis 查询操作,同时消耗大量的本地 socket 资源,更严重的情况会把本地 socket 资源耗尽。</p>
<p>还好除了同步获取任务结果的方式,还有异步的方式。<a href="https://github.com/RichardKnop/machinery#signatures">Signature</a> 可以设置 <code>OnSuccess: []*Signature</code> 和 <code>OnError: []*Signature</code> 字段,当任务完成后会执行设置的回调任务。这样就完美解决了大量轮询的情况。</p>bastengao下面是 machinery 获取任务执行结果的示例代码关于 Go 测试的一些实践2019-12-28T00:00:00+08:002019-12-28T00:00:00+08:00https://bastengao.com/blog/2019/12/go-test-practices<p>Go <code>testing</code> 包提供了比较丰富的测试功能,包括普通测试、基准测试、main 测试。
很容易编写对现有代码的测试,这里分享一些平日里一些实践。</p>
<h2 id="testify">testify</h2>
<p><a href="https://github.com/stretchr/testify">testify</a> 提供了两个比较有用的包。一个是 assert 包,它提供了丰富断言方法,让你从大量的啰嗦的 if 中解脱出来。</p>
<pre><code class="language-go">assert.NoError(t, err)
assert.Equal(t, 2019, year)
</code></pre>
<p>还有一个是 suite 包,它提供了测试生命周期管理,通俗讲就是它可以让一组测试复用准备前工作,测试后的清理工作。
<code>suite.Suite</code> 提供了一些钩子,可以按需使用。
钩子有这些 <code>SetupSuite</code>, <code>TearDownSuite</code>, <code>SetupTest</code>, <code>TearDownTest</code>, <code>BeforeTest(suiteName, methodName)</code>, <code>AfterTest(suiteName, methodName)</code></p>
<pre><code class="language-go">type ExampleSuite struct {
suite.Suite
}
// 在执行所有测试前执行,只执行一次
func (s *ExampleSuite) SetupSuite() {
}
// 每次测试前都会被执行
func (s *ExampleSuite) SetupTest() {
}
// your test
func (s *ExampleSuite) TestExample() {
year := 2019
s.Assert().Equal(2019, year)
}
// 每次测试后会被执行
func (s *ExampleSuite) TearDownTest() {
}
// 在执行所有测试后执行,只执行一次
func (s *ExampleSuite) TearDownSuite() {
}
// 通过 testing 驱动 suite
func TestExampleSuite(t *testing.T) {
suite.Run(t, new(ExampleSuite))
}
</code></pre>
<p>做一些与数据库有交互的测试就非常适合使用 suite,测试前准备测试数据,测试后清理数据,给下一个测试准备好干净的环境。</p>
<p>说道插入数据和清理数据,接下来介绍另外两个工具。</p>
<h2 id="dbcleaner">dbcleaner</h2>
<p><a href="https://github.com/khaiql/dbcleaner">dbcleaer</a> 提供数据库清理功能,而且他还能保证并行的运行测试时避免同时操作数据库导致测试失败。他底层是使用文件锁来做到多个测试之间同步执行的。</p>
<pre><code class="language-go">// 默认文件锁目录是在 /tmp, 如果有多用户同时执行的情况,可以把锁目录改到项目里,tmp 加到 .gitignore 里不要提交就好了
option := dbcleaner.SetLockFileDir("./tmp/")
var cleaner = dbcleaner.New(option)
// 测试前获取 users 表的锁
cleaner.Acquire("users")
// 测试后清理数据,并释放锁
defer cleaner.Clean("users")
// 执行测试
...
</code></pre>
<p>因为他底层使用的是文件锁,只要锁目录一致 <code>dbcleaner.New</code> 可以多次执行,不会影响结果。</p>
<h2 id="testfixtures">testfixtures</h2>
<p><a href="https://github.com/go-testfixtures/testfixtures">testfixtures</a> 提供了从 yml 导入数据到数据库的功能, 可以在测试前导入依赖的数据。</p>
<p>例如数据库有一个 <code>users</code> 表,加一个 <code>fixtures/users.yml</code> 文件,注意文件名要和表名一致。</p>
<pre><code class="language-yaml"># fixtures/users.yml
- id: 1
username: bastengao
created_at: 2019-12-28 10:09:08
updated_at: 2019-12-28 10:09:08
- id: 2
username: gopher
created_at: 2019-12-28 10:09:08
updated_at: 2019-12-28 10:09:08
</code></pre>
<pre><code class="language-go">import (
"github.com/go-testfixtures/testfixtures/v3"
)
db, _ = sql.Open("postgres", "xxxx")
fixtures, _ := testfixtures.New(
testfixtures.Database(db),
testfixtures.Dialect("postgres"),
testfixtures.Files("fixtures/users.yml"),
)
// 测试前导入数据
fixtures.Load()
// 执行测试
...
</code></pre>
<p>上面例子为了展示核心功能,没做错误处理,实际项目中一定要错误处理。
我个人比较喜欢按需导入数据, 也可以把整个数据导入, 将 <code>testfixtures.Files</code> 换成 <code>testfixtures.Directory("fixtures")</code> 就好了。</p>
<p>还有 <code>fixtures.Load()</code> 也会做数据库清理工作,但它不支持并发执行测试,所以可以配合 dbcleaner 来同步测试。</p>
<h2 id="mock">mock</h2>
<p><a href="https://github.com/golang/mock">mock</a> 顾名思义它提供了 mock 功能。当我们要测试的东西有其他依赖,例如第三方 API、还未完成的实现、或者依赖有副作用, 就可以使用 mock 对其进行替换,只测试我们关心的业务。</p>
<pre><code class="language-go">type Mailer interface {
Send(recipient, subject, content string) error
}
func registerUser(email string, mailer Mailer) error {
err := createUser(email)
if err != nil {
return err
}
return mailer.Send(email, "Registration", "welcome")
}
</code></pre>
<p>例如要测试 <code>registerUser</code> 方法,我们只关心有没有创建用户,不关心邮件发送,或者邮件发送还没有实现或者有副作用, 这时候就可以使用 mock 对 Mailer 进行替换。</p>
<p>先通过 mockgen 命令成成 Mailer 接口的 mock 文件</p>
<pre><code>mockgen -destination mock/mailer.go -package mock your/package/name Mailer
</code></pre>
<p>然后在测试中使用 mock</p>
<pre><code class="language-go">import (
"github.com/golang/mock/gomock"
// mock dir
"mock"
)
func TestRegisterUser(t testing.T) {
ctrl := gomock.NewController(s.T())
mailer := mock.NewMockMailer(ctrl)
mailer.
EXPECT().
Send(
gomock.Eq(email),
gomock.Any,
gomock.Any,
).
Return(nil)
err := registerUser(email, mailer)
assert.NoError(t, err)
...
}
</code></pre>
<h2 id="最后">最后</h2>
<p>完整的例子在这里 <a href="https://github.com/bastengao/go-test-example">go-testexample</a>, 供大家参考。</p>
<p>PS: dbcleaner 和 testfixtures 都是从 Ruby 和 Rails 借鉴过来的,Rails 提供给了一个相当完整的测试体验,有很多地方值得学习。
db migration 就是 Rails 一个非常好用强大的功能,目前还没有找到和它相媲美的工具。</p>bastengaoGo testing 包提供了比较丰富的测试功能,包括普通测试、基准测试、main 测试。 很容易编写对现有代码的测试,这里分享一些平日里一些实践。Go SMTP SSL 发邮件的一个小坑2019-11-23T00:00:00+08:002019-11-23T00:00:00+08:00https://bastengao.com/blog/2019/11/go-smtp-ssl<p>Go 标准库已经带了 net/stmp 的库,可以通过 <code>smtp.SendMail(addr, auth, from, to, msg)</code> 直接发送邮件。看到 SendMail https://godoc.org/net/smtp#SendMail 方法的描述说如果可以使用 TLS 认证。</p>
<pre><code class="language-plaintext">SendMail connects to the server at addr, switches to TLS if possible, authenticates with the optional mechanism a if possible, and then sends an email from address from, to addresses to, with message msg. The addr must include a port, as in "mail.example.com:smtp".
</code></pre>
<p>测试了非 SSL 连接发邮件没有问题,SSL 方式就会等待60后莫名其妙的返回 <code>EOF</code> 错误。
检查了多变配置后还是有问题,搜索到这个帖子 <a href="https://stackoverflow.com/questions/57063411/go-smtp-unable-to-send-email-through-gmail-getting-eof">go-smtp, unable to send email through gmail, getting EOF</a> ,里面解释了原因和解决办法。
使用 <a href="https://gist.github.com/chrisgillis/10888032">Golang SSL SMTP Example</a> 这个 gist 的代码可以解决这个问题,碰巧我在用 github.com/jordan-wright/email 这个库,这个库可以提供一些发送邮件便利方法,同时 <code>Email#SendWithTLS</code> 方法完成了上面 gist 同样的工作,直接用这个方法就可以省好多代码。
下面是个完整例子</p>
<pre><code class="language-go">import (
"crypto/tls"
"net/smtp"
"github.com/jordan-wright/email"
)
func sendExample() error {
ssl := true
host := "example.com"
auth := smtp.PlainAuth("", username, password, host)
e := email.NewEmail()
e.From = "from@example.com"
e.To = []string{"to@example.com"}
e.Subject = "subject"
e.Text = []byte("test")
if ssl {
return e.SendWithTLS("example.com:465", auth, &tls.Config{ServerName: host})
} else {
return e.Send("example.com:25", auth)
}
}
</code></pre>
<p>我的环境 go 1.13.4 。</p>bastengaoGo 标准库已经带了 net/stmp 的库,可以通过 smtp.SendMail(addr, auth, from, to, msg) 直接发送邮件。看到 SendMail https://godoc.org/net/smtp#SendMail 方法的描述说如果可以使用 TLS 认证。GraphQL ruby 实现入门和实践2019-08-27T00:00:00+08:002019-08-27T00:00:00+08:00https://bastengao.com/blog/2019/08/graphql-introduction-and-practices<p>从 2018 年初项目从 REST 切换成 GraphQL 后就一直从事 GraphQL 的开发,从 1.7 开始, 升级到 1.8 然后到 1.9 可以说是对 GraphQL ruby 有一个比较深入的使用。正好在 <a href="http://www.rubyconfchina.org/">RubyConf China 2019</a> 组织者之一的 <a href="https://ruby-china.org/hooopo">hooopo</a> 怂恿下,让我对 GraphQL 贡献一个话题,我也就怀着激动和忐忑的心接受了。</p>
<p>认证 header 我们是使用 JWT, 用起来非常方便,但是如果要做登出业务需要后端对 JWT 进行存储来进行二次验证。授权最早是使用 graphql-guard, 后面 1.8+ 版本加了授权支持也没有使用,不知道实际使用的效果如何。</p>
<p>如果可以项目一开始还是建议使用 Relay 实践,Object Idenfication 和 node 接口还是比较省事。基于游标的分页如果满足项目需求用起来也不错,如果是基于页码的分页我们进行了一些自己的封装,现在独立一个 gem 在这里 <a href="https://github.com/bastengao/graphql-paging">graphql-paging</a>。</p>
<p>Subscription 推送也是一个比较使用的特性,可以基于 ActionCable 实现,不过客户端集成那里 ActionCable 的 js adapter 有点小问题,不知道现在还会不会有坑。</p>
<p>总而言之如果是稍微大一点的 API 项目我觉得 GraphQL 还是有明显的优势,同时如果项目团队人员比较多的情况下也可以较少关于 API 文档的沟通成本。</p>
<ul>
<li><a href="/downloads/graphql-introduction-and-practices.pdf">PPT下载</a></li>
<li>视频在下面</li>
</ul>
<iframe width="560" height="315" src="https://www.youtube.com/embed/UquN88bxgqA" frameborder="0" allow="accelerometer; autoplay; encrypted-media; gyroscope; picture-in-picture" allowfullscreen=""></iframe>bastengao从 2018 年初项目从 REST 切换成 GraphQL 后就一直从事 GraphQL 的开发,从 1.7 开始, 升级到 1.8 然后到 1.9 可以说是对 GraphQL ruby 有一个比较深入的使用。正好在 RubyConf China 2019 组织者之一的 hooopo 怂恿下,让我对 GraphQL 贡献一个话题,我也就怀着激动和忐忑的心接受了。在 Go 语言中调用 C++ 代码2017-12-26T00:00:00+08:002017-12-26T00:00:00+08:00https://bastengao.com/blog/2017/12/go-cgo-cpp<p>上篇博客讲到<a href="http://bastengao.com/blog/2017/12/go-cgo-c.html">Go 如何调用 C</a>, 这篇主要讲 Go 如何调用 C++ 。C++ 的代码目前没法内联在 Go 代码里,只能通过外部库方式引用,同时 cgo 也没办法直接调用 C++ 代码, 类也没法 new, 除了 extern “C” 方式声明的函数。所以 Go 要想调用 C++ 代码,可以通过 C 代码调 C++, 然后通过 Go 调 C 代码来实现。下面讲一个具体的例子。</p>
<pre><code>example/
main.go
foo/
foo.h
foo.cpp
bridge.h
bridge.c
</code></pre>
<p><code>foo.h</code></p>
<pre><code class="language-c++">class Foo {
public:
Foo();
~Foo();
void bar();
};
</code></pre>
<p><code>foo.cpp</code></p>
<pre><code class="language-c++">#include <stdio.h>
#include "foo.h"
Foo::Foo() {
}
Foo::~Foo() {
}
void Foo::bar() {
printf("hello bar\n");
}
</code></pre>
<p><code>bridge.h</code></p>
<pre><code class="language-c">#ifdef __cplusplus
extern "C" {
#endif
void bar();
#ifdef __cplusplus
}
#endif
</code></pre>
<p><code>bridge.c</code></p>
<pre><code class="language-c">#include "foo.h"
#include "bridge.h"
void bar() {
Foo* foo = new Foo();
foo->bar();
}
</code></pre>
<p>以上 foo.h 和 foo.cpp 是 C++ 代码,我们使用 C 语言写个 bridge , 封装下 C++ 调用。</p>
<p>编译静态链接库</p>
<pre><code>cd foo/
g++ -c foo.cpp
g++ -c bridge.c
ar -crs libfoo.a foo.o bridge.o
</code></pre>
<p><code>main.go</code></p>
<pre><code class="language-go">package main
// #cgo CFLAGS: -I${SRCDIR}/foo
// #cgo LDFLAGS: -lstdc++ -L${SRCDIR}/foo -lfoo
// #include "bridge.h"
import "C"
func main() {
C.bar()
}
</code></pre>
<p>配置 <code>#cgo</code> 指令和调用 C 外部库一样,唯一需要注意的是 LDFLAGS 参数要加 <code>-lstdc++</code>。另外一个需要注意的地方是 C 语言调用 C++ 的时候头文件需要配置 <code>extern "C" { ... }</code> 大意是按照 C 语言的规范来编译和连接,我的主要工作语言不是 C/C++ 详细参考<a href="http://www.cnblogs.com/skynet/archive/2010/07/10/1774964.html">这篇博客</a>。最后使用 go build 编译 Go 代码。</p>
<pre><code>go build -o out
./out
</code></pre>bastengao上篇博客讲到Go 如何调用 C, 这篇主要讲 Go 如何调用 C++ 。C++ 的代码目前没法内联在 Go 代码里,只能通过外部库方式引用,同时 cgo 也没办法直接调用 C++ 代码, 类也没法 new, 除了 extern “C” 方式声明的函数。所以 Go 要想调用 C++ 代码,可以通过 C 代码调 C++, 然后通过 Go 调 C 代码来实现。下面讲一个具体的例子。