前述

我们在写一个web模块或者api接口的时候,我们总是能够想到利用mvc模式,将model层,controller的action层分离, 使得我们的代码总是干净,可维护性强,注入新的服务简单.这一篇博客,我将探索在go中如何利用golang语言的特性,写一个干净,整洁,注入型强的代码.

一个简单的代码需求

  1. 起一个web server
  2. 实现用户注册接口
  3. 单元测试完备
  4. 后期新的需求,可以快速在现有代码的基础上完成

尽量编写testable的代码

我们总是希望,我们的testing中枚举用例不受过多服务的牵连,也不受过多业务逻辑的牵连
例如,用户有existscreate方法,那我们最好的方式则是创建一组这样的user相关的接口,大致代码如下:

type Repository interface {
    Exists(email string) bool
    Create(*Form) (*User, error)
}
func (m *Memstore) Exists(email string) bool {
    return true
}

func (m *Memstore) Create(*Form) (*User, error) {
    return &User{
        Id:       1,
        Email:    "test@qq.com",
        Password: "123",
    }, nil
}

还有我们的user

type User struct {
    Id int `json:"id"`
    Email string `json:"email"`
    Password string `json:"password"`
}

那么我们对服务的testing可以这样:

func TestMemstore_Exists(t *testing.T) {
    type fields struct {
        Users []User
    }
    type args struct {
        email string
    }
    tests := []struct {
        name   string
        fields fields
        args   args
        want   bool
    }{
        // TODO: Add test cases.
    }
    for _, tt := range tests {
        t.Run(tt.name, func(t *testing.T) {
            m := &Memstore{
                Users: tt.fields.Users,
            }
            if got := m.Exists(tt.args.email); got != tt.want {
                t.Errorf("Exists() = %v, want %v", got, tt.want)
            }
        })
    }
}

实现handler

我们绑定/register 接口到handle上面, 注意这里不是handleFunc, 我们需要做的是在handle上注入更多的toolings,例如: model模型,Validater组件等

// 他需要两个服务,一个create,一个exists
type RegisterHandler struct {
    Repository
}

// 实现注册服务
func (r *RegisterHandler) ServeHTTP(http.ResponseWriter, *http.Request) {

}

这里有很重要的两点, 我们自己实现了http包的handler interface,我们将可testing的model模型绑定到了registerhandler上

我们在这里还可以绑定更多的组件

