配置加载

服务在启动的时候通常都需要一些配置来提供启动参数,如:监听端口,其他服务连接密钥等。

这里建议所有配置都通过环境变量读取,这样做的原因有以下几点:

  1. 生产环境的系统配置可能较为复杂,可以根据不同环境调整配置不同参数
  2. 生产环境密钥应在运维范畴保密,不应透露到开发者层面
  3. 开发环境更为复杂,开发者可以根据自己的环境灵活配置启动

go标准包os中提供了获取环境变量的方法:

1
2
3
4
5
6
7
8
9
10
package main

import (
"os"
)

func main() {
port := os.Getenv("UC_LISTEN_PORT")
// ...
}

服务定义和启动

为了避免全局变量的失控,我们通常会为服务定义一个实例,然后通过实例接口让其在main中被加载和启动。

那么对于一个http服务,我们需要为其实现http.Handler接口:

1
2
3
4
5
6
7
8
9
10
11
// Service impement http.Handler interface
// which can be initialized in a http server.
type Service struct{}

func New() *Service {
return &Service{}
}

func (s *Service) ServeHTTP(w http.ResponseWriter, req *http.Request) {
// TODO ...
}

然后,在main中实例化我们定义的服务,然后就可以通过标准包net/http提供的方法启动了:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
package main

import (
"net/http"
"os"

uc "github.com/mapleque/gostart/ms/uc/service"
)

func main() {
port := os.Getenv("UC_LISTEN_PORT")
s := uc.New()
server.ListenAndServe("0.0.0.0:"+port, s)
}

路由和处理函数

依据标准包net/http的实现,所有请求最终都会经过ServeHTTP方法处理,并且每个请求的处理都是一个单独的协程。

所以在ServeHTTP方法中,我们主要实现的就是为当前请求分配处理函数。这里也可以直接使用标准包net/http中的ServeMux实现:

1
2
3
4
5
6
7
8
9
10
// Service implement http.Handler interface
// which can be initialized in a http server.
type Service struct {
mux *http.ServeMux
}

// ServeHTTP implement http.Handler interface.
func (s *Service) ServeHTTP(w http.ResponseWriter, req *http.Request) {
s.mux.ServeHTTP(w, req)
}

很明显,要让

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
func (s *Service) signin(w http.ResponseWriter, req *http.Request) {
// TODO deal with /signin request
}

func (s *Service) signout(w http.ResponseWriter, req *http.Request) {
// TODO deal with /signout request
}

func (s *Service) userinfo(w http.ResponseWriter, req *http.Request) {
// deal with /userinfo request
switch req.Method {
case http.MethodGet:
s.getUserinfo(w, req)
case http.MethodPost:
s.postUserinfo(w, req)
default:
// return 405
w.WriteHeader(http.StatusMethodNotAllowed)
}
}

func (s *Service) getUserinfo(w http.ResponseWriter, req *http.Request) {
// TODO deal with GET /userinfo request
}

func (s *Service) postUserinfo(w http.ResponseWriter, req *http.Request) {
// TODO deal with POST /userinfo request
}

参数定义和校验

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
type SigninParam struct {
Username string `json:"username"`
Password string `json:"password"`
}

func bindAndValid(req *http.Request, obj interface{}) error {
if req == nil || req.Body == nil {
return fmt.Errorf("invalid request")
}
decoder := json.NewDecoder(req)
if err := decoder.Decode(obj); err != nil {
return err
}
// use github.com/go-playground/validator/v10
return validate.Struct(obj)
}

func (s *Service) signin(w http.ResponseWriter, req *http.Request) {
param := SigninParam{}
if err := bindAndValid(req, &param); err != nil {
// TODO response param-check error message
return
}
// TODO do login
// TODO response successful data
}

处理返回

1
2
3
4
5
6
7
8
9
type Response struct {
Status int `json:"status"`
Data interface{} `json:"data"`
Message interface{} `json:"message"`
}

type UserinfoResponse struct {
Token string `json:"token"`
}
1
2
3
4
5
6
7
func response(w, resp interface{}) {
encoder := json.NewEncoder(w)
err := encoder.Encode(resp)
if err != nil {
panic(err)
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
func (s *Service) signin(w http.ResponseWriter, req *http.Request) {
param := SigninParam{}
if err := bindAndValid(req, &param); err != nil {
// response param-check error message
//
// To custom with the error message,
// use https://github.com/go-playground/universal-translator
// see example at: https://github.com/go-playground/validator/blob/master/_examples/translations/main.go#L105
response(w, Response{StatusInvalidParam, nil, MessageInvalidParam}
return
}
// ... do login
// response successful data
response(w, Response{StatusSuccess, UserinfoResponse{token}, nil})
}

调用其他服务

这里以使用mysql服务为例,使用github.com/go-sql-driver/mysql

首先,想要在handle中使用mysql服务,就需要能够获取mysql的连接,通常我们会将服务连接池初始化放在服务初始化时进行:

1
2
3
4
5
type Service struct {
mux *http.ServeMux
// add a db property
db *sql.DB
}

日志输出

  1. 产品的最简模型
    采用何种技术架构能够做到高效快速的验证服务可行性?
    本文核心思想是:尽可能的依托第三方平台实现核心产品逻辑。
  2. 独立应用的基本架构
    自研产品都需要考虑哪些技术投入?
    一个完整的产品技术架构,不只是客户端和服务端,需要考虑的细节还很多,这些都将在本文中一一阐述。
  3. 统计数据收集展示和系统监控
    如何快速构建一个独立的统计数据收集和系统监控平台?
    本文主要讲述的是从第三方统计平台到自研数据平台演化过程和实现方案。
  4. 快速迭代和持续集成
    在保证稳定性的前提下如何提升迭代效率?
    本文的观点:完整的单元测试和自动化运维平台是快速迭代和持续集成的基础。
  5. 面向高可用的逐步升级
    不会挂的系统才是好系统,线上系统远比你想象的脆弱。
    那么如何面对:突发流量,系统故障,黑客攻击,程序员跑路等各种奇葩问题,将在本文中详细介绍。
  6. 接入第三方应用账号认证
    第三方账号登陆是引流必须跨过的一道门槛,技术上也并不复杂。
    本文主要介绍的就是接入微信,支付宝,google,facebook,twitter等的套路。
  7. 接入支付系统
    2c业务想赚钱,必须接入支付。
    那么接入微信,支付宝,银联,ApplePay等,都需要准备什么,需要注意避开哪些坑,本文将会提及。
  8. 多人合作模式
    在绝大多数情况下,多人合作的结果一定是1+1<2,但是你又不得不考虑。
    并不是因为产品需求繁重,而是为了应付不可控的人员流动。