Skip to content

博客

创建型模式

  • 简单工厂模式(Simple Factory)
  • 单例模式(Singleton)
  • 工厂方法模式(Factory Method)
  • 抽象工厂模式(Abstract Factory)
  • 创建者模式(Builder)
  • 原型模式(Prototype)

Go中没有构造函数,一般都是以NewXxx函数来进行相关初始化工作,当NewXxx函数返回接口时,这就是一个简单工厂模式,包中会封装接口的实现细节。如:

package simple
type API interface {
Do()
}
func NewAPI() API {
return api{}
}
type api struct {
}
func (a api) Do() {
//TBD...
}

使用

var api = simple.NewAPI()
api.DO()

单例模式将类型的实例化限制为单个对象。

package singleton
type singleton map[string]string
var (
once sync.Once
instance singleton
)
func New() singleton {
once.Do(func() {
instance = make(singleton)
})
return instance
}

使用

s := singleton.New()
s["this"] = "that"
s2 := singleton.New()
fmt.Println("This is ", s2["this"])
//This is that

经验:

单例模式表现的是一个全局状态,同时在大多数时候会降低可测试性。

TBD…

TBD…

TBD…

TBD…

设计模式:Options Pattern In Golang

下面将记录函数可选(functional options)模式在golang中的实现。

函数可选参数模式(或者可选参数模式、Optional Parameters Pattern),用在当构造函数和公共函数API需要可选参数,特别时当具有三个或者更多可选参数时。

这个模式的优势在于,可以实现一个方法,并用下面的简单方式进行调用,如:

obj.Method(mandatory1, mandatory2)

或者向下面这样,通过是用可选的参数,来改变它的行为:

obj.Method(mandatory1, mandatory2, option1, option2, option3)

这可以避免为可选参数,使用笨重的零值参数:

obj.Method(mandatory1, mandatory2, nil, "", 0)

或者使用同样笨重的使用配置对象的方式:

cfg := &ConfigForMethod{
Optional1: ...,
Optional2: ...,
Optional3: ...,
}
obj.Method(mandatory1, mandatory2, &cfg)

使用一个Option接口,该接口保存一个未导出的方法,同时在一个未导出options结构中记录可选的参数信息。

package db
type options struct {
cache bool
limit int
logger *zap.Logger
}
type Option interface {
apply(*options)
}
//cache
type cacheOption bool
func (c cacheOption)apply(opts *options){
opts.cache = bool(c)
}
func WithCache(c bool)Option{
return cacheOption(c)
}
//limit
type limitOption int
func (l limitOption)apply(opts *options){
opts.limit = int(l)
}
func WithLimit(limit int)Option{
return limitOption(limit)
}
//logger
type loggerOption struct {
logger *zap.Logger
}
func (l loggerOption)apply(opts *options){
opts.logger = l.logger
}
func WithLogger(log *zap.Logger) Option{
return loggerOption{logger: log}
}

函数:

package db
func Open(addr string, opts ...Option)(*Connection, error){
//默认值
options := options {
cache: 1,
limit: 1,
logger: zap.NewNop(),
}
for _, o := range opts {
o.apply(options)
}
//...
}

使用:

db.Open(addr)
db.Open(add, db.WithLimit(10))
db.Open(addr, db.WithLogger(log))
db.Open(addr, db.WithCache(1), db.WithLimit(10), db.WithLogger(log))

将Option定义为函数类型,使用闭包来实现

package db
type Option func(*options)
type options struct {
cache bool
limit int
logger *zap.Logger
}
func WitchCache(cache bool)Option{
return func(opts *options){
opts.cache = cache
}
}
func WithLimit(limit int) Option {
return func(opts *options){
opts.limit = limit
}
}
func WithLogger(log *zap.Logger) Option {
return func(opts *options) {
opts.logger = log
}
}

函数:

package db
func Open(addr string, opts ...Option)(*Connection, error){
//默认值
options := options {
cache: 1,
limit: 1,
logger: zap.NewNop(),
}
for _, o := range opts {
o(options)
}
//...
}