type RegistrationHandler struct {
    Validator *validator.Validate
    Repository

实现handler接口的绑定

func NewServer(addr string, r Repository) error {
    mux := http.NewServeMux()
    h := &RegisterHandler{r}
    mux.Handle("register", h)

    server := http.Server{
        Addr:    addr,
        Handler: h,
    }
    err := server.ListenAndServe()
    if err != nil {
        return err
    }
    return nil
}

这里将h注册到handle上,并将Repository具体实现传递给handler.

总结

当新的需求来临时,代码耦合的可能性增加,因为没有明显的设计来保护。服务对象的方式我们传递给每一个handler,使得每一个对象自己的独立功能尽可能不耦合,这样后期的可维护性也就大大增加了.

需求场景

我需要对http请求的body体做flter处理,以及拿到request的body做验签,之后http的request body又会参与二次或者多次的使用,例如透明传递给backend做转发,流量拷贝等.

名词解释

backend: 这个backend是我需要将流量(http 请求)复制给后端server,然后拿到后端server的结果返回给前端,整个过程你可以理解为是gateway或者类似nginx的porxy
所以这里的backend指的是: 后端server或者后端server的一些特征属性组成的一个实例.

bug复现

前端同学反馈给我,说backend提供了一个post请求的接口,直接访问backend的接口是200的status,且没有respose body,但是在网关层访问返回500.

查找bug思路第一步:

第一步,赶紧找log,所以log内容如下:

net/http: HTTP/1.x transport connection broken: http: ContentLength=30 with Body length 0

从日志看来是body的内容指定了content-length,但是是空body对应一个非0值的content-length

查找bug思路第二步->误入歧途:

这里我的第一脑补是backend的结果是空导致的,是不是backend返回给我了一个空body的情况下content-length是非0?
所以我就开始直接构造请求到backend
结果是:backend 返回没有问题, 到此我开始了阅读roundtrip源代码的漫长之路,可是整个过程很复杂,涉及到异步,涉及到http 协议的底层.

查找bug思路的第三步->回归现象:

无奈,源码阅读之后并无头绪,所以重新回归现象,仔细分析

其实log中可以看出来,说明以及很明确了,一定是body内容和content-length头不一致导致的,所以这里反思之后觉得是不是请求body的问题?

一旦想到这一点,我便去构造不同的body来验证length长度,果然length长度随着请求的body不同而不同

查找bug思路第四步->初见端倪:

一定是body被某一层的代码读走了,而roundtrip依然要使用,所以开始了各个flter层的查看,最后找到罪魁祸首:

bodyString, _ := ctx.GetRawData()

果然body被拿走,而没有回写,导致roundtrip拿不到body了

解决bug:

找到原因后,解bug不足一分钟
思路:回写request body到ctx中

bodyString, _ := ctx.GetRawData()
ctx.Request.Body = ioutil.NopCloser(bytes.NewBuffer([]byte(bodyString)))

总结:

当你读io的时候。读取时,它会把它读取完。一旦你阅读它,内容就消失了。你不能再读一遍。
io.reader 接近与水龙头:你可以得到水,但一旦它出来了,它就出来了。

需求场景

我们有一批struct对应的方法,同时对应的有这个structNew方法,而传递的参数是在首次启动应用程序时通过配置或者命令参数来决定的,这个时候我们就需要根据这些参数来NewDB(param1), NewLogger(param2)来生成实例,而这个实例将在应用程序的生命周期内不再改变,它可能渗透到应用程序的每一个package

常规做法

这个时候我们经常的做法是:

db := NewDB(param1)
logger := NewLogger(param2)
cmd.root.Run(db, logger)

然后这个实例logger 将贯穿我们的每一个包中(因为我们无时无刻需要在代码中打log来记录重要的信息)

缺点

上面的做法需要将logger传递到每一个package中,显得罗嗦,一旦有一个地方没有传递,那么就无法使用logger实例.

图示

问题描述:

中英文切换诡异问题

由于个人长久使用搜狗输入法,熟悉了一些配置,同时也用账号同步了历史以来的词汇,所以对于我来说,搜狗输入法真香,但是在mac ox环境下, ^ + 空格 用的总是那么不舒心,偶尔的自动切换到系统默认的输入法,偶尔的切换不过来中文,使得中文->英文,英文->中文,这两个操作有些烦躁.

官方不能本土化的定制

例如:
搜狗有符号设置: 可以在知乎页面中使用「」代替引号
再例如:
在中文输入法下,使用英文标点,且可以设置App清单
再例如:
定制化的皮肤

无法禁用Mac ox自带的默认输入法

可能这也是遇到问题的最根本原因,所以这篇文章主要说明的问题就是:如何解决掉自带的输入法.

解决方法:

思路:

其实思路很简单,mac ox上没法禁用自带的输入法,而这个输入法是由一个plist文件配置,但是这个配置文件是系统配置文件,所以需要关闭SIP(系统完整性保护),然后再修改plist配置文件

步骤:

  1. 关闭SIP

    (1)重启OSX系统,然后按住Command+R
    
    (2)出现界面之后,选择Utilities menu中Terminal
    
    (3)在Terminal中输入csrutil disable 关闭SIP(csrutil enable打开SIP)
    
    (4)重启reboot OSX
  2. 下载plist edit:

    这个只要输入关键字,百度有很多渠道下载,不再赘述

  3. 修改com.apple.HIToolbox.plist

    (1)open ~/Library/Preferences/
    (2)找到`com.apple.HIToolbox.plist`文件,右键用plistEdit打开
  4. 修改input属性(如下图):

    删除掉非sogou相关的序列,搞定!

使用golang和cobra构建健壮的命令行工具

开始之前

如果你长期工作于linux的命令行下,你会发现稍微复杂的工具,命令行复杂的要死,这个时候如果你选择记住这下命令是不明智的,这个时候如果有个好的usage则是极好的事情,例如我可以-h或者--help来查看支持的命令,以及命令的层级结构.

所以作为开发者的我们写一些给别人用,或者哪怕给自己用的工具时,我们就需要构建一个好的usage,这是工具使用侧,我们还需要有不同命令分层分文件的描述功能的实现,来构建工具代码侧.

上面即是我们的需求,好在开源软件为我们提供了优秀的cli工具包 cobra,下面我们就该工具包为大家阐述它的思想以及使用心得.

安装cobra

假定golang的环境你已经搭建完毕.并具有go代码的基础.

go get github.com/spf13/cobra/cobra

Cobra附带了自己的命令行程序 cobra,尽管我们可能不需要它来作为初始构建项目的办法,但是我强烈推荐使用该工具作为基础代码和结构的构建.

通常情况下,cobra程序在 ~/go/bin/cobra

这里的~/go 是你的gopath目录

执行 cobra后如下:

一个cobra开发工具的不成名规则

所有的Cobra项目遵循相同的开发周期。首先使用cobra工具初始化项目,然后创建命令和子命令,最后对生成的Go源文件进行所需的更改,以支持所需的功能。

拉出来实战

在本节中,您将了解如何使用三个名为insert、delete和list的命令来开发简单命令行实用程序的框架。

初始的结构

为了创建我们的第一个命令行实用工具,它将被称为three,我们需要执行以下命令:

cobra init three --pkg-name cobrathree
cd three
cobra add insert
cobra add delete
cobra add list

然后我们使用tree命令查看下目录结构:

➜  three tree -L 2
.
├── LICENSE
├── cmd
│   ├── delete.go
│   ├── insert.go
│   ├── list.go
│   └── root.go
└── main.go

1 directory, 6 files
➜  three

小插曲

本来到这里我们就可以 go build main.go 来测试程序了,但是我的代码环境是go1.13,所以需要在当前目录下生成go.mod来支撑 go mod的方式,而且本身我的代码也在gopath之外(为了方便测试),关于这一块的支持你可以搜索gomod来学习.所以我需要执行:

#这里的cobrathree需要和上面的--pkg-name一致
go mod init cobrathree

构建+测试

go run main.go insert

insert called

go run main.go delete

delete called

go run main.go list

list called

go run main.go doesNotExist

Error: unknown command "doesNotExist" for "three"
Run 'three --help' for usage.
unknown command "doesNotExist" for "three"
exit status 1

看看实现功能的代码(这里以delete为例)

/*
Copyright © 2019 NAME HERE <EMAIL ADDRESS>

Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at

    http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package cmd

import (
    "fmt"

    "github.com/spf13/cobra"
)

// deleteCmd represents the delete command
var deleteCmd = &cobra.Command{
    Use:   "delete",
    Short: "A brief description of your command",
    Long: `A longer description that spans multiple lines and likely contains examples
and usage of using your command. For example:

Cobra is a CLI library for Go that empowers applications.
This application is a tool to generate the needed files
to quickly create a Cobra application.`,
    Run: func(cmd *cobra.Command, args []string) {
        fmt.Println("delete called")
    },
}

func init() {
    rootCmd.AddCommand(deleteCmd)

    // Here you will define your flags and configuration settings.

    // Cobra supports Persistent Flags which will work for this command
    // and all subcommands, e.g.:
    // deleteCmd.PersistentFlags().String("foo", "", "A help for foo")

    // Cobra supports local flags which will only run when this command
    // is called directly, e.g.:
    // deleteCmd.Flags().BoolP("toggle", "t", false, "Help message for toggle")
}

我们来尝试修改代码

package cmd

import (
        "fmt"
        "github.com/spf13/cobra"
)

var deleteCmd = &cobra.Command{
        Use:   "delete",
        Short: "这是删除的简短介绍",
        Long:  `这是删除的长介绍.`,
        Run: func(cmd *cobra.Command, args []string) {
                fmt.Println("我是执行删除操作的!")
        },
}

func init() {
        rootCmd.AddCommand(deleteCmd)
}

然后执行 go run main.go delete 如你所见,预期返回!

带有第一级和第二级命令的实用程序

在本节中,您将学习如何将子命令添加到现有命令中——子命令是仅与特定命令相关联的命令。在本例中,我们将实现上一节中创建的实用程序的delete和list命令的all子命令。insert命令不需要这样的功能。

cobra add delete_all -p 'deleteCmd'

在这种情况下,应该使用delete命令的内部表示形式,即deleteCmd。all是delete的子命令这一事实是在./cmd/all的init()函数中定义的。如下:

// ./cmd/all.go
func init() {
        deleteCmd.AddCommand(allCmd)
}

同时我们添加list的子命令all

three cobra add list_all -p 'listCmd'

这里有个坑:

我们创建的子命令成了 list_all,我们修改代码为all即可

因为我们一旦创建 all自命令,那么list和delete的文件就冲突了

好了,到此执行下go run main.go delete all

go run main.go delete all
deleteAll called

带有flag命令的实用程序

这一次,我们将创建一个命令行实用程序,其中包含一个全局标志和一个仅连接到特定命令的标志。

全局flags
一个标志可以是“持久的”,这意味着这个标志将对它分配给的命令以及该命令下的每个命令可用。对于全局标志,将标志指定为根上的持久标志。

rootCmd.PersistentFlags().BoolVarP(&Verbose, "verbose", "v", false, "verbose output")
Local Flags

还可以在本地分配一个标志,它只适用于特定的命令。

localCmd.Flags().StringVarP(&Source, "source", "s", "", "Source directory to read from")

总结

其实cobra拥有更加强大的功能.例如别名,参数的检验等,但这不是最常用的,所以有需求我么可以查文档.