使用

db.Open(addr)
db.Open(add, db.WithLimit(10))
db.Open(addr, db.WithLogger(log))
db.Open(addr, db.WithCache(1), db.WithLimit(10), db.WithLogger(log))
type dailOption struct {
disableRetry bool
}
type DailOption interface {
apply(*dailOption)
}
type funcDialOption struct {
f func(*dailOption)
}
func (fdo *funcDialOption) apply(do *dailOption) {
fdo.f(do)
}
func newFuncDialOption(f func(o *dailOption)) *funcDialOption {
return &funcDialOption{f: f}
}
func WithDisableRetry() DailOption {
return newFuncDialOption(func(o *dailOption) {
o.disableRetry = true
})
}

函数:

// Dial creates a client connection to the given target.
func Dial(target string, opts ...DialOption) (*ClientConn, error) {
return DialContext(context.Background(), target, opts...)
}

使用:

grpc.Dial("localhost:8080")
grpc.Dial("localhost:8080", grpc.WithInsecure(), grpc.WithBlock())
grpc.Dial("localhost:8080", grpc.WithDisableRetry())

方式3中,需要将具体的配置选项暴露出来,而这种方式不用。

通用方式实现一个可复用组件,来实现一个具有下面形式参数的函数:

obj.Method(mandatory1, mandatory2, option1, option2, option3)

在内部,只需要像下面这样声明定义该方法:

func (obj *Object)Method(m1 Type1, m2 Type2, options ...Option) {
....
}

Option对象有两个部分,一个标识和一个值。标识和值都被声明成interface{},这样标识和值都可以是任意的数据类型。对于标识,通常最好使用一个未导出的空结构,如:

// Interface defines the minimum interface that an option must fulfill
type Option interface {
// Ident returns the "indentity" of this option, a unique identifier that
// can be used to differentiate between options
Ident() interface{}
// Value returns the corresponding value.
Value() interface{}
}
type pair struct {
ident interface{}
value interface{}
}
// New creates a new Option
func New(ident, value interface{}) Option {
return &pair{
ident: ident,
value: value,
}
}
func (p *pair) Ident() interface{} {
return p.ident
}
func (p *pair) Value() interface{} {
return p.value
}
type identOptionalParamOne struct{}
type identOptionalParamTwo struct{}
type identOptionalParamThree struct{}
func WithOptionOne(v ...) Option {
return option.New(identOptionalParamOne{}, v)
}

然后,可以通过下面的方式,调用上面定义的Method方法:

obj.Method(m1, m2, WithOptionOne(...), WithOptionTwo(...), WithOptionThree(...))

同时Method的options参数,需要用类似下面的方式进行解析:

func (obj *Object) Method(m1 Type1, m2 Type2, options ...Option) {
paramOne := defaultValueParamOne
for _, option := range options {
switch option.Ident() {
case identOptionalParamOne{}:
paramOne = option.Value().(...)
}
}
...
}

Apple Login

这里记录了,接入AppleID登录,服务端的实现。

针对后端验证苹果提供了两种验证方式:

  • 一种是 基于JWT的算法验证
  • 一种是 基于授权码的验证

identityToken是一个经过签名的JSON Web Token的方式实现,更多见Sign in with Apple REST API。 另一种方式是校验授权码,其主要分为两步:1. 用户授权后获取code;2. 通过code换取token。

服务端验证identityToken时,App客户端需要将苹果接口返回的identityToken, userID这两个参数传递给服务器,用于验证本次登录的有效性。

服务端,获取到Apple公钥,来验证identityToken的有效性。

公钥获取接口:https://appleid.apple.com/auth/keys , 文档见Fetch Apple’s Public Key for Verifying Token Signature

代码在GitHub

实现1,主要使用github.com/dgrijalva/jwt-go 和 github.com/lestrrat-go/jwx/jwk

func VerfiyToken(token string) error {
_, err := jwt.Parse(token, func(t *jwt.Token) (interface{}, error) {
set, err := jwk.Fetch(context.Background(), "https://appleid.apple.com/auth/keys")
if err != nil {
return nil, err
}
kid := t.Header["kid"]
if key, ok := set.LookupKeyID(fmt.Sprint(kid)); ok {
if _, ok := t.Method.(*jwt.SigningMethodRSA); !ok {
return nil, fmt.Errorf("unexpected signing method: %s", t.Header["alg"])
}
var rawKey interface{}
if err = key.Raw(&rawKey); err != nil {
return nil, err
}
return rawKey, nil
}
return nil, nil
})
return err
}

实现2 github.com/lestrrat-go/jwx/jwk

func VerifyToken2(payload string) error {
set, err := jwk.Fetch(context.Background(), "https://appleid.apple.com/auth/keys")
if err != nil {
return err
}
token, err := ljwt.Parse([]byte(payload), ljwt.WithKeySet(set), ljwt.UseDefaultKey(true))
if err != nil {
return err
}
fmt.Println(token)
return nil
}

依赖注入wire

wire是Google开源的一款代码生成工具,通过依赖注入来自动链接不同的组件。

依赖注入 是用来构建地耦合,灵活易维护地代码的标注技术。在wire中,组件间的依赖关系表示为函数参数(function parameters),通过显示的初始化而不是全局变量。wire没有使用运行时状态或反射来实现依赖注入,而是使用代码生成。

像Wire这样的依赖注入工具旨在简化初始化代码的管理。可以将服务及其依赖关系描述为代码或配置,Wire计算出依赖关系图,并确认如何传递每个服务所需的内容。通过更改函数签名、添加或删除初始化项来更改应用程序的依赖关系,然后Wire为整个依赖关系图生成初始化代码。

Golang社区中其他的依赖注入框架,如来自Uber的dig,Facebook的inject。他们都通过反射机制实现运行时依赖注入。Wire主要灵感来自于Dagger 2,使用代码生成而不是反射或service locators。这样做带来的好处:

  • 当依赖关系更加复杂时,运行时依赖注入会变得难以跟踪和调试。使用代码生成意味着在运行时执行的初始化代码是常规的,惯用的Go代码,易于理解和调试。特别是像忘记依赖之类的问题,将变成编译时错误而不是运行时错误
  • 因为不需要 Service Locators, 所以对命名没有特殊要求
  • 避免依赖膨胀变得更简单。Wire生成的代码仅仅只包含需要的依赖,所以构建出的程序中不会含有没有使用过的依赖。而运行时依赖注入直到执行时,才能识别出没有使用过的依赖
  • 依赖关系静态存于源码之中, 便于工具分析与可视化

Wire的两个核心概念是:providersinjectors

provider是Wire的核心机制,它是一个可以返回一个值的普通Go函数(function),如:

package foobarbaz
type Foo struct {
X int
}
// ProvideFoo returns a Foo.
func ProvideFoo() Foo {
return Foo{X: 42}
}

为了可以在其他包中使用,provider函数必须要可以是可以导出的,同时provider函数可以使用参数来指定依赖:

// ProvideBar returns a Bar: a negative Foo.
func ProvideBar(foo Foo) Bar {
return Bar{X: -foo.X}
}

provider也可以返回error:

// ProvideBaz returns a value if Bar is not zero.
func ProvideBaz(ctx context.Context, bar Bar) (Baz, error) {
if bar.X == 0 {
return Baz{}, errors.New("cannot provide baz when bar is zero")
}
return Baz{X: bar.X}, nil
}

通过ProviderSet可以将一组provider组合到一起,实践中,通常将一些一起使用的provider放到一起组成一个ProviderSet:

var SuperSet = wire.NewSet(ProvideFoo, ProvideBar, ProvideBaz)

provider set 可以再放入到另一个provider set 中:

var MegaSet = wire.NewSet(SuperSet, pkg.OtherSet)

injector是由Wire工具自动生成的函数,它将按照依赖顺序调用相关的provider。

通过编写一个函数来声明一个injector,在这个函数体中,将provider作为参数调用wire.Build。wire只关注这个定义函数的返回类型,wire在生成代码时会忽略函数的返回值。

在定义injector的函数中

// +build wireinject
// The build tag makes sure the stub is not built in the final build.
package main
import (
"context"
"github.com/google/wire"
"example.com/foobarbaz"
)
func initializeBaz(ctx context.Context) (foobarbaz.Baz, error) {
wire.Build(foobarbaz.MegaSet)
return foobarbaz.Baz{}, nil
}

在包目录上执行wire命令,生成injector的代码会放到wire_gen.go文件中。如:

// Code generated by Wire. DO NOT EDIT.
//go:generate go run github.com/google/wire/cmd/wire
//+build !wireinject
package main
// Injectors from wire.go:
func InitializeEvent() (Event, error) {
message := NewMessage()
greeter := NewGreeter(message)
event, err := NewEvent(greeter)
if err != nil {
return Event{}, err
}
return event, nil
}

Go标准库atomic

在 Go 语言标准库中,sync/atomic包将底层硬件提供的原子操作封装成了 Go 的函数。但这些操作只支持几种基本数据类型,Go 语言在 1.4 版本的时候向sync/atomic包中添加了一个新的类型Value。此类型的值相当于一个容器,可以被用来“原子地”存储(Store)和加载(Load)任意类型的值。

原子操作可以是一个步骤,也可以是多个操作步骤,但是其顺序不可以被打乱,也不可以被切割而只执行其中的一部分。即一个操作或者多个操作,要么全部执行并且执行的过程不会被任何因素打断,要么就都不执行。

原子性不可能由软件单独保证—必须需要硬件的支持,因此是和架构相关的。在x86 平台上,CPU提供了在指令执行期间对总线加锁的手段。CPU芯片上有一条引线#HLOCK pin,如果汇编语言的程序中在一条指令前面加上前缀”LOCK”,经过汇编以后的机器代码就使CPU在执行这条指令的时候把#HLOCK pin的电位拉低,持续到这条指令结束时放开,从而把总线锁住,这样同一总线上别的CPU就暂时不能通过总线访问内存了,保证了这条指令在多处理器环境中的原子性。

mutex操作系统实现,而atomic包中的原子操作则由底层硬件直接提供支持。因此原子操作可以在lock-free的情况下保证并发安全,并且它的性能也能做到随 CPU 个数的增多而线性扩展。

atomic提供的函数功能需要非常小心才能正确使用,除了在一些特殊的low-level程序中,同步最好使用通道或sync包的设施来完成。

1.4 版本前,sync/atomic包支持几种基本数据类型的原子操作,之后新增的atomic.Value类型,供一致类型值的原子加载和存储,支持任意类型的数据,内部的字段是一个interface{}类型。

// A Value provides an atomic load and store of a consistently typed value.
// The zero value for a Value returns nil from Load.
// Once Store has been called, a Value must not be copied.
//
// A Value must not be copied after first use.
type Value struct {
v interface{}
}

参见:Go 语言标准库中 atomic.Value 的前世今生

unsafe.Pointer 可以绕过 Go 语言类型系统的检查,完成任何类型与内建的 uintptr 类型之间的转化。unsafe.Pointer 可以实现四种其他类型不能的操作:

  • 任何类型的指针都可以转化为一个 unsafe.Pointer
  • 一个 unsafe.Pointer 可以转化成任何类型的指针
  • 一个 uintptr 可以转化成一个 unsafe.Pointer
  • 一个 unsafe.Pointer 可以转化成一个 uintptr
  1. [atomic doc] https://pkg.go.dev/sync/atomic
  2. [Go 语言标准库中 atomic.Value 的前世今生] https://mp.weixin.qq.com/s?__biz=MzAxMTA4Njc0OQ==&mid=2651445490&idx=2&sn=03e00ae51a511392a9923f6d72349fc9&scene=21#wechat_redirect
  3. [原子操作] https://baike.baidu.com/item/%E5%8E%9F%E5%AD%90%E6%93%8D%E4%BD%9C/1880992?fr=aladdin