<?xml version="1.0" encoding="UTF-8"?><rss version="2.0"
	xmlns:content="http://purl.org/rss/1.0/modules/content/"
	xmlns:wfw="http://wellformedweb.org/CommentAPI/"
	xmlns:dc="http://purl.org/dc/elements/1.1/"
	xmlns:atom="http://www.w3.org/2005/Atom"
	xmlns:sy="http://purl.org/rss/1.0/modules/syndication/"
	xmlns:slash="http://purl.org/rss/1.0/modules/slash/"
	>

<channel>
	<title>Golang归档 - 枫阿雨&#039;s blog</title>
	<atom:link href="https://www.crazyfay.com/tag/golang/feed/" rel="self" type="application/rss+xml" />
	<link>https://www.crazyfay.com/tag/golang/</link>
	<description>CrazyFay</description>
	<lastBuildDate>Sun, 28 May 2023 06:27:16 +0000</lastBuildDate>
	<language>zh-CN</language>
	<sy:updatePeriod>
	hourly	</sy:updatePeriod>
	<sy:updateFrequency>
	1	</sy:updateFrequency>
	<generator>https://wordpress.org/?v=6.5.2</generator>

<image>
	<url>https://www.crazyfay.com/wp-content/uploads/2023/04/cropped-DockerGopher-32x32.png</url>
	<title>Golang归档 - 枫阿雨&#039;s blog</title>
	<link>https://www.crazyfay.com/tag/golang/</link>
	<width>32</width>
	<height>32</height>
</image> 
	<item>
		<title>面向对象设计模式与Go语言实现 &#8211; 创建型模式</title>
		<link>https://www.crazyfay.com/2023/05/28/%e8%bd%af%e4%bb%b6%e8%ae%be%e8%ae%a1%e6%a8%a1%e5%bc%8f%e4%b8%8ego%e8%af%ad%e8%a8%80%e5%ae%9e%e7%8e%b0-%e5%88%9b%e5%bb%ba%e5%9e%8b%e6%a8%a1%e5%bc%8f/</link>
					<comments>https://www.crazyfay.com/2023/05/28/%e8%bd%af%e4%bb%b6%e8%ae%be%e8%ae%a1%e6%a8%a1%e5%bc%8f%e4%b8%8ego%e8%af%ad%e8%a8%80%e5%ae%9e%e7%8e%b0-%e5%88%9b%e5%bb%ba%e5%9e%8b%e6%a8%a1%e5%bc%8f/#respond</comments>
		
		<dc:creator><![CDATA[crazyfay]]></dc:creator>
		<pubDate>Sun, 28 May 2023 05:17:53 +0000</pubDate>
				<category><![CDATA[学习笔记]]></category>
		<category><![CDATA[Golang]]></category>
		<category><![CDATA[设计模式]]></category>
		<guid isPermaLink="false">https://www.crazyfay.com/?p=349</guid>

					<description><![CDATA[<p>创建型模式（Creational Patterns） 设计模式概览 创建型模式（Creational Patt [&#8230;]</p>
<p><a rel="nofollow" href="https://www.crazyfay.com/2023/05/28/%e8%bd%af%e4%bb%b6%e8%ae%be%e8%ae%a1%e6%a8%a1%e5%bc%8f%e4%b8%8ego%e8%af%ad%e8%a8%80%e5%ae%9e%e7%8e%b0-%e5%88%9b%e5%bb%ba%e5%9e%8b%e6%a8%a1%e5%bc%8f/">面向对象设计模式与Go语言实现 &#8211; 创建型模式</a>最先出现在<a rel="nofollow" href="https://www.crazyfay.com">枫阿雨&#039;s blog</a>。</p>
]]></description>
										<content:encoded><![CDATA[<h2>创建型模式（Creational Patterns）</h2>
<h3>设计模式概览</h3>
<ul>
<li>
<p><strong>创建型模式（Creational Patterns）：</strong></p>
<ol>
<li>
<p><strong>单例模式（Singleton）</strong></p>
</li>
<li>
<p><strong>工厂方法模式（Factory Method）</strong></p>
</li>
<li>
<p><strong>抽象工厂模式（Abstract Factory）</strong></p>
</li>
<li>
<p><strong>建造者模式（Builder）</strong></p>
</li>
<li>
<p><strong>原型模式（Prototype）</strong></p>
</li>
</ol>
</li>
<li>
<p>结构型模式（Structural Patterns）：</p>
<ol start="6">
<li>
<p>适配器模式（Adapter）</p>
</li>
<li>
<p>桥接模式（Bridge）</p>
</li>
<li>
<p>组合模式（Composite）</p>
</li>
<li>
<p>装饰者模式（Decorator）</p>
</li>
<li>
<p>外观模式（Facade）</p>
</li>
<li>
<p>享元模式（Flyweight）</p>
</li>
<li>
<p>代理模式（Proxy）</p>
</li>
</ol>
</li>
<li>
<p>行为型模式（Behavioral Patterns）：</p>
<ol start="13">
<li>
<p>责任链模式（Chain of Responsibility）</p>
</li>
<li>
<p>命令模式（Command）</p>
</li>
<li>
<p>解释器模式（Interpreter）</p>
</li>
<li>
<p>迭代器模式（Iterator）</p>
</li>
<li>
<p>中介者模式（Mediator）</p>
</li>
<li>
<p>备忘录模式（Memento）</p>
</li>
<li>
<p>观察者模式（Observer）</p>
</li>
<li>
<p>状态模式（State）</p>
</li>
<li>
<p>策略模式（Strategy）</p>
</li>
<li>
<p>模板方法模式（Template Method）</p>
</li>
<li>
<p>访问者模式（Visitor）</p>
</li>
</ol>
</li>
</ul>
<h3>0. 简单工厂模式</h3>
<blockquote>
<p>简单工厂模式（Simple Factory Pattern）</p>
<p>又叫做静态工厂方法模式（Static Factory Method Pattern），是一种对象创建型模式，并不属于GoF 的23种设计模式之一，但是是学习其他工厂模式的的基础。它定义了一个工厂类，用于创建不同类型的对象。简单来说，简单工厂模式就是将对象的创建过程封装起来，使得客户端可以通过一个工厂类来创建不同类型的对象，而无需知道具体的创建过程。</p>
<p>实现简单工厂，通常需要定义一个工厂方法来创建不同类型的对象。这个工厂方法通常是一个静态方法，它接收一个参数来指定要创建的对象类型，并返回一个实例化的对象。</p>
</blockquote>
<pre><code class="language-go">type MemTableType = int8
const (
    Btree MemTableType = iota
    ART
)

type MemTable interface {
    // Put Stores the Pos information for key pairs in the index
    Put(key, value []byte) bool

    // Get Retrieve the Pos information based on the key
    Get(key []byte) []byte

    // Del Delete the Pos information based on the key
    Del(key []byte) bool
}

type BTree struct {
    tree *btree.BTree
    lock *sync.RWMutex
}

// NewBTree Init BTree struct
func NewBTree() *BTree {
    return &amp;BTree{
        tree: btree.New(32),
        lock: new(sync.RWMutex),
    }
}

type AdaptiveRadixTree struct {
    tree art.Tree
    lock *sync.RWMutex
}

func NewAdaptiveRadixTree() *AdaptiveRadixTree {
    return &amp;AdaptiveRadixTree{
        tree: art.New(),
        lock: new(sync.RWMutex),
    }
}

func NewMemTable(typ MemTableType) MemTable {
    switch typ {
    case Btree:
        return NewBTree()
    case ART:
        return NewAdaptiveRadixTree()
    default:
        return NewBTree()
    }
}
func main()  {
    memTable := NewMemTable(Btree)
    memTable.Put([]byte(&quot;key&quot;), []byte(&quot;value&quot;))
}</code></pre>
<h3>1. 工厂方法模式</h3>
<blockquote>
<p><strong>工厂方法模式（Factory Method Pattern）</strong></p>
<p><strong>工厂方法模式也被称为虚拟构造器模式（Virtual Constructor Pattern）或多态工厂模式（Polymorphic Factory Pattern）</strong></p>
<p><strong>简单工厂模式虽然简单，但存在一个很严重的问题，违背了开闭原则。工厂方法模式，继承了简单工厂模式的优点，同时做出了修改以达到符合开闭原则的要求。在工厂方法模式中，不再提供一个统一的工厂类来创建所有的产品对象，而是针对不同的产品提供不同的工厂，系统提供一个与产品等级结构对应的工厂等级结构。</strong></p>
<p><strong>在很多场合有其实际应用，这种模式主要用于创建复杂对象，创建对象的逻辑可能包含一些业务需要的约束。</strong></p>
<p><strong>假设我们正在开发一个日志记录系统，这个系统可以将日志记录到不同的地方，例如，控制台，文件，或者是远程服务器。我们可以使用工厂方法模式来创建适合不同场景的日志记录器。</strong></p>
</blockquote>
<pre><code class="language-go">// Logger 是所有日志记录器的接口
type Logger interface {
    Log(message string)
}

// ConsoleLogger 是 Logger 的一个实现，它将日志记录到控制台
type ConsoleLogger struct{}

func (l ConsoleLogger) Log(message string) {
    fmt.Println(&quot;Console logger: &quot;, message)
}

// FileLogger 是 Logger 的一个实现，它将日志记录到文件
type FileLogger struct{}

func (l FileLogger) Log(message string) {
    fmt.Println(&quot;File logger: &quot;, message)
}

// LoggerFactory 是所有日志记录器工厂的接口
type LoggerFactory interface {
    CreateLogger() Logger
}

// ConsoleLoggerFactory 是 LoggerFactory 的一个实现，它创建 ConsoleLogger
type ConsoleLoggerFactory struct{}

func (f ConsoleLoggerFactory) CreateLogger() Logger {
    return ConsoleLogger{}
}

// FileLoggerFactory 是 LoggerFactory 的一个实现，它创建 FileLogger
type FileLoggerFactory struct{}

func (f FileLoggerFactory) CreateLogger() Logger {
    return FileLogger{}
}

func main() {
    var factory LoggerFactory

    factory = ConsoleLoggerFactory{}
    logger := factory.CreateLogger()
    logger.Log(&quot;This is a message&quot;)

    factory = FileLoggerFactory{}
    logger = factory.CreateLogger()
    logger.Log(&quot;This is another message&quot;)
}
</code></pre>
<h3>2. 抽象工厂模式</h3>
<blockquote>
<p><strong>抽象工厂模式（Abstract Factory Pattern）</strong></p>
<p><strong>抽象工厂模式是一种创建型设计模式，它提供了一种在不指定具体类的情况下创建一系列相关或相互依赖的对象的接口。抽象工厂允许你根据需求切换不同的具体工厂实现，以便在运行时根据配置或其他条件创建适当的对象。</strong><br />
<strong>简单来说，抽象工厂模式就是将多个工厂类的接口进行抽象，然后再用一个工厂类来封装这些工厂类的接口。在创建具体对象时，我们通常需要使用其他对象或者数据结构，因此我们还需要定义一些相关的产品接口和产品结构体。</strong></p>
</blockquote>
<pre><code class="language-go">// User 表示用户实体
type User struct {
    ID   int
    Name string
}

// UserDAO 定义了操作用户的接口
type UserDAO interface {
    GetUser(id int) (*User, error)
    SaveUser(*User) error
    DeleteUser(id int) error
}

// DBFactory 定义了抽象工厂接口
type DBFactory interface {
    CreateUserDAO() UserDAO
}

// MysqlUserDAO 实现了UserDAO接口，提供了对MySQL数据库的操作
type MysqlUserDAO struct{}

func (dao *MysqlUserDAO) GetUser(id int) (*User, error) {
    // 实现从MySQL数据库获取用户的具体操作
    // ...
    fmt.Println(&quot;Get user from MySQL database&quot;)
    return &amp;User{}, nil
}

func (dao *MysqlUserDAO) SaveUser(u *User) error {
    // 实现保存用户到MySQL数据库的具体操作
    // ...
    fmt.Println(&quot;Save user to MySQL database&quot;)
    return nil
}

func (dao *MysqlUserDAO) DeleteUser(id int) error {
    // 实现从MySQL数据库删除用户的具体操作
    // ...
    fmt.Println(&quot;Delete user from MySQL database&quot;)
    return nil
}

// MysqlFactory 实现了DBFactory接口，用于创建MysqlUserDAO
type MysqlFactory struct{}

func (f *MysqlFactory) CreateUserDAO() UserDAO {
    return &amp;MysqlUserDAO{}
}

// MongoUserDAO 实现了UserDAO接口，提供了对MongoDB数据库的操作
type MongoUserDAO struct{}

func (dao *MongoUserDAO) GetUser(id int) (*User, error) {
    // 实现从MongoDB数据库获取用户的具体操作
    // ...
    fmt.Println(&quot;Get user from MongoDB database&quot;)
    return &amp;User{}, nil
}

func (dao *MongoUserDAO) SaveUser(u *User) error {
    // 实现保存用户到MongoDB数据库的具体操作
    // ...
    fmt.Println(&quot;Save user to MongoDB database&quot;)
    return nil
}

func (dao *MongoUserDAO) DeleteUser(id int) error {
    // 实现从MongoDB数据库删除用户的具体操作
    // ...
    fmt.Println(&quot;Delete user from MongoDB database&quot;)
    return nil
}

// MongoFactory 实现了DBFactory接口，用于创建MongoUserDAO
type MongoFactory struct{}

func (f *MongoFactory) CreateUserDAO() UserDAO {
    return &amp;MongoUserDAO{}
}

func main() {
    mysqlFactory := &amp;MysqlFactory{}
    mongoFactory := &amp;MongoFactory{}

    userDAO := mysqlFactory.CreateUserDAO()
    userDAO.GetUser(1)
    userDAO.SaveUser(&amp;User{})
    userDAO.DeleteUser(1)

    userDAO = mongoFactory.CreateUserDAO()
    userDAO.GetUser(1)
    userDAO.SaveUser(&amp;User{})
    userDAO.DeleteUser(1)
}</code></pre>
<p>工厂方法模式和抽象工厂模式都属于创建型设计模式，它们都是用来创建对象的。但是，两者有一些重要的区别：</p>
<p><strong>工厂方法模式（Factory Method Pattern）：</strong></p>
<ul>
<li>工厂方法模式中的每个工厂只创建一种产品。</li>
<li>工厂方法模式主要用于创建一种类型的对象，但是这个对象的构造过程可能比较复杂。</li>
<li>在工厂方法模式中，客户端通常只需要知道具体工厂的接口，而不需要知道具体工厂类的类型。具体工厂类的类型可以在运行时通过配置或者其他方式决定。</li>
<li>例如，在上面的日志记录器的例子中，我们有一个日志记录器的工厂接口和多个实现这个接口的工厂类，每个工厂类都用于创建一种特定的日志记录器。</li>
</ul>
<p><strong>抽象工厂模式（Abstract Factory Pattern）：</strong></p>
<ul>
<li>抽象工厂模式中的每个工厂可以创建多种类型的产品。</li>
<li>抽象工厂模式主要用于创建相关的或者是依赖的对象组。对象组中的每个对象都是一个产品，但是这些产品需要一起工作以完成一些更大的功能。</li>
<li>在抽象工厂模式中，客户端通常只需要知道具体工厂的接口，而不需要知道具体工厂类的类型。具体工厂类的类型可以在运行时通过配置或者其他方式决定。</li>
<li>例如，在上面的数据库访问的例子中，我们有一个数据库访问对象的工厂接口和多个实现这个接口的工厂类，每个工厂类都用于创建一组相关的数据库访问对象，如用户数据访问对象，订单数据访问对象等。</li>
</ul>
<p>总的来说，如果你只需要创建一种类型的产品，那么使用工厂方法模式可能更简单；如果你需要创建多种类型的相关产品，那么使用抽象工厂模式可能更合适。</p>
<h3>3. 建造者模式</h3>
<blockquote>
<p><strong>建造者模式（Builder Pattern）</strong></p>
<p><strong>建造者模式是一种对象创建型模式，它可以将复杂对象的构建过程与表示分离开来，使得相同的构建过程可以创建不同的表示。</strong></p>
<p><strong>简单来说，建造者模式就是将一个复杂对象的创建过程基于链式调用封装起来，使得这个过程可以有不同的表示方式。</strong></p>
</blockquote>
<pre><code class="language-go">// RequestBuilder 是网络请求的建造者接口
type RequestBuilder interface {
    SetURL(url string) RequestBuilder
    SetMethod(method string) RequestBuilder
    SetHeader(key, value string) RequestBuilder
    SetBody(body string) RequestBuilder
    Build() *Request
}

// Request 是网络请求对象
type Request struct {
    URL     string
    Method  string
    Headers map[string]string
    Body    string
}

// HTTPRequestBuilder 是 HTTP 请求的具体建造者
type HTTPRequestBuilder struct {
    request *Request
}

// SetURL 设置请求的 URL
func (b *HTTPRequestBuilder) SetURL(url string) RequestBuilder {
    b.request.URL = url
    return b
}

// SetMethod 设置请求的方法
func (b *HTTPRequestBuilder) SetMethod(method string) RequestBuilder {
    b.request.Method = method
    return b
}

// SetHeader 设置请求的头信息
func (b *HTTPRequestBuilder) SetHeader(key, value string) RequestBuilder {
    b.request.Headers[key] = value
    return b
}

// SetBody 设置请求的消息体
func (b *HTTPRequestBuilder) SetBody(body string) RequestBuilder {
    b.request.Body = body
    return b
}

// Build 构建网络请求对象
func (b *HTTPRequestBuilder) Build() *Request {
    return b.request
}

// NewHTTPRequestBuilder 创建 HTTP 请求的建造者
func NewHTTPRequestBuilder() RequestBuilder {
    return &amp;HTTPRequestBuilder{
        request: &amp;Request{
            Headers: make(map[string]string),
        },
    }
}

func main() {
    // 创建一个 HTTP 请求的建造者
    builder := NewHTTPRequestBuilder()

    // 使用建造者设置请求的属性
    request := builder.
        SetURL(&quot;https://api.example.com&quot;).
        SetMethod(&quot;GET&quot;).
        SetHeader(&quot;Authorization&quot;, &quot;Bearer token&quot;).
        SetBody(&quot;data&quot;).
        Build()

    // 打印请求对象
    fmt.Println(request)
}</code></pre>
<h3>4. 原型模式</h3>
<blockquote>
<p><strong>原型模式（Prototype Pattern）</strong></p>
<p><strong>原型模式是一种创建型设计模式，它允许通过克隆现有对象来创建新对象，而无需通过实例化和配置新对象来完成。在原型模式中，我们创建一个原型对象，然后通过复制该对象来创建新的对象，而不是使用常规的构造函数和初始化流程。</strong></p>
<p><strong>应该注意原型模式不是用来获得性能优势的。它仅用于从原型实例创建新对象！</strong></p>
<p><strong>原型模式的实现需要满足以下要素：</strong></p>
<ul>
<li>
<p><strong>定义一个原型对象，该对象实现了 Clone() 方法，用于克隆自身并返回新的克隆对象。</strong></p>
</li>
<li>
<p><strong>定义一个工厂函数或方法，用于创建原型对象并初始化其属性。</strong></p>
</li>
<li>
<p><strong>客户端代码可以通过调用原型对象的 Clone() 方法来创建新的对象，而不是直接调用构造函数或工厂函数。</strong></p>
</li>
</ul>
</blockquote>
<pre><code class="language-go">// HTTPRequest 是HTTP请求的原型对象
type HTTPRequest struct {
    Method  string
    URL     string
    Headers map[string]string
    Body    []byte
}

// Clone 通过复制原型对象创建新的HTTP请求对象
func (r *HTTPRequest) Clone() *HTTPRequest {
    clone := *r
    return &amp;clone
}

// HTTPRequestPrototype 是HTTP请求的原型管理器
type HTTPRequestPrototype struct {
    prototypes map[string]*HTTPRequest
}

// Register 注册HTTP请求原型对象
func (p *HTTPRequestPrototype) Register(name string, request *HTTPRequest) {
    p.prototypes[name] = request
}

// Retrieve 根据名称从原型管理器中检索HTTP请求原型对象
func (p *HTTPRequestPrototype) Retrieve(name string) *HTTPRequest {
    prototype, ok := p.prototypes[name]
    if !ok {
        return nil
    }
    return prototype.Clone()
}

func main() {
    // 创建HTTP请求原型管理器
    prototypeManager := &amp;HTTPRequestPrototype{
        prototypes: make(map[string]*HTTPRequest),
    }

    // 注册不同的HTTP请求原型对象
    prototypeManager.Register(&quot;service1&quot;, &amp;HTTPRequest{
        Method: &quot;GET&quot;,
        URL:    &quot;http://service1.example.com&quot;,
        Headers: map[string]string{
            &quot;Content-Type&quot;: &quot;application/json&quot;,
        },
    })

    prototypeManager.Register(&quot;service2&quot;, &amp;HTTPRequest{
        Method: &quot;POST&quot;,
        URL:    &quot;http://service2.example.com&quot;,
        Headers: map[string]string{
            &quot;Content-Type&quot;: &quot;application/xml&quot;,
        },
        Body: []byte(`&lt;data&gt;Hello, Service 2&lt;/data&gt;`),
    })

    // 启动中介服务
    http.HandleFunc(&quot;/&quot;, func(w http.ResponseWriter, r *http.Request) {
        // 根据请求路径选择对应的HTTP请求原型对象
        name := r.URL.Path[1:] // 从路径中获取名称，例如 &quot;/service1&quot; 将匹配到 &quot;service1&quot;
        requestPrototype := prototypeManager.Retrieve(name)
        if requestPrototype == nil {
            http.NotFound(w, r)
            return
        }

        // 复制原型对象，并根据实际请求进行自定义修改
        request := requestPrototype.Clone()
        request.URL = fmt.Sprintf(&quot;%s%s&quot;, request.URL, r.URL.Path)

        // 发送请求并获取响应
        client := &amp;http.Client{}
        httpRequest, err := http.NewRequest(request.Method, request.URL, nil)
        if err != nil {
            http.Error(w, err.Error(), http.StatusInternalServerError)
            return
        }

        // 设置请求标头
        for key, value := range request.Headers {
            httpRequest.Header.Set(key, value)
        }

        // 发送请求
        response, err := client.Do(httpRequest)
        if err != nil {
            http.Error(w, err.Error(), http.StatusInternalServerError)
            return
        }
        defer response.Body.Close()

        // 读取响应内容
        body, err := ioutil.ReadAll(response.Body)
        if err != nil {
            http.Error(w, err.Error(), http.StatusInternalServerError)
            return
        }

        // 将响应内容写入到HTTP响应中
        w.Header().Set(&quot;Content-Type&quot;, response.Header.Get(&quot;Content-Type&quot;))
        w.Write(body)
    })

    // 启动HTTP服务器，监听端口
    err := http.ListenAndServe(&quot;:8080&quot;, nil)
    if err != nil {
        fmt.Println(&quot;Failed to start server:&quot;, err)
    }
}</code></pre>
<h3>5. 单例模式</h3>
<blockquote>
<p><strong>单例模式（Singleton Pattern）</strong></p>
<p><strong>顾名思义,指的就是一个类只能生成一个实例，且该类能自行创建这个实例的一种模式。</strong><br />
<strong>在服务的生命周期内，一个类生成的一个实例对象只能存在一个，调用时复用该单例对象即可，这样既节省了内存空间,也节省了创建新对象时的资源消耗，也便于资源管理。</strong></p>
<p><strong>在日常开发中,很多场景其实都可以被设计成单例，像线程池、全局日志、数据库等等，单例模式总结下来有几个特点:</strong></p>
<ul>
<li><strong>单例类只有一个实例对象；</strong></li>
<li><strong>该单例对象必须由单例类自行创建；</strong></li>
<li><strong>单例类对外提供一个访问该单例的全局访问点</strong></li>
</ul>
</blockquote>
<p>单例模式一般有两种实现方式</p>
<p><strong>饿汉式（Eager Initialization）：</strong></p>
<ul>
<li>
<p>在程序启动时或单例类被加载时，就立即创建并初始化单例实例。</p>
</li>
<li>
<p>单例实例在整个程序生命周期中都存在，无论是否使用。</p>
</li>
<li>
<p>线程安全，因为在实例创建时就完成了初始化。</p>
</li>
<li>
<p>示例代码：</p>
<pre><code class="language-go">type singleton struct {
}

var _instance *singleton

func init() {
_instance = &singleton{}
}

func GetInstance() *singleton {
return _instance
}</code></pre>
</li>
</ul>
<p><strong>懒汉式（Lazy Initialization）：</strong></p>
<ul>
<li>
<p>在第一次调用获取单例实例的方法时，才创建并初始化单例实例。</p>
</li>
<li>
<p>单例实例是按需创建的，可能存在多个线程同时请求获取单例实例的情况。</p>
</li>
<li>
<p>需要考虑线程安全性，通常使用锁或其他同步机制来保证只有一个实例被创建。</p>
</li>
<li>
<p>示例代码：</p>
<pre><code class="language-go">type singleton struct {
}

var _instance *singleton
var once sync.Once

// GetInstance lazy
func GetInstance() *singleton {
once.Do(func() {
    _instance = &singleton{}
})
return _instance
}</code></pre>
</li>
</ul>
<h3>extra 配置模式</h3>
<blockquote>
<p>选项模式（Options Pattern）是一种常用的设计模式，得益于对函数式编程的支持，选项模式Go语言中经常使用。选项模式用于在函数或结构体中设置多个可选参数，以便在调用时提供更灵活的配置选项。</p>
</blockquote>
<pre><code class="language-go">type Options struct {
   Path    string
   MaxSize int
   Sync    bool
}

// DefaultOptions 返回默认配置选项
func DefaultOptions() *Options {
   return _defaultConf
}

var _defaultConf = &amp;Options{
   Path:    &quot;/temp/opt&quot;,
   MaxSize: 100,
   Sync:    true,
}

// NewOptions 处理配置选项
func NewOptions(opts ...func(*Options)) *Options {
   // 获取默认配置选项
   options := _defaultConf

   // 遍历传入的选项函数列表，逐个应用选项函数
   for _, opt := range opts {
      opt(options)
   }

   return options
}

func WithPath(value string) func(*Options) {
   return func(opts *Options) {
      opts.Path = value
   }
}

func WithMaxSize(value int) func(*Options) {
   return func(opts *Options) {
      opts.MaxSize = value
   }
}

func WithSync(value bool) func(*Options) {
   return func(opts *Options) {
      opts.Sync = value
   }
}

func main() {
   // 使用选项模式调用函数
   opt := NewOptions(
      WithPath(&quot;custom value&quot;),
      WithMaxSize(200),
      WithSync(false),
   )

   fmt.Println(opt)
}</code></pre>
<hr />
<p><img decoding="async" src="https://www.crazyfay.com/wp-content/uploads/2023/05/工厂方法.png" alt="工厂方法模式" /><br />
<img decoding="async" src="https://www.crazyfay.com/wp-content/uploads/2023/05/抽象工厂.png" alt="抽象工厂模式" /><br />
<img decoding="async" src="https://www.crazyfay.com/wp-content/uploads/2023/05/生成器.png" alt="建造者模式" /><br />
<img decoding="async" src="https://www.crazyfay.com/wp-content/uploads/2023/05/原型.png" alt="原型模式" /><br />
<img decoding="async" src="https://www.crazyfay.com/wp-content/uploads/2023/05/单例.png" alt="单例模式" /></p>
<p><a rel="nofollow" href="https://www.crazyfay.com/2023/05/28/%e8%bd%af%e4%bb%b6%e8%ae%be%e8%ae%a1%e6%a8%a1%e5%bc%8f%e4%b8%8ego%e8%af%ad%e8%a8%80%e5%ae%9e%e7%8e%b0-%e5%88%9b%e5%bb%ba%e5%9e%8b%e6%a8%a1%e5%bc%8f/">面向对象设计模式与Go语言实现 &#8211; 创建型模式</a>最先出现在<a rel="nofollow" href="https://www.crazyfay.com">枫阿雨&#039;s blog</a>。</p>
]]></content:encoded>
					
					<wfw:commentRss>https://www.crazyfay.com/2023/05/28/%e8%bd%af%e4%bb%b6%e8%ae%be%e8%ae%a1%e6%a8%a1%e5%bc%8f%e4%b8%8ego%e8%af%ad%e8%a8%80%e5%ae%9e%e7%8e%b0-%e5%88%9b%e5%bb%ba%e5%9e%8b%e6%a8%a1%e5%bc%8f/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
			</item>
		<item>
		<title>面向对象设计模式与Go语言实现 – 面向对象设计原则</title>
		<link>https://www.crazyfay.com/2023/05/27/%e9%9d%a2%e5%90%91%e5%af%b9%e8%b1%a1%e8%ae%be%e8%ae%a1%e6%a8%a1%e5%bc%8f%e4%b8%8ego%e8%af%ad%e8%a8%80%e5%ae%9e%e7%8e%b0-%e9%9d%a2%e5%90%91%e5%af%b9%e8%b1%a1%e8%ae%be%e8%ae%a1%e5%8e%9f/</link>
					<comments>https://www.crazyfay.com/2023/05/27/%e9%9d%a2%e5%90%91%e5%af%b9%e8%b1%a1%e8%ae%be%e8%ae%a1%e6%a8%a1%e5%bc%8f%e4%b8%8ego%e8%af%ad%e8%a8%80%e5%ae%9e%e7%8e%b0-%e9%9d%a2%e5%90%91%e5%af%b9%e8%b1%a1%e8%ae%be%e8%ae%a1%e5%8e%9f/#respond</comments>
		
		<dc:creator><![CDATA[crazyfay]]></dc:creator>
		<pubDate>Sat, 27 May 2023 02:13:22 +0000</pubDate>
				<category><![CDATA[学习笔记]]></category>
		<category><![CDATA[Golang]]></category>
		<category><![CDATA[设计模式]]></category>
		<guid isPermaLink="false">https://www.crazyfay.com/?p=353</guid>

					<description><![CDATA[<p>本系列文章是针对设计模式的系列文章，网上的很多设计模式的文章只讲了个大概和一部分纯理论，示例代码也是实际场景中 [&#8230;]</p>
<p><a rel="nofollow" href="https://www.crazyfay.com/2023/05/27/%e9%9d%a2%e5%90%91%e5%af%b9%e8%b1%a1%e8%ae%be%e8%ae%a1%e6%a8%a1%e5%bc%8f%e4%b8%8ego%e8%af%ad%e8%a8%80%e5%ae%9e%e7%8e%b0-%e9%9d%a2%e5%90%91%e5%af%b9%e8%b1%a1%e8%ae%be%e8%ae%a1%e5%8e%9f/">面向对象设计模式与Go语言实现 – 面向对象设计原则</a>最先出现在<a rel="nofollow" href="https://www.crazyfay.com">枫阿雨&#039;s blog</a>。</p>
]]></description>
										<content:encoded><![CDATA[<p>本系列文章是针对设计模式的系列文章，网上的很多设计模式的文章只讲了个大概和一部分纯理论，示例代码也是实际场景中根本遇不到，往往只会出现在课本中的无生产意义的代码模型。本系列文章则希望在讲述设计模式的同时，采用更贴合生产实践中的代码，在实战中学习设计模式的相关概念，理解设计模式的精髓。</p>
<h2>面向对象设计原则</h2>
<p>关于面向对象设计原则众说纷纭，有人说是6种有人说是7种，但无论有几种实际上内容都是一致的。</p>
<p>本文将基于 亚历山大 · 什韦茨（Alexander Shvets）的《深入设计模式》中的分类，介绍面软件设计原则。</p>
<h3>1. 封装</h3>
<p>尽可能的封装变化的内容，即找到程序中的变化内容并将其与不变的内容区分开。该原则的主要目的是将变更造成的影响最小化。</p>
<p>可进行方法层面的封装与类层面的封装</p>
<h3>2. 面向接口</h3>
<p>面向接口进行开发， 而不是面向实现； 依赖于抽象类型，而不是具体类。</p>
<p>如果无需修改已有代码就能轻松对类进行扩展，那就可以说这样的设计是灵活的。</p>
<p>当你需要两个类进行合作时，可以让其中一个类依赖于另一个类。但是，还有另外一种更灵活的方式来设置对象之间的合作关系。</p>
<ol>
<li>确定一个对象对另一对象的确切需求：它需执行哪些方法？</li>
<li>在一个新的接口或抽象类中描述这些方法。</li>
<li>让被依赖的类实现该接口。</li>
<li>现在让有需求的类依赖于这个接口， 而不依赖于具体的类。你仍可与原始类中的对象进行互动，但现在其连接将会灵活得多。</li>
</ol>
<h3>3. 组合</h3>
<p>组合优于继承。继承可能是类之间最明显、最简便的代码复用方式。如果你有两个代码相同的类， 就可以为它们创建一个通用的基类，<br />
然后将相似的代码移动到其中。轻而易举！</p>
<p>不过，继承这件事通常只有在程序中已包含大量类，且修改任何东西都非常困难时才会引起关注。下面就是此类问题的清单。</p>
<ul>
<li><strong>子类不能减少超类的接口。</strong>你必须实现父类中所有的抽象方法，即使它们没什么用。</li>
<li><strong>在重写方法时，你需要确保新行为与其基类中的版本兼容。</strong>这一点很重要，因为子类的所有对象都可能被传递给以超类<br />
对象为参数的任何代码，相信你不会希望这些代码崩溃的。</li>
<li>继承打破了超类的封装，因为子类拥有访问父类内部详细内容的权限。此外还可能会有相反的情况出现，那就是程序员为了进一步扩展的方便而让超类知晓子类的内部详细内容。</li>
<li><strong>子类与超类紧密耦合。</strong>超类中的任何修改都可能会破坏子类的功能。</li>
<li><strong>通过继承复用代码可能导致平行继承体系的产生。</strong>继承通常仅发生在一个维度中。只要出现了两个以上的维度，你就必须创建数量巨大的类组合，从而使类层次结构膨胀到不可思议的程度。</li>
</ul>
<p>好在Golang在语言层面就解决了这个问题，因为Go语言中没有继承 XD</p>
<h3>4. SOLID原则</h3>
<p>SOLID 的五条原则是在罗伯特·马丁的著作《敏捷软件开发：原则、模式与实践》中首次提出的。SOLID 是让软件设计更易于理解、更加灵活和更易于维护的五个原则的简称。</p>
<h4>S：单一职责原则</h4>
<blockquote>
<p>单一职责原则（Single Responsibility Principle）</p>
<p>修改一个类的原因只能有一个。</p>
</blockquote>
<p>尽量让每个类只负责软件中的一个功能，并将该功能完全封装（你也可称之为隐藏）在该类中。<br />
这条原则的主要目的是减少复杂度。你不需要费尽心机地去构思如何仅用200 行代码来实现复杂设计，实际上完全可以使用十几个清晰的方法。<br />
当程序规模不断扩大、变更不断增加后，真实问题才会逐渐显现出来。到了某个时候，类会变得过于庞大，以至于你无法记住其细节。查找代码将变得非常缓慢，你必须浏览整个类，甚至整个程序才能找到需要的东西。程序中实体的数量会让你的大脑堆栈过载，你会感觉自己对代码失去了控制。<br />
还有一点：如果类负责的东西太多，那么当其中任何一件事发生改变时，你都必须对类进行修改。而在进行修改时，你就有可能改动类中自己并不希望改动的部分。</p>
<h4>O：开闭原则</h4>
<blockquote>
<p>开闭原则（open/closed Principle）</p>
<p>对于扩展， 类应该是“开放”的； 对于修改， 类则应是“封闭”的。</p>
</blockquote>
<p>本原则的主要理念是在实现新功能时能保持已有代码不变。</p>
<p>如果你可以对一个类进行扩展，可以创建它的子类并对其做任何事情（如新增方法或成员变量、重写基类行为等）， 那么它就是开放的。有些编程语言允许你通过特殊关键字（例如final ） 来限制对于类的进一步扩展， 这样类就不再是“开放”的了。如果某个类已做好了充分的准备并可供其他类使用的话（即其接口已明确定义且以后不会修改），那么该类就是封闭（你可以称之为完整）的。</p>
<p>如果一个类已经完成开发、测试和审核工作，而且属于某个框架或者可被其他类的代码直接使用的话，对其代码进行修改就是有风险的。你可以创建一个子类并重写原始类的部分内容以完成不同的行为，而不是直接对原始类的代码进行修改。这样你既可以达成自己的目标，但同时又无需修改已有的原始类客户端。</p>
<p>这条原则并不能应用于所有对类进行的修改中。如果你发现类中存在缺陷，直接对其进行修复即可，不要为它创建子类。<br />
子类不应该对其父类的问题负责。</p>
<h4>L：里氏替换原则</h4>
<blockquote>
<p>里氏替换原则（Liskov Substitution Principle）</p>
<p>当你扩展一个类时， 记住你应该要能在不修改客户端代码的情况下将子类的对象作为父类对象进行传递。</p>
</blockquote>
<p>这意味着子类必须保持与父类行为的兼容。在重写一个方法时，你要对基类行为进行扩展，而不是将其完全替换。<br />
替换原则是用于预测子类是否与代码兼容，以及是否能与其超类对象协作的一组检查。这一概念在开发程序库和框架时<br />
非常重要， 因为其中的类将会在他人的代码中使用——你是无法直接访问和修改这些代码的。<br />
与有着多种解释方式的其他设计模式不同，替代原则包含一组对子类（特别是其方法）的形式要求。</p>
<ul>
<li>
<p><strong>子类方法的参数类型必须与其超类的参数类型相匹配或更加抽象。</strong></p>
<p>即设计子类方法时，尽可能的将入参类型设置为父类的类型</p>
</li>
<li>
<p><strong>子类方法的返回值类型必须与超类方法的返回值类型或是其子类别相匹配。</strong></p>
<p>即设计子类方法时，尽可能的将入参类型设置为子类的类型</p>
</li>
<li>
<p><strong>子类中的方法不应抛出基础方法预期之外的异常类型</strong></p>
<p>在C++、Java语言中，如果抛出基础方法预期之外的异常类型，编译时会发生错误，而Golang、Rust等使用ErrCode处理错误的语言则需要额外注意返回错误的处理</p>
</li>
<li>
<p><strong>子类不应该加强其前置条件</strong></p>
<p>例如，基类的方法有一个<code>int</code>类型的参数。如果子类重写该方法时，要求传递给该方法的参数值必须为正数（如果该值为负则抛出异常）， 这就是加强了前置条件。客户端代码之前将负数传递给该方法时程序能够正常运行，但现在使用子类的对象时会使程序出错。</p>
</li>
<li>
<p><strong>子类不能削弱其后置条件</strong></p>
<p>假如你的某个类中有个方法需要使用数据库，该方法应该在接收到返回值后关闭所有活跃的数据库连接。<br />
你创建了一个子类并对其进行了修改，使得数据库保持连接以便重用。但客户端可能对你的意图一无所知。由于它认为<br />
该方法会关闭所有的连接，因此可能会在调用该方法后就马上关闭程序，使得无用的数据库连接对系统造成“污染”。</p>
</li>
<li>
<p><strong>超类的不变量必须保留</strong></p>
<p>不变量是让对象有意义的条件。因此，扩展一个类的最安全做法是引入新的成员变量和方法，而不要去招惹超类中已<br />
有的成员。当然在实际中，这并非总是可行。</p>
</li>
<li>
<p><strong>子类不能修改超类中私有成员变量的值</strong></p>
<p>有些编程语言允许通过反射机制来访问类的私有成员。还有一些语言（Python 和JavaScript）没有对私有成员进行任何保护。</p>
</li>
</ul>
<blockquote>
<p>其实总结一点就是，暴露给用户的总是父类/接口，所以在子类实现父类/接口的方法时，要以上层为准。</p>
</blockquote>
<h4>I：接口隔离原则</h4>
<blockquote>
<p>接口隔离原则（Interface Segregation Principle）</p>
<p>客户端不应被强迫依赖于其不使用的方法。</p>
</blockquote>
<p>尽量缩小接口的范围，使得客户端的类不必实现其不需要的行为。</p>
<p>根据接口隔离原则，你必须将“臃肿”的方法拆分为多个颗粒度更小的具体方法。客户端必须仅实现其实际需要的方法。否则，对于“臃肿”接口的修改可能会导致程序出错，即使客户端根本没有使用修改后的方法。</p>
<p>继承只允许类拥有一个超类，但是它并不限制类可同时实现的接口的数量。因此，你不需要将大量无关的类塞进单个接口。你可将其拆分为更精细的接口，如有需要可在单个类中实现所有接口，某些类也可只实现其中的一个接口。</p>
<p>请过度使用这条原则。不要进一步划分已经非常具体的接口。记住，创建的接口越多，代码就越复杂。因此要保持平衡。</p>
<h4>D：依赖倒置原则</h4>
<blockquote>
<p>依赖倒置原则（Dependency Inversion Principle）</p>
<p>高层次的类不应该依赖于低层次的类。两者都应该依赖于抽象接口。抽象接口不应依赖于具体实现。具体实现应该依赖于抽象接口。</p>
</blockquote>
<p>通常在设计软件时，你可以辨别出不同层次的类。</p>
<ul>
<li>低层次的类实现基础操作（例如磁盘操作、传输网络数据和<br />
连接数据库等）。</li>
<li>高层次类包含复杂业务逻辑以指导低层次类执行特定操作。</li>
</ul>
<p>有时人们会先设计低层次的类， 然后才会开发高层次的类。当你在新系统上开发原型产品时，这种情况很常见。由于低层次的东西还没有实现或不确定，你甚至无法确定高层次类能实现哪些功能。如果采用这种方式，业务逻辑类可能会更依赖于低层原语类。</p>
<p>依赖倒置原则通常和开闭原则共同发挥作用：你无需修改已有类就能用不同的业务逻辑类扩展低层次的类。</p>
<h2>面向对象设计原则与设计模式</h2>
<p>在1995 年，GoF（Gang of Four，四人组/四人帮）合作出版了《设计模式：可复用面向对象软件的基础》一书，共收录了 23 种设计模式，从此树立了软件设计模式领域的里程碑，人称「GoF设计模式」。<br />
这23种设计模式包括了三大类型，即<strong>创建型设计模式、结构型设计模式、行为型设计模式</strong>，以此为分类依据，这23种设计模式的概览如下：</p>
<ul>
<li>
<p>创建型模式（Creational Patterns）：</p>
<ol>
<li>
<p>单例模式（Singleton）</p>
</li>
<li>
<p>工厂方法模式（Factory Method）</p>
</li>
<li>
<p>抽象工厂模式（Abstract Factory）</p>
</li>
<li>
<p>建造者模式（Builder）</p>
</li>
<li>
<p>原型模式（Prototype）</p>
</li>
</ol>
</li>
<li>
<p>结构型模式（Structural Patterns）：</p>
<ol start="6">
<li>
<p>适配器模式（Adapter）</p>
</li>
<li>
<p>桥接模式（Bridge）</p>
</li>
<li>
<p>组合模式（Composite）</p>
</li>
<li>
<p>装饰者模式（Decorator）</p>
</li>
<li>
<p>外观模式（Facade）</p>
</li>
<li>
<p>享元模式（Flyweight）</p>
</li>
<li>
<p>代理模式（Proxy）</p>
</li>
</ol>
</li>
<li>
<p>行为型模式（Behavioral Patterns）：</p>
<ol start="13">
<li>
<p>责任链模式（Chain of Responsibility）</p>
</li>
<li>
<p>命令模式（Command）</p>
</li>
<li>
<p>解释器模式（Interpreter）</p>
</li>
<li>
<p>迭代器模式（Iterator）</p>
</li>
<li>
<p>中介者模式（Mediator）</p>
</li>
<li>
<p>备忘录模式（Memento）</p>
</li>
<li>
<p>观察者模式（Observer）</p>
</li>
<li>
<p>状态模式（State）</p>
</li>
<li>
<p>策略模式（Strategy）</p>
</li>
<li>
<p>模板方法模式（Template Method）</p>
</li>
<li>
<p>访问者模式（Visitor）</p>
</li>
</ol>
</li>
</ul>
<p>设计模式是践行面向对象设计原则的良好案例，本系列文章将按照这三种分类，讲述这23种经典的设计模式，并适当扩展在此23种设计模式之外的常用设计模式</p>
<h2>写在后面</h2>
<p>面向对象设计原则与设计模式，不一定总是适用的，在一些复杂场景下，运用设计模式可以将系统解耦，增加系统的可维护性、可拓展性，降低系统的复杂度。但是过度运用设计模式反而会大大增加系统的复杂度和理解难度，所以请适当使用设计模式。<br />
而且面向对象中的很多设计都过于理想化，实际生产过程中很大一部分都没有完全利用到面向对象理想中的最大的优势，因为将一个系统模块进行面向对象的抽象见面在一些复杂场景下往往是极具挑战性，或者说从实践角度上来说是意义不大的。正如如今在服务端开发领域用的最多的MVC模型，其本质上就是典型的面向过程的贫血模型，而真正面向对象充血模式的DDD，多数是难以落地的，多是进行进一步封装抽象，过度追求面向对象往往只会徒增系统复杂度。<br />
所以说，学习设计模式的过程是一个则其善者而从之，其不善者而改之的过程，软件设计没有银弹，只有在合适的场合运用合适的设计。</p>
<p><a rel="nofollow" href="https://www.crazyfay.com/2023/05/27/%e9%9d%a2%e5%90%91%e5%af%b9%e8%b1%a1%e8%ae%be%e8%ae%a1%e6%a8%a1%e5%bc%8f%e4%b8%8ego%e8%af%ad%e8%a8%80%e5%ae%9e%e7%8e%b0-%e9%9d%a2%e5%90%91%e5%af%b9%e8%b1%a1%e8%ae%be%e8%ae%a1%e5%8e%9f/">面向对象设计模式与Go语言实现 – 面向对象设计原则</a>最先出现在<a rel="nofollow" href="https://www.crazyfay.com">枫阿雨&#039;s blog</a>。</p>
]]></content:encoded>
					
					<wfw:commentRss>https://www.crazyfay.com/2023/05/27/%e9%9d%a2%e5%90%91%e5%af%b9%e8%b1%a1%e8%ae%be%e8%ae%a1%e6%a8%a1%e5%bc%8f%e4%b8%8ego%e8%af%ad%e8%a8%80%e5%ae%9e%e7%8e%b0-%e9%9d%a2%e5%90%91%e5%af%b9%e8%b1%a1%e8%ae%be%e8%ae%a1%e5%8e%9f/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
			</item>
		<item>
		<title>Bitmap的设计实现及实战应用</title>
		<link>https://www.crazyfay.com/2023/04/08/bitmap%e7%9a%84%e8%ae%be%e8%ae%a1%e5%ae%9e%e7%8e%b0%e5%8f%8a%e5%ae%9e%e6%88%98%e5%ba%94%e7%94%a8/</link>
					<comments>https://www.crazyfay.com/2023/04/08/bitmap%e7%9a%84%e8%ae%be%e8%ae%a1%e5%ae%9e%e7%8e%b0%e5%8f%8a%e5%ae%9e%e6%88%98%e5%ba%94%e7%94%a8/#respond</comments>
		
		<dc:creator><![CDATA[crazyfay]]></dc:creator>
		<pubDate>Sat, 08 Apr 2023 11:52:01 +0000</pubDate>
				<category><![CDATA[代码实战]]></category>
		<category><![CDATA[Bitmap]]></category>
		<category><![CDATA[Golang]]></category>
		<guid isPermaLink="false">http://net.crazyfay.xyz/?p=237</guid>

					<description><![CDATA[<p>大家估计都知道今天的主角Bitmap（位图）这个东西，也都知道它是一种非常有趣且鹅妹子嘤带点儿“黑科技”的数据 [&#8230;]</p>
<p><a rel="nofollow" href="https://www.crazyfay.com/2023/04/08/bitmap%e7%9a%84%e8%ae%be%e8%ae%a1%e5%ae%9e%e7%8e%b0%e5%8f%8a%e5%ae%9e%e6%88%98%e5%ba%94%e7%94%a8/">Bitmap的设计实现及实战应用</a>最先出现在<a rel="nofollow" href="https://www.crazyfay.com">枫阿雨&#039;s blog</a>。</p>
]]></description>
										<content:encoded><![CDATA[<p>大家估计都知道今天的主角Bitmap（位图）这个东西，也都知道它是一种非常有趣且鹅妹子嘤带点儿“黑科技”的数据结构，它能够用非常非常低的存储成本存储数据的状态，在这篇文章中，我将从0到1实现一个Bitmap，并基于Redis的Bitmap在实际应用场景中发挥Bitmap的优点。</p>
<h2>实现一个Bitmap</h2>
<h3>原理与实现思路</h3>
<p>我们首先来简单的再过一遍位图的原理，Bitmap是一种将每一个字节用到极致的数据结构，位图的使用场景实际上和布尔数组是相同的，都是表示某个索引代表的数据非0即1的二元对立关系，只不过数据在bitmap中的存储会更加紧凑，使用bit位来作为下标索引，以0或1来代替bool的false or true。当需要在bool数组中判断下标1024的数据是否为true时，就等价于在bitmap中的第1024位是否为1。bool数组在大多数语言中每个bool值的大小都是1byte（8bit）这么算下来，bitmap可以省去8倍的存储空间。</p>
<blockquote>
<p>在Java中，单个bool值占4byte，而bool数组的底层实现实际为byte数组，即1byte</p>
</blockquote>
<p>在绝大多数的编程语言中，可供我们直接操作的最小单位是byte，只有一个字节，也就是8个bit（这与内存寻址是以byte为单位有关）。所以我们要想操作bit来实现bitmap，需要通过位移操作来完成。</p>
<pre><code class="language-golang">Bitmap实例与创建方法
// Bitmap的数据底层由byte slice组织
type Bitmap []byte

// 通过指定的bit长度新建一个Bitmap实例
func MakeBitmapWithBitSize(nBits int) Bitmap {
    // 规定一个最小长度
    if nBits &lt; 64 {
        nBits = 64
    }
    // 需要确保bit长度为8的倍数
    return MakeBitmapWithByteSize((nBits + 7) / 8)
}

// 通过指定的byte长度新建一个Bitmap实例
func MakeBitmapWithByteSize(nBytes int) Bitmap {
    return make([]byte, nBytes)
}

// 注: pos的起始位置为0
// 将指定pos的bit设置为1
func (b Bitmap) SetTrue(bitPos uint32) {
    b[bitPos/8] |= 1 &lt;&lt; (bitPos % 8)
}

// 将指定pos的bit设置为0
func (b Bitmap) SetFalse(bitPos uint32) {
    b[bitPos/8] &amp;= ^(1 &lt;&lt; (bitPos % 8))
}

// 判断指定pos的bit是否为1
func (b Bitmap) IsTrue(bitPos uint32) bool {
    return b[bitPos/8]&amp;(1&lt;&lt;(bitPos%8)) != 0
}

// 重置Bitmap
func (b Bitmap) Reset() {
    for i := range b {
        b[i] = 0
    }
}

// Bitmap所存储的字节数
func (b Bitmap) ByteSize(bitPos uint32) int {
    return len(b)
}

// Bitmap所存储的位数
func (b Bitmap) BitSize(bitPos uint32) int {
    return len(b) * 8
}</code></pre>
<p>是的，我们这样就实现了一个Bitmap，是不是会感觉 <strong>就这？</strong> 确实，Bitmap本身就是一个非常简单朴实的数据结构，给人一种大道至简的感觉。 所有的核心操作都集中以下三步当中，我们来逐一拆解操作过程</p>
<pre><code class="language-golang">// SetTrue
b[bitPos/8] |= 1 &lt;&lt; (bitPos % 8)
// SetFalse
b[bitPos/8] &amp;= ^(1 &lt;&lt; (bitPos % 8))
// IsTrue
b[bitPos/8] &amp; (1 &lt;&lt; (bitPos % 8)) != 0</code></pre>
<h3>bi[bitPos/8] |= 1 &lt;&lt; (bitPos % 8)</h3>
<p>这个操作将Bitmap的 bitPos 位置上的bit位设置为1。 我们通过结合例子的方式逐步用白话解析这个操作：</p>
<ul>
<li><strong>现假设我需要将<code>0110 0101</code>的第2位通过bitmap.SetTrue(1)置为1</strong></li>
</ul>
<ol>
<li><strong>首先</strong>，bitPos / 8 计算出需要操作的字节索引（即第几个字节）。因为一个字节有8个bit位，所以我们使用除法运算来计算该位所在的那个字节。在此例中就是在下标为0的索引上的byte处。</li>
<li><strong>然后</strong>，bitPos % 8 计算出该字节中需要操作的位数。使用取模运算，可以计算出该位在字节中的偏移量。在此例中偏移量为1</li>
<li><strong>接下来</strong>，执行 1 &lt;&lt; (bitPos % 8) 按位左移运算，将 1（即0000 0001） 左移 bitPos % 8 位（即位于字节中的偏移量）。在此例中就是将 0000 0001 向左移1位得到 0000 0010</li>
<li><strong>最后</strong>，执行 b[bitPos/8] |= 1 &lt;&lt; (bitPos % 8) 操作，我们采用位或运算的操作&quot;|&quot;进行赋值（0|0 = 0、0|1 = 1、1|1 = 1）将该位设置为 1。在此例中就是计算0110 0101 | 0000 0010 = 0111 最终得到我们想要的结果 <code>0110 0111</code>。</li>
</ol>
<h3>b[bitPos/8] &amp;= ^(1 &lt;&lt; (bitPos % 8))</h3>
<p>这个操作将Bitmap的 bitPos 位置上的bit位设置为0。 我们通过结合例子的方式逐步用白话解析这个操作：</p>
<ul>
<li><strong>现假设我需要将<code>0110 0101</code>的第6位通过bitmap.SetFalse(5)置为0</strong></li>
</ul>
<ol>
<li><strong>首先</strong>，bitPos / 8 计算出需要操作的字节索引（即第几个字节）。因为一个字节有8个bit位，所以我们使用除法运算来计算该位所在的那个字节。在此例中就是在下标为0的索引上的byte处。</li>
<li><strong>然后</strong>，bitPos % 8 计算出该字节中需要操作的位数。使用取模运算，可以计算出该位在字节中的偏移量。在此例中偏移量为5</li>
<li><strong>接下来</strong>，执行 1 &lt;&lt; (bitPos % 8) 按位左移运算，将 1（即0001） 左移 bitPos % 8 位（即位于字节中的偏移量）。在此例中就是将 0000 0001 向左移5位得到 0010 0000</li>
<li><strong>接着</strong>，应用按位取反运算符将掩码取反（在Go语言中 &quot;^&quot; 作为一元运算符时表示按位取反操作），得到一个这个除了我们要删除的那个bit位之外所有位都是1的掩码。再此例中就是 1101 1111.</li>
<li><strong>最后</strong>，执行 b[bitPos/8] &amp;= ^(1 &lt;&lt; (bitPos % 8)) 操作，我们采用位或运算的操作&quot;&amp;&quot;进行赋值（0|0 = 0、0|1 =0、1|1 = 1）将该位设置为 0。在此例中就是计算 0110 0101 &amp; 1101 1111 = 0111 最终得到我们想要的结果 <code>0100 0101</code>。</li>
</ol>
<h3>b[bitPos/8 ] &amp; (1 &lt;&lt; (bitPos % 8)) != 0</h3>
<p>这个操作判断 bitmap 的 bitPos 位置上的二进制位是否为1。 我们通过结合例子的方式逐步用白话解析这个操作：</p>
<ul>
<li><strong>现假设我需要使用bitmap.IsTrue(6)判断<code>0110 0101</code>的第7位是否为1</strong></li>
</ul>
<ol>
<li><strong>首先</strong>，bitPos / 8 计算出需要操作的字节索引（即第几个字节）。因为一个字节有8个bit位，所以我们使用除法运算来计算该位所在的那个字节。在此例中就是在下标为0的索引上的byte处。</li>
<li><strong>然后</strong>，bitPos % 8 计算出该字节中需要操作的位数。使用取模运算，可以计算出该位在字节中的偏移量。在此例中偏移量为6</li>
<li><strong>接下来</strong>，执行 1 &lt;&lt; (bitPos % 8) 按位左移运算，将 1（即0000 0001） 左移 bitPos % 8 位（即位于字节中的偏移量）。在此例中就是将 0000 0001 向左移6位得到 0100 0000。</li>
<li><strong>接着</strong>，执行 b[bitPos/8] &amp; (1 &lt;&lt; (bitPos % 8)) 操作，我们通过与运算只保留bitmap中的指定字节其他bit位均为0，如果不为1的话，那么这8个bit全部都归零，如果为1，则这8个bit组成的byte必不等于0。在此例中就是计算 0110 0101 &amp; 0100 0000 = 0100 0000</li>
<li><strong>最后</strong>，执行 != 0 操作，如果该位为1，则返回true；否则返回false。</li>
</ol>
<p>怎么样是不是非常的清清爽爽~</p>
<p>既然造完了轮子咱们就开始实战吧~</p>
<h2>利用Redis的Bitmap实现用户点赞</h2>
<p>嘿嘿，bitmap的实战我偏不用自己造的轮子，就是玩儿~</p>
<blockquote>
<p>其实主要是Redis中的数据结构是可以跨进程共享的，所以一般应用的更广泛些，我们造好的轮子将在后面的实战文章中使用（用来造另一个轮子XD）</p>
</blockquote>
<p>在很多的业务中模块，都会有一个点赞的功能，点赞操作可能会比较频繁，而且因为可能会涉及到资源的热度和推荐指数，且用户的直接感知很强，所以也算是一个相对重要的功能。</p>
<p>点赞功能的设计有以下几点：</p>
<ol>
<li>要能够及时反馈，让用户有所感知，并且要尽量避免用户显示数据的不一致</li>
<li>要能够标记用户是否已点赞，避免用户刷赞操作</li>
<li>一个页面加载时可能会有大量需要统计点赞的资源，需要对点赞进行缓存处理，避免大量的磁盘io</li>
</ol>
<p>结合以上设计需求，我先说出最终可以选择的方案：</p>
<p>采用基于Redis的异步缓存写入策略，以资源类型的前缀与资源ID作为Key，维护一个Bitmap，以<strong>用户的自增ID</strong>作为Bitmap中的标识索引，实现用户的点赞操作的记录。后台运行一个定时定时任务，定时将Redis中的数据批量同步到数据库的持久化存储中。</p>
<p>这样设计有以下的特点：</p>
<ol>
<li>用户获取点赞数、执行点赞操作都是直接和基于内存的Redis打交道，速度相对较快。</li>
<li>使用Bitmap存储用户点赞记录，可省下大量的内存空间。</li>
<li>采用异步缓存写入策略，本质上是<strong>依赖Redis作为稳定的存储介质</strong>而非简单的缓存，只要Redis不崩溃在客户端就不会发生不一致的现象（比如点赞后用户刷新，发现点赞消失）。</li>
</ol>
<blockquote>
<p>当然，模块的设计与技术选型是需要根据业务本身的特点而定的，在当前的业务设计中就是默认Redis是较为可靠的存储，基本不会出现Redis崩溃的状况。</p>
</blockquote>
<h3>代码示例</h3>
<p>以评论模块的点赞操作为例</p>
<p>一些const枚举值</p>
<pre><code class="language-golang">type IsLike int
const (
    UNLIKE IsLike = iota
    LIKE
)

const(
  // COMMENT:LIKE:commentID -&gt; bitmap
  COMMENT_LIKE_REDISPREKEY = &quot;COMMENT:LIKE:&quot;
)</code></pre>
<p>核心方法</p>
<pre><code class="language-golang">// 更改点赞状态，由上游传入的isLike判断是点赞操作还是取消点赞操作
func (c *CommentDomain) ConvertLikeState(commentId int64, uid int64, isLike IsLike) *errs.BError {
    ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second)
    defer cancel()
    key := COMMENT_LIKE_REDISPREKEY+strconv.FormatInt(commentId, 10)
    err := c.redis.SetBit(ctx, key, uid, int(isLike)).Error()
    if err != nil {
        zap.L().Error(&quot;updateLikeState redis SetBit ERROR&quot;, zap.Error(err))
        return errs.RedisError
    }
    return nil
}

// 判断用户是否已点赞（前端需要知晓的状态）
func (c *CommentDomain) HasLiked(commentId int64, uid int64) (bool, *errs.BError) {
    ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second)
    defer cancel()
    key := COMMENT_LIKE_REDISPREKEY+strconv.FormatInt(commentId, 10)
    bit, err := c.redis.GetBit(ctx, key, uid).Result()
    if err != nil {
        zap.L().Error(&quot;HasLiked redis GetBit ERROR&quot;, zap.Error(err))
        return false, errs.RedisError
    }
    return bit == 1, nil
}

// 获取评论的点赞数
func (c *CommentDomain) CountLikeNum(commentId int64) (int64, *errs.BError) {
    ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second)
    defer cancel()
    key := COMMENT_LIKE_REDISPREKEY+strconv.FormatInt(commentId, 10)
    count, err := c.redis.BitCount(ctx, key, &amp;redis.BitCount{Start: 0, End: -1}).Result()
    if err != nil {
        zap.L().Error(&quot;CountLikeNum redis BitCount ERROR&quot;, zap.Error(err))
        return 0, errs.RedisError
    }
    return count, nil
}</code></pre>
<p>简单的测试用例</p>
<pre><code class="language-golang">var commentDomain = domain.NewCommentDomain()

func TestLike(t *testing.T) {
    count, err := commentDomain.CountLikeNum(5)
    log.Printf(&quot;init comment %d Like Num %d \n&quot;, 5, count)

    err = commentDomain.ConvertLikeState(5, 1000, LIKE)
    count, err = commentDomain.CountLikeNum(5)
    log.Printf(&quot;comment %d Like Num %d  after %d like \n&quot;, 5, count, 1000)

    err = commentDomain.ConvertLikeState(5, 1001, LIKE)
    count, err = commentDomain.CountLikeNum(5)
    log.Printf(&quot;comment %d Like Num %d  after %d like \n&quot;, 5, count, 1000)

    err = commentDomain.ConvertLikeState(5, 1001, UNLIKE)
    count, err = commentDomain.CountLikeNum(5)
    log.Printf(&quot;comment %d Like Num %d  after %d unlike \n&quot;, 5, count, 1001)

    liked, err := commentDomain.HasLiked(5, 1001)
    count, err = commentDomain.CountLikeNum(5)
    log.Printf(&quot;%d hasLiked comment %d : %t \n&quot;, 1001, count, liked)

    liked, err = commentDomain.HasLiked(5, 1000)
    count, err = commentDomain.CountLikeNum(5)
    log.Printf(&quot;%d hasLiked comment %d : %t \n&quot;, 1000, count, liked)

    if err != nil {
        log.Fatal(err)
    }
}</code></pre>
<p>输出结果</p>
<pre><code class="language-sh">=== RUN   TestLike
2023/04/08 16:52:33 init comment 5 Like Num 0 
2023/04/08 16:52:33 comment 5 Like Num 1  after 1000 like 
2023/04/08 16:52:33 comment 5 Like Num 2  after 1000 like 
2023/04/08 16:52:33 comment 5 Like Num 1  after 1001 unlike 
2023/04/08 16:52:33 1001 hasLiked comment 1 : false 
2023/04/08 16:52:33 1000 hasLiked comment 1 : true 
--- PASS: TestLike (0.02s)
PASS</code></pre>
<p>这样，一个最基本的评论点赞模块就做好了，当然，这是将封装好的模块拆出来放在这里的示例代码，在实际应用中应该遵循项目的模块进行封装</p>
<h3>Extra</h3>
<p>在调研其他网站的api设计的时候，发现了一个很有意思的点。CSDN在点赞模块的设计上，点赞与取消点赞的客户端请求<strong>完全一样</strong>！</p>
<p>无论是请求URL、请求方法、表单数据全部都一样！</p>
<p><img decoding="async" src="https://img-1307890592.cos.ap-chengdu.myqcloud.com/typroa/da6cd9b4459447f5fa33a663f10d7d55.png" alt="截图" /></p>
<p><img decoding="async" src="https://img-1307890592.cos.ap-chengdu.myqcloud.com/typroa/433cb8d7b8a7f274465c9338c5cef8b2.png" alt="截图" /></p>
<p>在返回相应的时候，数据才会有所不同</p>
<p><img decoding="async" src="https://img-1307890592.cos.ap-chengdu.myqcloud.com/typroa/3040d9343b4d18eefc6d4ee8c114399a.png" alt="截图" /></p>
<p><img decoding="async" src="https://img-1307890592.cos.ap-chengdu.myqcloud.com/typroa/696d458c963cc150f365f9f745c1d46d.png" alt="截图" /></p>
<p>这是一个很有意思的点，如果前端不传状态的话，后端该如何判断此操作是取消点赞还是点赞操作呢？</p>
<p>既然前端没有标明，那就只能后端先查再改了，但是这无疑是比较吃资源的行为。假设csdn的后台也是用Redis来存储点赞标识，那么也需要查两次Redis，多一次网络通信的开销，我能想到的稍微合理一点的操作逻辑是配合使用lua脚本，在Redis中执行lua脚本进行分支判断进行Compare And Swap的原子操作，代码实例如下</p>
<pre><code class="language-golang">func (c *CommentDomain) ConvertLikeStateCAS(commentId int64, uid int64) (int64, bool, *errs.BError) {
    ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second)
    defer cancel()
    // lua脚本
    script := `
        local key = KEYS[1]
        local userId = ARGV[1]
        local isLiked = redis.call(&quot;GETBIT&quot;, key, userId)
        if isLiked == 1 then
            redis.call(&quot;SETBIT&quot;, key, userId, 0)
            local isLiked = 0
        else
            redis.call(&quot;SETBIT&quot;, key, userId, 1)
            local isLiked = 1
        end
        local likeCount = redis.call(&quot;BITCOUNT&quot;, key)
        return {likeCount, isLiked}
    `
    key := COMMENT_LIKE_REDISPREKEY + strconv.FormatInt(commentId, 10)
    result, err := c.redis.Eval(ctx, script, []string{key}, uid)
    resArr := result.([]interface{})
    likeCount := resArr[0].(int64)
    hassLiked := resArr[1].(int64)
    if err != nil {
        zap.L().Error(&quot;ConvertLikeStateCAS redis Eval ERROR&quot;, zap.Error(err))
        return 0, false, errs.RedisError
    }
    return likeCount, hassLiked == 0, nil
}</code></pre>
<p>简单的测试用例</p>
<pre><code class="language-golang">func TestEval(t *testing.T) {
    // 1000 1001 1002 点赞 1002取消赞
    num, like, bError := commentDomain.ConvertLikeStateCAS(6, 1000)
    log.Println(&quot;after 0 like status: &quot;, like, &quot; likeNum: &quot;, num)

    num, like, bError = commentDomain.ConvertLikeStateCAS(6, 1001)
    log.Println(&quot;after 1 like status: &quot;, like, &quot; likeNum: &quot;, num)

    num, like, bError = commentDomain.ConvertLikeStateCAS(6, 1002)
    log.Println(&quot;after 2 like status: &quot;, like, &quot; likeNum: &quot;, num)

    num, like, bError = commentDomain.ConvertLikeStateCAS(6, 1003)
    log.Println(&quot;after 3 like status: &quot;, like, &quot; likeNum: &quot;, num)

    num, like, bError = commentDomain.ConvertLikeStateCAS(6, 1001)
    log.Println(&quot;after 1 unlike status: &quot;, like, &quot; likeNum: &quot;, num)

    for i := 1000; i &lt; 1004; i++ {
        hasLiked, _ := commentDomain.HasLiked(6, int64(i))
        log.Println(&quot;member&quot;, i, &quot;hasliked &quot;, hasLiked)
    }
    if bError != nil {
        log.Fatal(bError)
    }
    log.Println(&quot;after likeNum: &quot;, num)
}</code></pre>
<p>简单的测试用例</p>
<pre><code class="language-golang">func TestEval(t *testing.T) {
    // 1000 1001 1002 点赞 1002取消赞
    num, like, bError := commentDomain.ConvertLikeStateCAS(6, 1000)
    log.Println(&quot;after 1000 like status: &quot;, like, &quot; likeNum: &quot;, num)
    num, like, bError = commentDomain.ConvertLikeStateCAS(6, 1001)
    log.Println(&quot;after 1001 like status: &quot;, like, &quot; likeNum: &quot;, num)
    num, like, bError = commentDomain.ConvertLikeStateCAS(6, 1002)
    log.Println(&quot;after 1002 like status: &quot;, like, &quot; likeNum: &quot;, num)
    num, like, bError = commentDomain.ConvertLikeStateCAS(6, 1003)
    log.Println(&quot;after 1003 like status: &quot;, like, &quot; likeNum: &quot;, num)
    num, like, bError = commentDomain.ConvertLikeStateCAS(6, 1001)
    log.Println(&quot;after 1001 unlike status: &quot;, like, &quot; likeNum: &quot;, num)

    for i := 1000; i &lt; 1004; i++ {
        hasLiked, _ := commentDomain.HasLiked(6, int64(i))
        log.Println(&quot;member&quot;, i, &quot;hasliked &quot;, hasLiked)
    }
    if bError != nil {
        log.Fatal(bError)
    }
    log.Println(&quot;after likeNum: &quot;, num)
}</code></pre>
<p>输出结果</p>
<pre><code class="language-sh">=== RUN   TestEval
2023/04/08 19:34:29 after 1000 like status:  true  likeNum:  1
2023/04/08 19:34:29 after 1001 like status:  true  likeNum:  2
2023/04/08 19:34:29 after 1002 like status:  true  likeNum:  3
2023/04/08 19:34:29 after 1003 like status:  true  likeNum:  4
2023/04/08 19:34:29 after 1001 unlike status:  false  likeNum:  3
2023/04/08 19:34:29 member 1000 hasliked  true
2023/04/08 19:34:29 member 1001 hasliked  false
2023/04/08 19:34:29 member 1002 hasliked  true
2023/04/08 19:34:29 member 1003 hasliked  true
2023/04/08 19:34:29 after likeNum:  3
--- PASS: TestEval (0.03s)
PASS</code></pre>
<p>这样一来，就可以实现和csdn的点赞一样的效果了（虽然至今我也不知道这样设计的优点在哪里，但还是挺有意思的</p>
<blockquote>
<p>说起csdn，推荐一个油猴脚本 ，可以避免登录才能复制之类的讨厌的限制（虽然改变不了csdn里面的内容多数都是又水又烂的现状，能在国内的掘金知乎找到的东西还是别去看csdn了吧<br />
<a href="https://greasyfork.org/zh-CN/scripts/378351-持续更新-csdn广告完全过滤-人性化脚本优化-不用再登录了-让你体验令人惊喜的崭新csdn"><img src="https://s.w.org/images/core/emoji/15.0.3/72x72/1f525.png" alt="🔥" class="wp-smiley" style="height: 1em; max-height: 1em;" />持续更新<img src="https://s.w.org/images/core/emoji/15.0.3/72x72/1f525.png" alt="🔥" class="wp-smiley" style="height: 1em; max-height: 1em;" /> CSDN广告完全过滤、人性化脚本优化：<img src="https://s.w.org/images/core/emoji/15.0.3/72x72/1f195.png" alt="🆕" class="wp-smiley" style="height: 1em; max-height: 1em;" /> 不用再登录了！让你体验令人惊喜的崭新CSDN (greasyfork.org)</a></p>
</blockquote>
<h2>Tips：</h2>
<p>以上的设计是基于用户id为自增id的基础上而实现的，如果在你的系统中使用的是UUID或者雪花算法生成的ID请勿直接使用bitmap，这会导致bitmap爆掉~<br />
UUID是128位的字符串，且完全没有顺序，并不适合使用bitmap存储<br />
雪花算法生成ID是64位的整型，也不能直接使用bitmap因为 <strong>根 本 存 不 下</strong>！</p>
<pre><code class="language-shell">2^64 bit = 18446744073709551616 bit
= 2305843009213696 MB
= 2251799813685248 GB
= 2202670619951340.672 TB</code></pre>
<p>（不过雪花算法是有序的，所以理论上可以从最小id开始记录）<br />
所以你也看到了bitmap的一个缺陷，它最适合存储的是连续的数据，它所占用的空间也是根据最大的数而定的，如果系统中的数据分布的非常广，可能bitmap就并不是很合适，这种情况下，可能采用hash的方式是更合理的。<br />
此外，Redis中还提供了一种叫做HyperLogLog的数据结构用于基数统计，它要比bitmap还要更“黑科技”，不过相应的，它也是以部分准确性的代价来换取的，可以用在对准确性没有那么高的统计数值上，比如“浏览量”，之后有时间的话也许会写一片文章来介绍一下HLL在实践中的应用吧XD</p>
<p><a rel="nofollow" href="https://www.crazyfay.com/2023/04/08/bitmap%e7%9a%84%e8%ae%be%e8%ae%a1%e5%ae%9e%e7%8e%b0%e5%8f%8a%e5%ae%9e%e6%88%98%e5%ba%94%e7%94%a8/">Bitmap的设计实现及实战应用</a>最先出现在<a rel="nofollow" href="https://www.crazyfay.com">枫阿雨&#039;s blog</a>。</p>
]]></content:encoded>
					
					<wfw:commentRss>https://www.crazyfay.com/2023/04/08/bitmap%e7%9a%84%e8%ae%be%e8%ae%a1%e5%ae%9e%e7%8e%b0%e5%8f%8a%e5%ae%9e%e6%88%98%e5%ba%94%e7%94%a8/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
			</item>
		<item>
		<title>CFC-Golang 开发规范</title>
		<link>https://www.crazyfay.com/2023/02/16/cfc-golang-%e5%bc%80%e5%8f%91%e8%a7%84%e8%8c%83/</link>
					<comments>https://www.crazyfay.com/2023/02/16/cfc-golang-%e5%bc%80%e5%8f%91%e8%a7%84%e8%8c%83/#respond</comments>
		
		<dc:creator><![CDATA[crazyfay]]></dc:creator>
		<pubDate>Thu, 16 Feb 2023 02:36:55 +0000</pubDate>
				<category><![CDATA[实践经验]]></category>
		<category><![CDATA[CFC]]></category>
		<category><![CDATA[Golang]]></category>
		<category><![CDATA[开发规范]]></category>
		<guid isPermaLink="false">http://net.crazyfay.xyz/?p=19</guid>

					<description><![CDATA[<p>CFC-Golang 开发规范 注：此开发规范整合了部分网络上有价值的参考意见和开发实践中的总结 Github [&#8230;]</p>
<p><a rel="nofollow" href="https://www.crazyfay.com/2023/02/16/cfc-golang-%e5%bc%80%e5%8f%91%e8%a7%84%e8%8c%83/">CFC-Golang 开发规范</a>最先出现在<a rel="nofollow" href="https://www.crazyfay.com">枫阿雨&#039;s blog</a>。</p>
]]></description>
										<content:encoded><![CDATA[<h1>CFC-Golang 开发规范</h1>
<blockquote>
<p>注：此开发规范整合了部分网络上有价值的参考意见和开发实践中的总结<br />
Github地址：<a href="https://github.com/Kirov7/cfc-golang-develop-norms">https://github.com/Kirov7/cfc-golang-develop-norms</a></p>
</blockquote>
<h2>为什么需要编程规范？</h2>
<p>编程规范又叫代码规范，是团队之间在程序开发时需要遵守的约定。俗话说，无规矩不成方圆，一个开发团队应该就一种编程规范达成一致。编程规范有很多好处，我们简单说几个最主要的。</p>
<ul>
<li>促进团队合作</li>
</ul>
<p>现代项目大多是由团队完成的，但是如果每个人书写出的代码风格迥异，最后集成代码时很容易杂乱无章、可读性极差。相反，风格统一的代码将大大提高可读性，易于理解，促进团队协作。</p>
<ul>
<li>规避错误</li>
</ul>
<p>每一种语言都有容易犯的错误，Go语言也不例外。但是编码规范可以规避掉像Map并发读写等问题。不仅如此，规范的日志处理、错误处理还能够加快我们查找问题的速度。</p>
<ul>
<li>提升性能</li>
</ul>
<p>优秀的开发者，能够在头脑中想象出不同程序运行的过程和结果，写出高性能的程序非常考验开发者的内功。但每个人的水平都有差异，这一点并不可控。但是如果我们将高性能编程的常见手段归纳整理出来，开发者只需要遵守这些简单的规则，就能够规避性能陷阱、极大提升程序性能。</p>
<ul>
<li>便于维护</li>
</ul>
<p>我们习惯于关注编写代码的成本，但实际上维护代码的成本要高得多。大部分的项目是在前人的基础上完成开发的。我们在开发代码的时候，也会花大量时间阅读之前的代码。符合规范的代码更容易上手维护、更少出现牵一发动全身的耦合现象、也更容易看出业务处理逻辑。</p>
<p>知道了编程规范的好处，那我们应该规范什么内容呢？这其实涉及到我们对好代码的定义。针对这个问题，每个人都能够说个几句。但总体来说，好的代码首先是整洁、一致的，同时它还是高效、健壮和可扩展的。</p>
<p>有一些规范可以是强制的，因为我们可以通过工具和代码review强制要求用户遵守，还有一些规范是建议的，因为它更具有灵活性，很难被约束。在后面的规范中，[强制 xxx]中的“xxx”代表的就是可强制检查的工具。</p>
<h2>整洁、一致</h2>
<p>好代码的第一个要求，是整洁和一致。有一句话是这样说的：</p>
<blockquote>
<p>Any fool can write code that a computer can understand. Good programmers write code that humans can understand.</p>
</blockquote>
<p>它的意思是，任何傻瓜都可以编写计算机可以理解的代码，而优秀的程序员编写的是人类可以理解的代码。</p>
<p>如果我们的代码看起来乱七八糟，就像喝醉的人写的那样，这样不严谨的代码让我们有理由相信，项目的其他各个方面也隐藏着对细节的疏忽，并埋下了重大的隐患。</p>
<p>阅读整洁的代码就像看到精心设计的手表或汽车一样赏心悦目，因为它凝聚了团队的智慧。</p>
<p>阅读整洁的代码也像读到的武侠小说，书中的文字被脑中的图像取代，你看到了角色，听到了声音，体验到了悲怆和幽默。</p>
<p>但是，明白什么是整洁的代码并不意味着你能写出整洁的代码。就好像我们知道如何欣赏一幅画不意味着我们能成为画家。</p>
<p>整洁的代码包括对格式化、命名、函数等细节的密切关注，更需要在项目中具体实践。接下来我们就来看看整洁代码关注的这些细节和最佳的实践。</p>
<h3><strong>格式化</strong></h3>
<p><strong>代码长度</strong></p>
<p>代码应该有足够的垂直密度，能够肉眼一次获得到更多的信息。同时，单个函数、单行、单文件也需要限制长度，保证可阅读性和可维护性。</p>
<p>[强制 lll] 一行内不超过 120 个字符，同时应当避免刻意断行。如果你发现某一行太长了，要么改名，要么调整语义，往往就可以解决问题了。</p>
<p>[强制 funlen] 单个函数的行数不超过 40 行，过长则表示函数功能不专一、定义不明确、程序结构不合理，不易于理解。当函数过长时，可以提取函数以保持正文小且易读。</p>
<p>[强制] 单个文件不超过 2000 行，过长说明定义不明确，程序结构划分不合理，不利于维护。</p>
<p><strong>代码布局</strong></p>
<p>我们先试想一篇写得很好的报纸文章。在顶部，你希望有一个标题，它会告诉你故事的大致内容，并影响你是否要阅读它。文章的第一段会为你提供整个故事的概要、粗略的概念，但是隐藏了所有细节。继续向下阅读，详细信息会逐步增加。</p>
<p>[建议]  Go 文件推荐按以下顺序进行布局。</p>
<ol>
<li>
<p>包注释：对整个模块和功能的完整描述，写在文件头部</p>
</li>
<li>
<p>Package：包名称</p>
</li>
<li>
<p>Imports：引入的包</p>
</li>
<li>
<p>Constants：常量定义</p>
</li>
<li>
<p>Typedefs：类型定义</p>
</li>
<li>
<p>Globals：全局变量定义</p>
</li>
<li>
<p>Functions：函数实现</p>
</li>
</ol>
<p>每个部分之间用一个空行分割。每个部分有多个类型定义或者有多个函数时，也用一个空行分割。示例如下：</p>
<pre><code class="language-go">/*
注释
*/
package http

import (
 &quot;fmt&quot;
 &quot;time&quot;
)

const (
 VERSION = &quot;1.0.0&quot;
)
type Request struct{
}

var msg = &quot;HTTP success&quot;

func foo() {
 //...
}</code></pre>
<p>[强制 goimports] 当 import 多个包时，应该对包进行分组。同一组的包之间不需要有空行，不同组之间的包需要一个空行。标准库的包应该放在第一组。</p>
<p><strong>空格与缩进</strong></p>
<p>为了让阅读代码时视线畅通，自上而下思路不被打断，我们需要使用一些空格和缩进。</p>
<p>空格是为了分离关注点，将不同的组件分开。缩进是为了处理错误和边缘情况，与正常的代码分隔开。</p>
<p>较常用的有下面这些规范：</p>
<p>[强制 gofmt] 注释和声明应该对齐。示例如下：</p>
<pre><code class="language-go">type T struct {
    name    string // name of the object
    value   int    // its value
}</code></pre>
<p>[强制 gofmt] 小括号()、中括号[]、大括号{} 内侧都不加空格。</p>
<p>[强制 gofmt] 逗号、冒号（slice中冒号除外）前都不加空格，后面加 1 个空格。</p>
<p>[强制 gofmt] 所有二元运算符前后各加一个空格，作为函数参数时除外。例如<code>b := 1 + 2</code>。[强制 gofmt] 使用 Tab 而不是空格进行缩进。</p>
<p>[强制 nlreturn] return前方需要加一个空行，让代码逻辑更清晰。</p>
<p>[强制 gofmt] 判断语句、for语句需要缩进1个 Tab，并且右大括号<code>}</code>与对应的 if 关键字垂直对齐。例如：</p>
<pre><code class="language-go">if xxx {

} else {

}</code></pre>
<p>[强制 goimports] 当 import 多个包时，应该对包进行分组。同一组的包之间不需要有空行，不同组之间的包需要一个空行。标准库的包应该放在第一组。这同样适用于常量、变量和类型声明：</p>
<pre><code class="language-go">import (
    &quot;fmt&quot;
    &quot;hash/adler32&quot;
    &quot;os&quot;

    &quot;appengine/foo&quot;
    &quot;appengine/user&quot;
    &quot;github.com/foo/bar&quot;
    &quot;rsc.io/goversion/version&quot;
)</code></pre>
<p>[推荐] 避免 else 语句中处理错误返回，避免正常的逻辑位于缩进中。如下代码实例，else中进行错误处理，代码逻辑阅读起来比较费劲。</p>
<pre><code class="language-go">if something.OK() {
        something.Lock()
        defer something.Unlock()
        err := something.Do()
        if err == nil {
                stop := StartTimer()
                defer stop()
                log.Println(&quot;working...&quot;)
                doWork(something)
                &lt;-something.Done() // wait for it
                log.Println(&quot;finished&quot;)
                return nil
        } else {
                return err
        }
} else {
        return errors.New(&quot;something not ok&quot;)
}</code></pre>
<p>如果把上面的代码修改成下面这样会更加清晰：</p>
<pre><code class="language-go">if !something.OK() {
    return errors.New(&quot;something not ok&quot;)
}
something.Lock()
defer something.Unlock()
err := something.Do()
if err != nil {
    return err
}
stop := StartTimer()
defer stop()
log.Println(&quot;working...&quot;)
doWork(something)
&lt;-something.Done() // wait for it
log.Println(&quot;finished&quot;)
return nil</code></pre>
<p>[推荐] 函数内不同的业务逻辑处理建议用单个空行加以分割。</p>
<p>[推荐] 注释之前的空行通常有助于提高可读性——新注释的引入表明新思想的开始。</p>
<h3><strong>命名</strong></h3>
<blockquote>
<p>Good naming is like a good joke. If you have to explain it, it’s not funny.<br />
———Dave Cheney</p>
</blockquote>
<p>一个好的名字应该满足几个要素：</p>
<ul>
<li>短，容易拼写；</li>
<li>保持一致性；</li>
<li>意思准确，容易理解，没有虚假和无意义的信息。</li>
</ul>
<p>例如，像下面这样的命名就是让人迷惑的：</p>
<pre><code class="language-go">int d; // elapsed time in days</code></pre>
<p>[强制 revive] Go中的命名统一使用驼峰式、不要加下划线。</p>
<p>[强制 revive] 缩写的专有名词应该大写，例如： ServeHTTP、IDProcessor。</p>
<p>[强制] 区分变量名应该用有意义的名字，而不是使用阿拉伯数字：a1, a2, .. aN。</p>
<p>[强制] 不要在变量名称中包含你的类型名称。</p>
<p>[建议]变量的作用域越大，名字应该越长。</p>
<p>现代 IDE 已经让更改名称变得更容易了，巧妙地使用IDE的功能，能够级联地同时修改多处命名。</p>
<p><strong>包名</strong></p>
<p>包名应该简短而清晰。</p>
<p>[强制] 使用简短的小写字母，不需要下划线或混合大写字母。</p>
<p>[建议]  合理使用缩写，例如：</p>
<pre><code>strconv（字符串转换）
syscall（系统调用）
fmt（格式化的 I/O）</code></pre>
<p>[强制] 避免无意义的包名，例如<code>util</code>,<code>common,base</code>等。</p>
<p><strong>接口命名</strong></p>
<p>[建议]单方法接口由方法名称加上 -er 后缀或类似修饰来命名。例如：<code>Reader</code>, <code>Writer</code>, <code>Formatter</code>, <code>CloseNotifier</code> ，当一个接口包含多个方法时，请选择一个能够准确描述其用途的名称（例如：net.Conn、http.ResponseWriter、io.ReadWriter）。</p>
<p><strong>本地变量命名</strong></p>
<p>[建议]尽可能地短。在这里，i 指代 index，r 指代 reader，b 指代 buffer。</p>
<p>例如，下面这段代码就可以做一个简化：</p>
<pre><code class="language-go">for index := 0; index &lt; len(s); index++ {
    //
}</code></pre>
<p>可以替换为：</p>
<pre><code class="language-go">for i := 0; i &lt; len(s); i++ {
    //
}</code></pre>
<p><strong>函数参数命名</strong></p>
<p>[建议]如果函数参数的类型已经能够看出参数的含义，那么函数参数的命名应该尽量简短：</p>
<pre><code class="language-go">func AfterFunc(d Duration, f func()) *Timer
func Escape(w io.Writer, s []byte)</code></pre>
<p>[建议]如果函数参数的类型不能表达参数的含义，那么函数参数的命名应该尽量准确：</p>
<pre><code class="language-go">func Unix(sec, nsec int64) Time
func HasPrefix(s, prefix []byte) bool</code></pre>
<p><strong>函数返回值命名</strong></p>
<p>[建议] 对于公开的函数，返回值具有文档意义，应该准确表达含义，如下所示：</p>
<pre><code class="language-go">func Copy(dst Writer, src Reader) (written int64, err error)

func ScanBytes(data []byte, atEOF bool) (advance int, token []byte, err error)</code></pre>
<p><strong>可导出的变量名</strong></p>
<p>[建议] 由于使用可导出的变量时会带上它所在的包名，因此，不需要对变量重复命名。例如bytes包中的ByteBuffer替换为Buffer，这样在使用时就是bytes.Buffer，显得更简洁。类似的还有把strings.StringReader修改为strings.Reader，把<strong>errors.NewError 修改为errors.New。</strong></p>
<p><strong>Error值命名</strong></p>
<p>[建议] 错误类型应该以Error结尾。</p>
<p>[建议] Error变量名应该以Err开头。</p>
<pre><code class="language-go">type ExitError struct {
    ...
}
var ErrFormat = errors.New(&quot;image: unknown format&quot;)</code></pre>
<h3><strong>函数</strong></h3>
<p>[强制 cyclop] 圈复杂度（Cyclomatic complexity）&lt;10。</p>
<p>[强制 gochecknoinits] 避免使用init函数。</p>
<p>[强制 revive] Context 应该作为函数的第一个参数。</p>
<p>[强制] 正常情况下禁用unsafe。</p>
<p>[强制] 禁止return裸返回，如下例中第一个return：</p>
<pre><code class="language-go">func (f *Filter) Open(name string) (file File, err error) {
    for _, c := range f.chain {
        file, err = c.Open(name)
        if err != nil {
            return
        }
    }
    return f.source.Open(name)
}</code></pre>
<p>[强制] 不要在循环里面使用defer，除非你真的确定defer的执行流程。</p>
<p>[强制] 对于通过:=进行变量赋值的场景，禁止出现仅部分变量初始化的情况。例如在下面这个例子中，f函数返回的res是初始化的变量，但是函数返回的err其实复用了之前的err：</p>
<pre><code class="language-go">var err error
res,err := f()</code></pre>
<p>[建议] 函数返回值大于 3 个时，建议通过 struct 进行包装。</p>
<p>[建议] 函数参数不建议超过 3 个，大于 3 个时建议通过 struct 进行包装。</p>
<h3>控制结构</h3>
<p>[强制] 禁止使用goto。</p>
<p>[强制 gosimple] 当一个表达式为 bool 类型时，应该使用 expr 或 !expr 判断，禁止使用 == 或 != 与 true / false 比较。</p>
<p>[强制 nestif] if 嵌套深度不大于5。</p>
<h3><strong>方法</strong></h3>
<p>[强制 revive] receiver 的命名要保持一致，如果你在一个方法中将接收器命名为 &quot;c&quot;，那么在其他方法中不要把它命名为 &quot;cl&quot;。</p>
<p>[强制] receiver 的名字要尽量简短并有意义，禁止使用 this、self 等。</p>
<pre><code class="language-go">func (c Client) done() error {
 // ...
}
func (cl Client) call() error {
 // ...
}</code></pre>
<h3>注释</h3>
<p>Go提供C风格的注释。有/**/ 的块注释和 // 的单行注释两种注释风格。注释主要有下面几个用处。</p>
<ol>
<li>注释不仅仅可以提供具体的逻辑细节，还可以提供代码背后的意图和决策。</li>
<li>帮助澄清一些晦涩的参数或返回值的含义。一般来说，我们会尽量找到一种方法让参数或返回值的名字本身就是清晰的。但是当它是标准库的一部分时，或者在你无法更改的第三方库中，一个清晰的注释会非常有用。</li>
<li>强调某一个重要的功能。例如，提醒开发者修改了这一处代码必须连带修改另一处代码。</li>
</ol>
<p>总之，好的注释给我们讲解了what、how、why，方便后续的代码维护。</p>
<p>[强制] 无用注释直接删除，无用的代码不应该注释而应该直接删除。即使日后需要，我们也可以通过Git快速找到。</p>
<p>[强制] 使用行注释而不是尾注释，注释一律写在所描述内容的上一行。</p>
<p>[强制] 统一使用中文注释，中英文字符之间严格使用空格分隔。</p>
<pre><code class="language-go">// 从 Redis 中批量读取属性，对于没有读取到的 id ， 记录到一个数组里面，准备从 DB 中读取</code></pre>
<p>[强制] 注释不需要额外的格式，例如星号横幅。</p>
<p>[强制] 包、函数、方法和类型的注释说明都是一个完整的句子，以被描述的对象为主语开头。Go源码中都是这样的。</p>
<p>示例如下：</p>
<pre><code class="language-go">// queueForIdleConn queues w to receive the next idle connection for w.cm.
// As an optimization hint to the caller, queueForIdleConn reports whether
// it successfully delivered an already-idle connection.
func (t *Transport) queueForIdleConn(w *wantConn) (delivered bool)</code></pre>
<p>[强制] Go语言提供了<a href="https://tip.golang.org/doc/comment">文档注释工具go doc</a>，可以生成注释和导出函数的文档。文档注释的写法可以参考文稿中的链接。</p>
<p>[强制 godot] 注释最后应该以句号结尾。</p>
<p>[建议] 当某个部分等待完成时，可用 <code>TODO:</code> 开头的注释来提醒维护人员。</p>
<p>[建议] 大部分情况下使用行注释。块注释主要用在包的注释上，不过块注释在表达式中或禁用大量代码时很有用。</p>
<p>[建议] 当某个部分存在已知问题需要修复或改进时，可用 <code>FIXME:</code> 开头的注释来提醒维护人员。</p>
<p>[建议] 需要特别说明某个问题时，可用 <code>NOTE:</code> 开头的注释。</p>
<h3>结构体</h3>
<p>[强制] 不要将 Context 成员添加到 Struct 类型中。</p>
<h3>Commit 规范</h3>
<p><strong>commit message格式</strong></p>
<pre><code class="language-text">&lt;type&gt;(&lt;scope&gt;): &lt;subject&gt;</code></pre>
<p>[强制] <strong>type(必须)</strong></p>
<p>用于说明git commit的类别，只允许使用下面的标识。</p>
<p>feat：新功能（feature）。</p>
<p>fix/to：修复bug，可以是QA发现的BUG，也可以是研发自己发现的BUG。</p>
<ul>
<li>fix：产生diff并自动修复此问题。适合于一次提交直接修复问题</li>
<li>to：只产生diff不自动修复此问题。适合于多次提交。最终修复问题提交时使用fix</li>
</ul>
<p>docs：文档（documentation）。</p>
<p>style：格式（不影响代码运行的变动）。</p>
<p>refactor：重构（即不是新增功能，也不是修改bug的代码变动）。</p>
<p>perf：优化相关，比如提升性能、体验。</p>
<p>test：增加测试。</p>
<p>chore：构建过程或辅助工具的变动。</p>
<p>revert：回滚到上一个版本。</p>
<p>merge：代码合并。</p>
<p>sync：同步主线或分支的Bug。</p>
<p>[建议] <strong>scope(可选)</strong></p>
<p>scope用于说明 commit 影响的范围，比如数据层、控制层、视图层等等，视项目不同而不同。</p>
<p>例如在Angular，可以是location，browser，compile，compile，rootScope， ngHref，ngClick，ngView等。如果你的修改影响了不止一个scope，你可以使用*代替。</p>
<p>[强制] <strong>subject(必须)</strong></p>
<p>subject是commit目的的简短描述，不超过50个字符。</p>
<p>建议使用中文（感觉中国人用中文描述问题能更清楚一些）。</p>
<ul>
<li>结尾不加句号或其他标点符号。</li>
<li>根据以上规范git commit message将是如下的格式：</li>
</ul>
<pre><code class="language-text">fix(DAO):用户查询缺少username属性 
feat(Controller):用户查询接口开发</code></pre>
<blockquote>
<p>这样规范 git commit 有以下优点</p>
</blockquote>
<ul>
<li>便于开发者对提交历史进行追溯，了解发生了什么情况。</li>
<li>一旦约束了commit message，意味着我们将慎重的进行每一次提交，不能再一股脑的把各种各样的改动都放在一个git commit里面，这样一来整个代码改动的历史也将更加清晰。</li>
<li>格式化的commit message才可以用于自动化输出Change log。</li>
</ul>
<h2>高效</h2>
<p>[强制] Map在初始化时需要指定长度<code>make(map[T1]T2, hint)</code>。</p>
<p>[强制] Slice在初始化时需要指定长度和容量<code>make([]T, length, capacity)</code>。</p>
<p>我们来看下下面这段程序，它的目的是往切片中循环添加元素。</p>
<pre><code class="language-go">func createSlice(n int) (slice []string) {
   for i := 0; i &lt; n; i++ {
      slice = append(slice, &quot;I&quot;, &quot;love&quot;, &quot;go&quot;)
   }
   return slice
}</code></pre>
<p>从功能上来看，这段代码没有问题。但是，这种写法忽略了一个事实，如下图所示，往切片中添加数据时，切片会自动扩容，Go运行时会创建新的内存空间并执行拷贝。</p>
<p><img decoding="async" src="https://img-1307890592.cos.ap-chengdu.myqcloud.com/typroa/image-20221124144357089.png" alt="image-20221124144357089" /></p>
<p>自动扩容显然是有成本的。在循环操作中执行这样的代码会放大性能损失，减慢程序的运行速度。性能损失的对比可参考<a href="https://github.com/uber-go/guide/blob/master/style.md#prefer-specifying-container-capacity">这篇文章</a>。我们可以改写一下上面这段程序，在初始化时指定合适的切片容量：</p>
<pre><code class="language-go">func createSlice(n int) []string {
   slice := make([]string, 0, n*3)
   for i := 0; i &lt; n; i++ {
      slice = append(slice, &quot;I&quot;, &quot;love&quot;, &quot;go&quot;)
   }
   return slice
}</code></pre>
<p>这段代码在一开始就指定了需要的容量，最大程度避免了内存的浪费。同时，运行时不需要再执行自动扩容操作，加速了程序的运行。</p>
<p>[强制]  time.After()在某些情况下会发生泄露，替换为使用Timer。</p>
<p>[强制] 数字与字符串转换时，<a href="https://gist.github.com/evalphobia/caee1602969a640a4530">使用strconv，而不是fmt</a>。</p>
<p>[强制] 读写磁盘时，使用<a href="https://www.instana.com/blog/practical-golang-benchmarks/#file-i-o">读写buffer</a>。</p>
<p>[建议] 谨慎使用Slice的截断操作和append操作，除非你知道下面的代码输出什么：</p>
<pre><code class="language-go">x := []int{1, 2, 3, 4}
y := x[:2]
fmt.Println(cap(x), cap(y))
y = append(y, 30)
fmt.Println(&quot;x:&quot;, x)
fmt.Println(&quot;y:&quot;, y)</code></pre>
<p>[建议] 任何书写的协程，都需要明确协程什么时候退出。</p>
<p>[建议] 热点代码中，内存分配复用内存可以使用 sync.Pool <a href="https://www.instana.com/blog/practical-golang-benchmarks/#object-creation">提速</a>。</p>
<p>[建议] 将频繁的字符串拼接操作（+=），替换为<strong>StringBuffer 或 StringBuilder。</strong></p>
<p>[建议] 使用正则表达式重复匹配时，利用Compile提前编译<a href="https://www.instana.com/blog/practical-golang-benchmarks/#regular-expressions">提速</a>。</p>
<p>[建议]  当程序严重依赖Map时，Map的Key使用int而不是string将<a href="https://www.instana.com/blog/practical-golang-benchmarks/#map-access">提速</a>。</p>
<p>[建议]  多读少写的场景，使用读写锁而不是写锁将提速。</p>
<h2>健壮性</h2>
<p>[强制] 除非出现不可恢复的程序错误，否则不要使用 panic 来处理常规错误，使用 error 和多返回值。</p>
<p>[强制] 永远只在 main 函数和 init 函数中调用 log.Fatal() 方法。</p>
<p>[强制] 永远先关闭 写channel 再关闭 读channel，否则对已关闭 写channel 进行写入时会引发 panic 。</p>
<p>[强制 <a href="https://revive.run/r#error-strings">revive</a>] 错误信息不应该首字母大写（除专有名词和缩写词外），也不应该以标点符号结束。因为错误信息通常在其他上下文中被打印。</p>
<p>[强制 <a href="https://golangci-lint.run/usage/linters/#errcheck">errcheck</a>] 不要使用 _ 变量来丢弃 error。如果函数返回 error，应该强制检查。</p>
<p>[强制] 在 Release模式 使用 context 的时候需要设定超时时间。</p>
<p>[建议] 在处理错误时，如果我们逐层返回相同的错误，那么在最后日志打印时，我们并不知道代码中间的执行路径。例如找不到文件时打印的<code>No such file or directory</code>，这会减慢我们排查问题的速度。因此，在中间处理err时，需要使用fmt.Errorf 或<a href="https://godoc.org/github.com/pkg/errors">第三方包</a>给错误添加额外的上下文信息。像下面这个例子，在fmt.Errorf中，除了实际报错的信息，还加上了授权错误信息<code>authenticate failed</code> ：</p>
<pre><code class="language-go">func AuthenticateRequest(r *Request) error {
        err := authenticate(r.User)
        if err != nil {
                return fmt.Errorf(&quot;authenticate failed: %v&quot;, err)
        }
        return nil
}</code></pre>
<p>当有多个错误需要处理时，可以考虑将fmt.Errorf放入defer中：</p>
<pre><code class="language-go">func DoSomeThings(val1 int, val2 string) (_ string, err error) {
    defer func() {
        if err != nil {
            err = fmt.Errorf(&quot;in DoSomeThings: %w&quot;, err)
        }
    }()
    val3, err := doThing1(val1)
    if err != nil {
        return &quot;&quot;, err
    }
    val4, err := doThing2(val2)
    if err != nil {
        return &quot;&quot;, err
    }
    return doThing3(val3, val4)
}</code></pre>
<p>[强制] 利用recover捕获panic时，需要由defer函数直接调用。</p>
<p>例如，下面例子中的panic是可以被捕获的：</p>
<pre><code class="language-go">package main

import &quot;fmt&quot;

func printRecover() {
    r := recover()
    fmt.Println(&quot;Recovered:&quot;, r)
}

func main() {
    defer printRecover()

    panic(&quot;OMG!&quot;)
}</code></pre>
<p>但是下面这个例子中的panic却不能被捕获：</p>
<pre><code class="language-go">package main

import &quot;fmt&quot;

func printRecover() {
    r := recover()
    fmt.Println(&quot;Recovered:&quot;, r)
}

func main() {
    defer func() {
        printRecover()
    }()

    panic(&quot;OMG!&quot;)
}</code></pre>
<p>[强制] 不用重复使用recover，只需要在每一个协程的最上层函数拦截即可。recover只能够捕获当前协程，而不能跨协程捕获panic，下例中的panic就是无法被捕获的。</p>
<pre><code class="language-go">package main

import &quot;fmt&quot;

func printRecover() {
    r := recover()
    fmt.Println(&quot;Recovered:&quot;, r)
}

func main() {
    defer printRecover()
    go func() {
        panic(&quot;OMG!&quot;)
    }()
    // ...
}</code></pre>
<p>[强制] 有些特殊的错误是recover不住的，例如Map的并发读写冲突。这种错误可以通过race工具来检查。</p>
<h2>扩展性</h2>
<p>[建议] 利用接口实现扩展性。接口特别适用于访问外部组件的情况，例如访问数据库、访问下游服务。另外，接口可以方便我们进行功能测试。关于接口的最佳实践，需要单独论述。</p>
<p>[建议] 使用功能选项模式(options 模式)对一些公共API的构造函数进行扩展，大量第三方库例如gomicro、zap等都使用了这种策略。</p>
<pre><code class="language-go">db.Open(addr, db.DefaultCache, zap.NewNop())
可以替换为=&gt;
db.Open(
    addr,
    db.WithCache(false),
    db.WithLogger(log),
)</code></pre>
<h2>内部实践</h2>
<h3>Gin</h3>
<h4>项目启动</h4>
<p>[强制] 在大型的项目开发中，采用优雅服务启停设计，采用 httpServer.ListenAndServer() 方法进行web服务的启动，而不是 Gin 框架的方法，web服务需额外启动一个 goroutine 来运行，通过信号监听的方式控制结束程序。</p>
<pre><code class="language-golang">func main() {
    r := gin.Default()
    srv := &amp;http.Server{
        Addr:    &quot;:80&quot;,
        Handler: r,
    }

    // 优雅的启停
    go func() {
        log.Printf(&quot;web server running in %s \n&quot;, srv.Addr)
        if err := srv.ListenAndServe(); err != nil &amp;&amp; err != http.ErrServerClosed {
            log.Fatalln(err)
        }
    }()

    quit := make(chan os.Signal)
    // SIGINT 用户发送 INTR 字符(Ctrl + C)触发 kill -2
    // SIGTERM 结束程序 (可以被捕获、阻塞或忽略)
    signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM)
    &lt;-quit
    log.Println(&quot;Shutting Down project Web server...&quot;)

    ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second)
    defer cancel()
    if err := srv.Shutdown(ctx); err != nil {
        log.Fatalln(&quot;Web server shutdown, cause by : &quot;, err)
    }
    select {
    case &lt;-ctx.Done():
        log.Println(&quot;wait timeout...&quot;)
    }
    log.Println(&quot;Web Server stop success...&quot;)
}</code></pre>
<blockquote>
<p>以上代码可进一步封装，以保证 main 的代码结构清晰</p>
</blockquote>
<h4>返回值格式</h4>
<p>[强制] json格式的返回值，code部分统一采用 net/http 包中的 http.StatusOK，obj 则为业务对象的统一格式封装。</p>
<pre><code class="language-golang">ctx.JSON(http.StatusOK, result.Success(list))
ctx.JSON(http.StatusOK, result.Fail(http.StatusBadRequest, &quot;参数错误&quot;))</code></pre>
<p>[强制] 业务对象中的统一字段为：code、msg、data，其中code为错误代码，由具体的服务实现方定义，需要保证全局唯一，如果没有错误则固定为 200 ，msg为错误信息，由具体的服务实现方定义，需要简洁的描述错误的原因，错误可统一定义为全局变量，当在api层产生参数错误、数据校验失败等请求值错误，则使用 http.StatusBadRequest 作为错误码， data 则为前端所需要的业务对象，推荐使用在api服务中封装返回值结构体实例。</p>
<p>对应结构体实例：</p>
<pre><code class="language-golang">type BusinessCode int
type Result struct {
    Code BusinessCode `json:&quot;code&quot;`
    Msg  string       `json:&quot;msg&quot;`
    Data any          `json:&quot;data&quot;`
}</code></pre>
<p>[强制] 在给前端返回 json 后必须 return 。</p>
<h3>grpc</h3>
<h4>proto文件与命名规范</h4>
<p>[强制] proto文件中的 service、rpc Function、message 以大驼峰的方式命名。</p>
<p>[推荐] 尽可能的将 proto 文件中 service 命名为 xxxService 、 RPC 方法的入参命名为 XxxRequest，出参命名为 XxxResponse。</p>
<p>[强制] 将引用的生成 rpc 包重命名为 xxxRpc 以避免module中的本地模块产生冲突。</p>
<pre><code class="language-go">import(
    departmentRpc &quot;github.com/Kirov7/project-grpc/project/department&quot;   
)</code></pre>
<p>[推荐] 编译 proto 文件时推荐不要直接生成到会被其他模块引用的包下，建议生成到指定目录后手动迁移文件。</p>
<h4>注册grpc服务</h4>
<p>[建议] 优雅的启动 grpcServer 示例，其中 RegisterGrpc() 返回的 grpc.Server 可供上层调用 grpcSever.Stop() 等，可对 grpcServer 生命周期进行管控。</p>
<pre><code class="language-golang">type gRPCConfig struct {
    Addr         string
    RegisterFunc func(server *grpc.Server)
}

func RegisterGrpc() *grpc.Server {
    c := gRPCConfig{
        Addr: config.AppConf.GrpcConfig.Addr,
        RegisterFunc: func(g *grpc.Server) {
            login.RegisterLoginServiceServer(g, loginServiceV1.NewLoginService())
      },
    }
    //cacheInterceptor := interceptor.NewCacheInterceptor()
    //s := grpc.NewServer(cacheInterceptor.CacheInterceptor())
    s := grpc.NewServer()
    c.RegisterFunc(s)
    listen, err := net.Listen(&quot;tcp&quot;, c.Addr)
    if err != nil {
        log.Printf(&quot;listen port %s fail\n&quot;, c.Addr)
    }
    go func() {
        log.Printf(&quot;grpc server started as %s \n&quot;, c.Addr)
        err = s.Serve(listen)
        if err != nil {
            log.Printf(&quot;server started error: %s\n&quot;, err)
            return
        }
    }()
    return s
}</code></pre>
<h3>项目结构核心部分示例</h3>
<p>此示例结合了部分DDD领域驱动设计的思想，但并非完全按照DDD的模式进行组织，旨在优化项目组织结构的同时，尽量避免过于复杂抽象的概念。</p>
<h4>main.go</h4>
<p>进行各类初始化操作，如初始化http服务、路由注册、初始化grpc客户端、grpc服务注册等。</p>
<h4>config 包</h4>
<p>内有 config.go 和 config.yaml 文件，config.go 中拥有包含配置的全局变量，<strong>推荐使用 viper 进行配置读取</strong>。</p>
<h4>internal 包</h4>
<p>internal包 是 golang 中特殊的一个包，它对外部module不可见，可用来存放数据操作等敏感内容。</p>
<p>可包含domain、repository、dao、rpc、data等包</p>
<ul>
<li>
<p>domain：领域服务，负责表达业务概念，业务状态信息以及业务规则，通过调用 repository 、rpc 或其他 domain 进行数据操作与数据整合，实现完整的某一领域模块的业务逻辑。在进行单元测试时可以绕过 service 直接对 domain 进行细粒度的测试，简化测试流程。</p>
</li>
<li>
<p>repository：仓库，负责封装数据的查询、创建、更新、删除等逻辑的接口，屏蔽底层实现，供使用者调用</p>
</li>
<li>
<p>dao：数据操作层，实现 repository 的接口，封装对MySQL、Redis等数据库操作的具体实现的。</p>
</li>
<li>
<p>rpc：rpc客户端，对其他rpc服务进行远程调用的客户端。</p>
</li>
<li>
<p>data：数据表的直接映射，并包含对于数据传输模型的转换。</p>
</li>
</ul>
<h4>pkg 包</h4>
<p>pkg包 包含了服务具体实现与其所依赖的预定义的常量与全局变量。</p>
<p>可包含model、service等包</p>
<ul>
<li>model：预定义的常量与全局变量，便于在业务中重用，简化操作逻辑。可包含预定于的 rediskey、业务所需的枚举值、预定义的错误等。</li>
<li>service：业务层，接受客户端传输的数据，对请求数据进行校验与转换，通过调用 domain 层进行业务操作，整合调用的结果值，返回给客户端。</li>
</ul>
<h2>工具</h2>
<p>要人工来保证团队成员遵守了上述的编程规范并不是一件容易的事情。因此，我们有许多静态的和动态的代码分析工具帮助团队识别代码规范的错误，甚至可以发现一些代码的bug。</p>
<h3>golangci-lint</h3>
<p>golangci-lint 是当前大多数公司采用的静态代码分析工具，词语Linter 指的是一种分析源代码以此标记编程错误、代码缺陷、风格错误的工具。</p>
<p>而golangci-lint是集合多种Linter的工具。要查看支持的 Linter 列表以及启用/禁用了哪些Linter，可以使用下面的命令：</p>
<pre><code>golangci-lint help linters</code></pre>
<p>Go语言定义了实现Linter的API，它还提供了golint工具，用于集成了几种常见的Linter。在<a href="https://cs.opensource.google/go/x/tools/+/refs/tags/v0.1.11:go/analysis/passes/unreachable/unreachable.go">源码</a>中，我们可以查看怎么在标准库中实现典型的Linter。</p>
<p>Linter的实现原理是静态扫描代码的AST（抽象语法树），Linter的标准化意味着我们可以灵活实现自己的Linters。不过golangci-lint里面其实已经集成了包括golint在内的总多Linter，并且有灵活的配置能力。所以在自己写Linter之前，建议先了解golangci-lint现有的能力。</p>
<p>在大型项目中刚开始使用golang-lint会出现大量的错误，这种情况下我们只希望扫描增量的代码。如下所示，可以通过在<a href="https://golangci-lint.run/usage/configuration/">golangci-lint配置文件</a>中调整new-from-rev参数，配置以当前基准分支为基础实现增量扫描</p>
<pre><code class="language-yaml">linters:
 enable-all: true
issues:
 new-from-rev: master</code></pre>
<h3>Pre-Commit</h3>
<p>在代码通过Git Commit提交到代码仓库之前，git 提供了一种pre-commit的hook能力，用于执行一些前置脚本。在脚本中加入检查的代码，就可以在本地拦截住一些不符合规范的代码，避免频繁触发CI或者浪费时间。pre-commit的配置和使用方法，可以参考<a href="https://github.com/pingcap/tidb/blob/master/hooks/pre-commit">TiDB</a>。</p>
<h3>并发检测 race</h3>
<p>Go 1.1 提供了强大的检查工具race来排查数据争用问题。race 可以用在多个Go指令中，一旦检测器在程序中找到数据争用，就会打印报告。这份报告包含发生race冲突的协程栈，以及此时正在运行的协程栈。可以在编译时和运行时执行race，方法如下：</p>
<pre><code class="language-shell">$ go test -race mypkg
$ go run -race mysrc.go
$ go build -race mycmd
$ go install -race mypkg</code></pre>
<p>在下面这个例子中， 运行中加入race检查后直接报错。从报错后输出的栈帧信息中，我们能看出具体发生并发冲突的位置。</p>
<pre><code class="language-shell">» go run -race 2_race.go
==================
WARNING: DATA RACE
Read at 0x00000115c1f8 by goroutine 7:
    main.add()
        bookcode/concurrence_control/2_race.go:5 +0x3a
Previous write at 0x00000115c1f8 by goroutine 6:
    main.add()
        bookcode/concurrence_control/2_race.go:5 +0x56</code></pre>
<p>第四行Read at 表明读取发生在2_race.go 文件的第5行，而第七行Previous write 表明前一个写入也发生在2_race.go 文件的第5行。这样我们就可以非常快速地定位数据争用问题了。</p>
<p>竞争检测的成本因程序而异。对于典型的程序，内存使用量可能增加 5~10 倍，执行时间会增加2~20倍。同时，竞争检测器会为当前每个defer和recover语句额外分配8字节。在Goroutine退出前，这些额外分配的字节不会被回收。这意味着，如果有一个长期运行的Goroutine，而且定期有defer 和recover调用，那么程序内存的使用量可能无限增长。（这些内存分配不会显示到 runtime.ReadMemStats或runtime / pprof 的输出。）</p>
<h3>覆盖率</h3>
<p>一般我们会使用代码覆盖率来判断代码书写的质量，识别无效代码。go tool cover 是go语言提供的识别代码覆盖率的工具。</p>
<p><a rel="nofollow" href="https://www.crazyfay.com/2023/02/16/cfc-golang-%e5%bc%80%e5%8f%91%e8%a7%84%e8%8c%83/">CFC-Golang 开发规范</a>最先出现在<a rel="nofollow" href="https://www.crazyfay.com">枫阿雨&#039;s blog</a>。</p>
]]></content:encoded>
					
					<wfw:commentRss>https://www.crazyfay.com/2023/02/16/cfc-golang-%e5%bc%80%e5%8f%91%e8%a7%84%e8%8c%83/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
			</item>
		<item>
		<title>Window-TinyLFU缓存实现</title>
		<link>https://www.crazyfay.com/2023/02/02/window-tinylfu%e7%bc%93%e5%ad%98%e5%ae%9e%e7%8e%b0/</link>
					<comments>https://www.crazyfay.com/2023/02/02/window-tinylfu%e7%bc%93%e5%ad%98%e5%ae%9e%e7%8e%b0/#respond</comments>
		
		<dc:creator><![CDATA[crazyfay]]></dc:creator>
		<pubDate>Thu, 02 Feb 2023 06:12:50 +0000</pubDate>
				<category><![CDATA[代码实战]]></category>
		<category><![CDATA[cache]]></category>
		<category><![CDATA[Golang]]></category>
		<guid isPermaLink="false">http://net.crazyfay.xyz/?p=222</guid>

					<description><![CDATA[<p>在我的一个基于LSM-Tree结构的存储引擎项目中，由于LSM-Tree结构的查询操作高度依赖缓存，所以我需要 [&#8230;]</p>
<p><a rel="nofollow" href="https://www.crazyfay.com/2023/02/02/window-tinylfu%e7%bc%93%e5%ad%98%e5%ae%9e%e7%8e%b0/">Window-TinyLFU缓存实现</a>最先出现在<a rel="nofollow" href="https://www.crazyfay.com">枫阿雨&#039;s blog</a>。</p>
]]></description>
										<content:encoded><![CDATA[<p>在我的一个基于LSM-Tree结构的存储引擎项目中，由于LSM-Tree结构的查询操作高度依赖缓存，所以我需要一个高性能的本地缓存模块来提高查询效率，抱着学习的目的，我们来 <del>重复造轮子</del> 亲自实现一个拥有良好性能的缓存模块。</p>
<p>所谓缓存，本质上就是 <code>映射+淘汰策略</code> ，如何在有限的空间存储最”有用“的数据，也就是如何设定淘汰策略往往是我们所需要关注的，在Redis中，我们很多时候都是用的TTL（Time To Live），以固定生存时间的角度控制数据的淘汰，但是对于一个存储引擎来说，显然不是一个良好的方案，而除了固定生存时间的角度就是访问频率的角度，基于访问的角度则指向了两种算法： <code>LRU</code> 和 <code>LFU</code> 。</p>
<h2>LRU 和 LFU 算法</h2>
<h3>LRU 算法</h3>
<p>LRU （Least Recently Used） 即最近最久未使用算法</p>
<p>LRU 算法的宗旨是，在定量空间的存储数据时，当新增数据后超出缓存设定的阈值，淘汰最长时间没有访问到数据。在 LRU 算法的思想下，如果一段数据在最近的时间内没有被访问到，那么它接下来被访问到的概率也很小，执行淘汰。</p>
<p>在某些情况下，LRU算法是简单且有效的，但是在有些情况下，它就并不是十分的合理，如下操作。</p>
<table>
<thead>
<tr>
<th>最早访问</th>
<th>中间访问</th>
<th>最晚访问</th>
<th></th>
</tr>
</thead>
<tbody>
<tr>
<td>A</td>
<td>B</td>
<td>C</td>
<td>初始数据</td>
</tr>
<tr>
<td>B</td>
<td>C</td>
<td>A</td>
<td>访问A</td>
</tr>
<tr>
<td>B</td>
<td>A</td>
<td>C</td>
<td>访问C</td>
</tr>
<tr>
<td>B</td>
<td>A</td>
<td>C</td>
<td>访问C</td>
</tr>
<tr>
<td>B</td>
<td>C</td>
<td>A</td>
<td>访问A</td>
</tr>
<tr>
<td>B</td>
<td>A</td>
<td>C</td>
<td>访问C</td>
</tr>
<tr>
<td>A</td>
<td>C</td>
<td>B</td>
<td>访问B</td>
</tr>
<tr>
<td>C</td>
<td>B</td>
<td>D</td>
<td>插入D</td>
</tr>
</tbody>
</table>
<p>经过多次访问后，最终插入数据时淘汰了数据A，而通过访问情况我们可以发现，数据A和C的访问频率都相比B要高，而淘汰策略却保留了B舍弃了A。在局部性显著的时候，LRU这种淘汰策略也是正确的，但是在其它局部性不显著的大量数据访问的情况下（如全量遍历），缓存就有可能被污染，导致查询性能下降。且LRU对热门数据的保护不强，不过这也使得LRU有用更强的访问模式适应能力</p>
<blockquote>
<p>此处由于展示方式有限，数据量较少，可自行脑补大量数据时产生热门数据被淘汰的特殊情况</p>
</blockquote>
<h3>LFU 算法</h3>
<p>LFU（Least Frequently Used）即最近最少使用算法。</p>
<p>LFU算法根据数据的历史访问频率来淘汰数据，其核心思想是“如果数据过去被访问多次，那么将来被访问的频率也更高”。</p>
<p>由于其淘汰策略所致，对于突发的稀疏流量，LFU的应对能力不如LRU，大量新数据可能不被缓存，但LFU所带来的好处就是其对于热点数据的缓存命中率会更高</p>
<h3>优缺点对比</h3>
<p>LRU</p>
<ul>
<li>优点：实现简单，可以应对突变的访问模式</li>
<li>缺点：难以应对缓存污染，对于热数据的缓存命中率低于LFU</li>
</ul>
<p>LFU</p>
<ul>
<li>优点：拥有更高的热门数据命中率</li>
<li>缺点：难以应对突变的稀疏流量、可能存在旧数据长期不淘汰，且需要额外消耗来记录更新访问次数</li>
</ul>
<h2>Window-TinyLFU算法</h2>
<p>在 <code>Java</code> 中有一个很出名的 <code>Caffeine</code> 的高性能本地缓存库，正是因为其实现的 <code>Window-TinyLFU</code> 的回收策略为它提供了良好的缓存命中率。</p>
<p>在《TinyLFU: A Highly E cient Cache Admission Policy》论文中，详细介绍了 TinyLFU 这种通过LRU实现类LFU功能的结构设计。</p>
<h2>实现</h2>
<p>对外暴露的结构体，其中包含了读写锁、window-lr、分段lru、布隆过滤器、cmSketch算法次数统计器、保险的阈值</p>
<pre><code class="language-go">type Cache struct {
    // 读写锁
    m         sync.RWMutex
    // window-lru
    lru       *windowLRU
    // 分段lru
    slru      *segmentedLRU
    // 布隆过滤器
    bf        *BloomFilter
    // cmSketch 次数统计器
    c         *cmSketch
    // total 总共的访问次数
    t         int32
    // 保险设计的阈值 
    threshold int32
    // 数据的实际存储
    data      map[uint64]*list.Element
}</code></pre>
<h2>window-lru</h2>
<pre><code class="language-go">type windowLRU struct {
    data map[uint64]*list.Element
    cap  int
    list *list.List
}

type storeItem struct {
    stage    int
    key      uint64
    conflict uint64
    value    interface{}
}

func newWindowLRU(size int, data map[uint64]*list.Element) *windowLRU {
    return &amp;windowLRU{
        data: data,
        cap:  size,
        list: list.New(),
    }
}

func (lru *windowLRU) add(newItem storeItem) (eitem storeItem, evicted bool) {
    // If part of window is not full, insert it directly
    if lru.list.Len() &lt; lru.cap {
        lru.data[newItem.key] = lru.list.PushFront(&amp;newItem)
        return storeItem{}, false
    }7

    evictItem := lru.list.Back()
    item := evictItem.Value.(*storeItem)

    delete(lru.data, item.key)

    eitem, *item = *item, newItem

    lru.data[item.key] = evictItem
    lru.list.MoveToFront(evictItem)
    return eitem, true
}

func (lru *windowLRU) get(v *list.Element) {
    lru.list.MoveToFront(v)
}

func (lru *windowLRU) String() (s string) {
    for e := lru.list.Front(); e != nil; e = e.Next() {
        s += fmt.Sprintf(&quot;%v,&quot;, e.Value.(*storeItem).value)
    }
    return s
}</code></pre>
<h2>segmented-lru</h2>
<pre><code class="language-go">type segmentedLRU struct {
    data                     map[uint64]*list.Element
    stageOneCap, stageTwoCap int
    stageOne, stageTwo       *list.List
}

const (
    STAGE_ONE = iota + 1
    STAGE_TWO
)

func newS2LRU(data map[uint64]*list.Element, stageOneCap, stageTwoCap int) *segmentedLRU {
    return &amp;segmentedLRU{
        data:        data,
        stageOneCap: stageOneCap,
        stageTwoCap: stageTwoCap,
        stageOne:    list.New(),
        stageTwo:    list.New(),
    }
}

func (s2lru *segmentedLRU) add(newItem storeItem) {
    // 先进来的都放 stageOne
    newItem.stage = STAGE_ONE
    // 如果 stageOne 没满, 整个LFU区域也没满
    if s2lru.stageOne.Len() &lt; s2lru.stageOneCap || s2lru.Len() &lt; s2lru.stageOneCap+s2lru.stageTwoCap {
        s2lru.data[newItem.key] = s2lru.stageOne.PushFront(&amp;newItem)
        return
    }
    // 走到这里说明 stageOne 满了,或者整个LFU都满了
    // 则需要载 stageOne 淘汰数据
    e := s2lru.stageOne.Back()
    item := e.Value.(*storeItem)
    // 淘汰数据
    delete(s2lru.data, item.key)
    *item = newItem
    s2lru.data[item.key] = e
    s2lru.stageOne.MoveToFront(e)
}

func (s2lru *segmentedLRU) get(v *list.Element) {
    item := v.Value.(*storeItem)

    // 若访问的缓存数据已经载StageTwo,只需要按照LRU规则提前即可
    if item.stage == STAGE_TWO {
        s2lru.stageTwo.MoveToFront(v)
        return
    }
    // 若访问的数据还在StageOne,那么两次被访问倒,就需要提升到StageTwo阶段了
    if s2lru.stageTwo.Len() &lt; s2lru.stageTwoCap {
        s2lru.stageOne.Remove(v)
        item.stage = STAGE_TWO
        s2lru.data[item.key] = s2lru.stageTwo.PushFront(item)
        return
    }
    // 新数据加入StageTwo, 需要淘汰旧数据
    // StageTwo 中淘汰的数据不会丢失,会进入StageOne
    // StageOne 中,访问频率低的数据,可能会被淘汰
    // 将第二个链表和第一个链表中的数据进行交换
    back := s2lru.stageTwo.Back()
    bItem := back.Value.(*storeItem)
    *bItem, *item = *item, *bItem
    bItem.stage = STAGE_TWO
    item.stage = STAGE_ONE

    // 数据提前
    s2lru.data[item.key] = v
    s2lru.data[bItem.key] = back
    s2lru.stageOne.MoveToFront(v)
    s2lru.stageOne.MoveToFront(back)
}
func (s2lru *segmentedLRU) victim() *storeItem {
    // 如果s2lru的容量未满,不需要淘汰
    if s2lru.Len() &lt; s2lru.stageOneCap+s2lru.stageTwoCap {
        return nil
    }
    // 如果已经满了, 则需要从20%的区域淘汰数据,直接从末尾部拿最后一个数据即可
    v := s2lru.stageOne.Back()
    return v.Value.(*storeItem)
}

func (s2lru *segmentedLRU) String() (s string) {
    for e := s2lru.stageTwo.Front(); e != nil; e = e.Next() {
        s += fmt.Sprintf(&quot;%v,&quot;, e.Value.(*storeItem).value)
    }
    s += fmt.Sprintf(&quot; | &quot;)
    for e := s2lru.stageOne.Front(); e != nil; e = e.Next() {
        s += fmt.Sprintf(&quot;%v,&quot;, e.Value.(*storeItem).value)
    }
    return s
}

func (s2lru *segmentedLRU) Len() int {
    return s2lru.stageOne.Len() + s2lru.stageTwo.Len()
}</code></pre>
<h2>cmSketch</h2>
<pre><code class="language-go">const (
    cmDepth = 4
)

type cmSketch struct {
    rows [cmDepth]cmRow
    seed [cmDepth]uint64
    mask uint64
}

func newCmSketch(numCounters int64) *cmSketch {
    if numCounters == 0 {
        panic(&quot;cmSketch: invalid numCounters&quot;)
    }
    // 因为在位图的实际存储中2个Counters存放在一个byte中，所以numCounters为一定为偶数
    numCounters = next2Power(numCounters)
    // mask 为numcounter - 1 即一定是0111...111，用以保留后n位
    sketch := &amp;cmSketch{mask: uint64(numCounters - 1)}
    source := rand.New(rand.NewSource(time.Now().UnixNano()))

    // 假设预计cache 6条数据，初始化[4]rows如下
    // 0000,0000|0000,0000|0000,0000
    // 0000,0000|0000,0000|0000,0000
    // 0000,0000|0000,0000|0000,0000
    // 0000,0000|0000,0000|0000,0000
    for i := 0; i &lt; cmDepth; i++ {
        sketch.seed[i] = source.Uint64()
        sketch.rows[i] = newCmRow(numCounters)
    }
    return sketch
}

// 在计数器中增加某key的计数
func (s *cmSketch) Increment(hashed uint64) {
    // 对于每行进行相同操作
    for i := range s.rows {
        s.rows[i].increment((hashed ^ s.seed[i]) &amp; s.mask)
    }
}

// 估算的访问次数
func (s *cmSketch) Estimate(hashed uint64) int64 {
    min := byte(255)
    for i := range s.rows {
        val := s.rows[i].get((hashed ^ s.seed[i]) &amp; s.mask)
        if val &lt; min {
            min = val
        }
    }
    return int64(min)
}

// 将所有计数器值减半，即保鲜机制
func (s *cmSketch) Reset() {
    for _, r := range s.rows {
        r.reset()
    }
}

// 将所有计数器归零
func (s *cmSketch) Clear() {
    for _, r := range s.rows {
        r.clear()
    }
}

// 快速计算大于 X，且最接近 X 的二次幂
func next2Power(x int64) int64 {
    x--
    x |= x &gt;&gt; 1
    x |= x &gt;&gt; 2
    x |= x &gt;&gt; 4
    x |= x &gt;&gt; 8
    x |= x &gt;&gt; 16
    x |= x &gt;&gt; 32
    x++
    return x
}

// 计数器位图
type cmRow []byte

// 计数器的每个key的计数值（counter）占用4bit，每个byte为8bit，故cmRow的长度为计数总量的一半
func newCmRow(numCounters int64) cmRow {
    return make(cmRow, numCounters/2)
}

func (r cmRow) get(n uint64) byte {
    return r[n/2] &gt;&gt; ((n &amp; 1) * 4) &amp; 0x0f
}

func (r cmRow) increment(n uint64) {
    // 定位到第i个couter
    i := n / 2
    // 右移距离，偶数为0，奇数为4
    // 决定了取前4bit 还是后4bit
    s := (n &amp; 1) * 4
    v := (r[i] &gt;&gt; s) &amp; 0x0f
    // 若没有超过最大计数，则计数+1
    if v &lt; 15 {
        r[i] += 1 &lt;&lt; s
    }
}

// 保险机制
func (r cmRow) reset() {
    // 给每个byte中的2个counter同时减半
    for i := range r {
        r[i] = (r[i] &gt;&gt; 1) &amp; 0x77
    }
}

// 清零
func (r cmRow) clear() {
    for i := range r {
        r[i] = 0
    }
}

func (r cmRow) String() (s string) {
    for i := uint64(0); i &lt; uint64(len(r)*2); i++ {
        s += fmt.Sprintf(&quot;%02d &quot;, (r[(i/2)]&gt;&gt;((i&amp;1)*4))&amp;0x0f)
    }
    s = s[:len(s)-1]
    return s
}</code></pre>
<h2>最终的封装</h2>
<pre><code class="language-go">type Cache struct {
    m         sync.RWMutex
    lru       *windowLRU
    slru      *segmentedLRU
    bf        *BloomFilter
    c         *cmSketch
    t         int32
    threshold int32
    data      map[uint64]*list.Element
}

type Options struct {
    lruPct uint8
}

// NewCache size: 要缓存的数据数量
func NewCache(size int) *Cache {
    // 定义window部分缓存所占百分比,这里定义为1%
    const lruPct = 1
    // 计算window部分的容量
    lruSize := (lruPct * size) / 100
    if lruSize &lt; 1 {
        lruSize = 1
    }
    // 计算LFU部分的缓存容量
    slruSize := int(float64(size) * ((100 - lruPct) / 100.0))
    if slruSize &lt; 1 {
        slruSize = 1
    }
    // LFU 分为两部分, stageOne部分占比20%
    slru1 := int(0.2 * float64(slruSize))
    if slru1 &lt; 1 {
        slru1 = 1
    }
    data := make(map[uint64]*list.Element, size)
    return &amp;Cache{
        lru:  newWindowLRU(lruSize, data),
        slru: newS2LRU(data, slru1, slruSize-slru1),
        bf:   NewBloomFilter(size, 0.01),
        c:    newCmSketch(int64(size)),
        data: data,
    }
}

// Set
// todo Optimize this method by using generics
func (c *Cache) Set(key, value interface{}) bool {
    c.m.Lock()
    defer c.m.Lock()
    return c.set(key, value)
}

func (c *Cache) set(key, value interface{}) bool {
    // keyHash 用来快速定位, conflictHash 用来判断冲突
    keyHash, conflictHash := c.key2Hash(key)
    // 刚放进去的缓存都先放到window lru 中, 所以stage = 0
    i := storeItem{
        stage:    0,
        key:      keyHash,
        conflict: conflictHash,
        value:    value,
    }
    // 如果window 已满, 返回被淘汰的数据
    eitem, evicted := c.lru.add(i)

    if !evicted {
        return true
    }

    // 如果window中有被淘汰的数据,会走到这里
    // 需要从LFU的stageOne 部分找到一个淘汰者
    // 二者进行再次比较
    victim := c.slru.victim()
    // 如果LFU未满,那么window lru的淘汰数据,可以进入stageOne
    if victim == nil {
        c.slru.add(eitem)
        return true
    }
    // 先在bloomfilter中查找
    // 如果存在,说明访问频率 &gt;= 2
    if !c.bf.Allow(uint32(eitem.key)) {
        return true
    }
    // 估算windowlru和LFU中淘汰数据, 历史访问频次
    // 访问频率高的,更有资格留下
    vcount := c.c.Estimate(victim.key)
    ocount := c.c.Estimate(eitem.key)
    if ocount &lt; vcount {
        return true
    }
    // 留下来的进入 stageOne
    c.slru.add(eitem)
    return true
}

func (c *Cache) Get(key interface{}) (interface{}, bool) {
    c.m.RLock()
    defer c.m.RUnlock()
    return c.get(key)
}

func (c *Cache) get(key interface{}) (interface{}, bool) {
    c.t++
    if c.t == c.threshold {
        c.c.Reset()
        c.bf.Reset()
        c.t = 0
    }
    keyHash, confilctHash := c.key2Hash(key)
    val, ok := c.data[keyHash]
    if !ok {
        c.bf.Allow(uint32(keyHash))
        c.c.Increment(keyHash)
        return nil, false
    }
    item := val.Value.(*storeItem)
    if item.conflict != confilctHash {
        c.bf.Allow(uint32(keyHash))
        c.c.Increment(keyHash)
        return nil, false
    }
    c.bf.Allow(uint32(keyHash))
    c.c.Increment(item.key)

    v := item.value
    if item.stage == 0 {
        c.lru.get(val)
    } else {
        c.slru.get(val)
    }
    return v, true
}

func (c *Cache) Del(key interface{}) (interface{}, bool) {
    c.m.Lock()
    defer c.m.Unlock()
    return c.del(key)
}

func (c *Cache) del(key interface{}) (interface{}, bool) {
    keyHash, conflictHash := c.key2Hash(key)
    val, ok := c.data[keyHash]
    if !ok {
        return 0, false
    }
    item := val.Value.(*storeItem)

    if conflictHash != 0 &amp;&amp; (conflictHash != item.conflict) {
        return 0, false
    }
    delete(c.data, keyHash)
    return item.conflict, true
}

func (c *Cache) key2Hash(key interface{}) (uint64, uint64) {
    if key == nil {
        return 0, 0
    }
    switch k := key.(type) {
    case uint64:
        return k, 0
    case string:
        return MemHashString(k), xxhash.Sum64String(k)
    case []byte:
        return MemHash(k), xxhash.Sum64(k)
    case byte:
        return uint64(k), 0
    case int:
        return uint64(k), 0
    case int32:
        return uint64(k), 0
    case uint32:
        return uint64(k), 0
    case int64:
        return uint64(k), 0
    default:
        panic(&quot;Key type not supported&quot;)
    }
}

type stringStruct struct {
    str unsafe.Pointer
    len int
}

//go:noescape
//go:linkname memhash runtime.memhash
func memhash(p unsafe.Pointer, h, s uintptr) uintptr

func MemHashString(str string) uint64 {
    ss := (*stringStruct)(unsafe.Pointer(&amp;str))
    return uint64(memhash(ss.str, 0, uintptr(ss.len)))
}

func MemHash(data []byte) uint64 {
    ss := (*stringStruct)(unsafe.Pointer(&amp;data))
    return uint64(memhash(ss.str, 0, uintptr(ss.len)))
}</code></pre>
<h2>扩展阅读</h2>
<p>[《TinyLFU: A Highly E cient Cache Admission Policy》](<a href="https://arxiv.org/abs/1512.00727">[1512.00727] TinyLFU: A Highly Efficient Cache Admission Policy (arxiv.org)</a>)</p>
<p><a rel="nofollow" href="https://www.crazyfay.com/2023/02/02/window-tinylfu%e7%bc%93%e5%ad%98%e5%ae%9e%e7%8e%b0/">Window-TinyLFU缓存实现</a>最先出现在<a rel="nofollow" href="https://www.crazyfay.com">枫阿雨&#039;s blog</a>。</p>
]]></content:encoded>
					
					<wfw:commentRss>https://www.crazyfay.com/2023/02/02/window-tinylfu%e7%bc%93%e5%ad%98%e5%ae%9e%e7%8e%b0/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
			</item>
		<item>
		<title>记一次gorm.ErrRecordNotFound踩坑记录</title>
		<link>https://www.crazyfay.com/2022/08/20/%e8%ae%b0%e4%b8%80%e6%ac%a1gorm-errrecordnotfound%e8%b8%a9%e5%9d%91%e8%ae%b0%e5%bd%95/</link>
					<comments>https://www.crazyfay.com/2022/08/20/%e8%ae%b0%e4%b8%80%e6%ac%a1gorm-errrecordnotfound%e8%b8%a9%e5%9d%91%e8%ae%b0%e5%bd%95/#respond</comments>
		
		<dc:creator><![CDATA[crazyfay]]></dc:creator>
		<pubDate>Sat, 20 Aug 2022 03:05:31 +0000</pubDate>
				<category><![CDATA[实践经验]]></category>
		<category><![CDATA[Golang]]></category>
		<category><![CDATA[踩坑日记]]></category>
		<guid isPermaLink="false">http://net.crazyfay.xyz/?p=164</guid>

					<description><![CDATA[<p>在某个项目中，有个数据验证的业务，即在数据库中查询数据是否存在，若数据已存在则返回错误并给前端提示。稍想了一下 [&#8230;]</p>
<p><a rel="nofollow" href="https://www.crazyfay.com/2022/08/20/%e8%ae%b0%e4%b8%80%e6%ac%a1gorm-errrecordnotfound%e8%b8%a9%e5%9d%91%e8%ae%b0%e5%bd%95/">记一次gorm.ErrRecordNotFound踩坑记录</a>最先出现在<a rel="nofollow" href="https://www.crazyfay.com">枫阿雨&#039;s blog</a>。</p>
]]></description>
										<content:encoded><![CDATA[<p>在某个项目中，有个数据验证的业务，即在数据库中查询数据是否存在，若数据已存在则返回错误并给前端提示。稍想了一下就能写出如下代码<code>Func01</code></p>
<p><code>Func01</code></p>
<pre><code class="language-go">func (t *ServiceInfo) Find(c *gin.Context, tx *gorm.DB, search *ServiceInfo) (*ServiceInfo, error) {
    out := &amp;ServiceInfo{}
    err := tx.WithContext(c).Where(search).Find(out).Error
    if err != nil {
        return nil, err
    }
    return out, nil
}</code></pre>
<p>gorm在之前的版本中，因为gorm的查询是链式的语句，所以中间出现的错误会存入到Error的参数集中处理。而且当没有查询到数据的时候也会得到错误ErrRecordNotFound。所以此代码就把错误同一处理，当controller中没有收到任何错误时，可以说明数据库中查询到了此数据，即校验重复了。</p>
<p>但是经过测试后发现无论如何err都是nil，且RowsAffected也明明为0。在网上也没有直接搜到这个坑的blog，于是我去翻了gorm最新的文档，发现了此段话！</p>
<p><img decoding="async" src="https://img-1307890592.cos.ap-chengdu.myqcloud.com/typroa/image-20220725104008262.png" alt="image-20220725104008262" /></p>
<p>即说明Find()方法不会再得到ErrRecordNotFound的错误</p>
<p>于是我采用了First()进行了测试</p>
<p><code>Func02</code></p>
<pre><code class="language-go">func (t *ServiceInfo) Find(c *gin.Context, tx *gorm.DB, search *ServiceInfo) (*ServiceInfo, error) {
    out := &amp;ServiceInfo{}
    resultFind := tx.WithContext(c).Where(search).Find(out)
    resultFirst := tx.WithContext(c).Where(search).First(out)
    log.Print(&quot;Find() Err: &quot;, resultFind.Error, &quot;\tFind Rows Affected: &quot;, resultFind.RowsAffected)
    log.Print(&quot;First() Err: &quot;, resultFirst.Error, &quot;\tFind Rows Affected: &quot;, resultFirst.RowsAffected)
    err := resultFind.Error
    if err != nil {
        return nil, err
    }
    return out, nil
}</code></pre>
<p>得到输出</p>
<pre><code class="language-shell">2022/07/25 10:00:35 Find() Err: &lt;nil&gt;   Find Rows Affected: 0
2022/07/25 10:00:35 First() Err: record not found       Find Rows Affected: 0</code></pre>
<p>很明显，当查询不到结果的时候First()方法会返回ErrRecordNotFound，而Find()方法并不会</p>
<p>因此，若不改变代码原有的逻辑基础上，可以通过手动添加Error的方法来完成数据校验的工作</p>
<p><code>Func03</code></p>
<pre><code class="language-go">func (t *ServiceInfo) Find(c *gin.Context, tx *gorm.DB, search *ServiceInfo) (*ServiceInfo, error) {
    out := &amp;ServiceInfo{}
    resultFind := tx.WithContext(c).Where(search).Find(out)
    if resultFind.RowsAffected &lt; 1 {
        err := resultFind.AddError(gorm.ErrRecordNotFound)
        if err != nil {
            return nil, err
        }
    }
    err := resultFind.Error
    if err != nil {
        return nil, err
    }
    return out, nil
}</code></pre>
<p>即通过<code>resultFind.RowsAffected &lt; 1</code>来判断是否查询到数据，再通过 <code>resultFind.AddError(gorm.ErrRecordNotFound)</code> 手动添加</p>
<p>ErrRecordNotFound错误，藉此来完成在旧版本中存在的功能。</p>
<blockquote>
<p>PS：我个人并不是很理解为什么要取消Find()方法中的这个错误提示</p>
<p>此实现方式仅供参考，如有更漂亮的方法希望不吝赐教</p>
</blockquote>
<p><a rel="nofollow" href="https://www.crazyfay.com/2022/08/20/%e8%ae%b0%e4%b8%80%e6%ac%a1gorm-errrecordnotfound%e8%b8%a9%e5%9d%91%e8%ae%b0%e5%bd%95/">记一次gorm.ErrRecordNotFound踩坑记录</a>最先出现在<a rel="nofollow" href="https://www.crazyfay.com">枫阿雨&#039;s blog</a>。</p>
]]></content:encoded>
					
					<wfw:commentRss>https://www.crazyfay.com/2022/08/20/%e8%ae%b0%e4%b8%80%e6%ac%a1gorm-errrecordnotfound%e8%b8%a9%e5%9d%91%e8%ae%b0%e5%bd%95/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
			</item>
		<item>
		<title>Golang实现支持中间件的简易TCP框架</title>
		<link>https://www.crazyfay.com/2022/08/16/golang%e5%ae%9e%e7%8e%b0%e6%94%af%e6%8c%81%e4%b8%ad%e9%97%b4%e4%bb%b6%e7%9a%84%e7%ae%80%e6%98%93tcp%e6%a1%86%e6%9e%b6/</link>
					<comments>https://www.crazyfay.com/2022/08/16/golang%e5%ae%9e%e7%8e%b0%e6%94%af%e6%8c%81%e4%b8%ad%e9%97%b4%e4%bb%b6%e7%9a%84%e7%ae%80%e6%98%93tcp%e6%a1%86%e6%9e%b6/#respond</comments>
		
		<dc:creator><![CDATA[crazyfay]]></dc:creator>
		<pubDate>Tue, 16 Aug 2022 07:48:29 +0000</pubDate>
				<category><![CDATA[代码实战]]></category>
		<category><![CDATA[Golang]]></category>
		<category><![CDATA[TCP]]></category>
		<category><![CDATA[网络]]></category>
		<guid isPermaLink="false">http://net.crazyfay.xyz/?p=138</guid>

					<description><![CDATA[<p>在golang的标准库中没有为tcp直接提供像http那样简单易用的服务框架，我们不妨自己手动实现一个 主体思 [&#8230;]</p>
<p><a rel="nofollow" href="https://www.crazyfay.com/2022/08/16/golang%e5%ae%9e%e7%8e%b0%e6%94%af%e6%8c%81%e4%b8%ad%e9%97%b4%e4%bb%b6%e7%9a%84%e7%ae%80%e6%98%93tcp%e6%a1%86%e6%9e%b6/">Golang实现支持中间件的简易TCP框架</a>最先出现在<a rel="nofollow" href="https://www.crazyfay.com">枫阿雨&#039;s blog</a>。</p>
]]></description>
										<content:encoded><![CDATA[<p>在golang的标准库中没有为tcp直接提供像http那样简单易用的服务框架，我们不妨自己手动实现一个</p>
<h2>主体思路</h2>
<p>我们的实现的主题思路分为以下四个内容</p>
<ol>
<li>监听服务</li>
<li>获取构建新连接对象并设置超时时间及keepalive</li>
<li>设置方法退出时连接关闭</li>
<li>调用回调接口 TcpHandler</li>
</ol>
<h2>主要结构体和接口</h2>
<p>首先是TCPServer的结构体，我们希望用户可以自由构建TcpServer并设置超时时间等自定义选项</p>
<pre><code class="language-go">type TcpServer struct {
   Addr    string
   Handler TCPHandler  &lt;- 对外提供的服务方法接口
   err     error
   BaseCtx context.Context

   WriteTimeout     time.Duration
   ReadTimeout      time.Duration
   KeepAliveTimeout time.Duration

   mu         sync.Mutex
   inShutdown int32
   doneChan   chan struct{}
   l          *onceCloseListener
}</code></pre>
<p>像httpHandler一样，对外提供抽象的ServeTCP方法</p>
<pre><code class="language-go">type TCPHandler interface {
   ServeTCP(ctx context.Context, conn net.Conn)
}</code></pre>
<h2>服务启动方法</h2>
<p>用户可以通过自行构建TcpServer实例再通过ListenAndServe()调用服务，或通过<code>tcp.ListenAndServe(&quot;:8080&quot;, handler)</code> 使用默认的TcpServer实例快速启动服务。</p>
<p>在 <code>ListenAndServe()</code> 方法中，进行参数的校验和初始化操作 </p>
<p><code>Serve(l net.Listener)</code> 方法中，通过 <code>l.Accept()</code> 接收信息，包装接收到的conn并另起一个协程处理服务</p>
<pre><code class="language-go">func ListenAndServe(addr string, handler TCPHandler) error {
    server := &amp;TcpServer{Addr: addr, Handler: handler, doneChan: make(chan struct{})}
    return server.ListenAndServe()
}

func (s *TcpServer) ListenAndServe() error {
   if s.shuttingDown() {
      return ErrServerClosed
   }
   if s.doneChan == nil {
      s.doneChan = make(chan struct{})
   }
   addr := s.Addr
   if addr == &quot;&quot; {
      return errors.New(&quot;need addr&quot;)
   }
   ln, err := net.Listen(&quot;tcp&quot;, addr)
   if err != nil {
      return err
   }
   return s.Serve(tcpKeepAliveListener{
      ln.(*net.TCPListener)})
}

func (s *TcpServer) Serve(l net.Listener) error {
    s.l = &amp;onceCloseListener{Listener: l}
    defer s.l.Close() //执行listener关闭
    if s.BaseCtx == nil {
        s.BaseCtx = context.Background()
    }
    baseCtx := s.BaseCtx
    ctx := context.WithValue(baseCtx, ServerContextKey, s) &lt;- 将TcpServer实例存入context中
    for {
        rw, e := l.Accept()
        if e != nil {
            select {
            case &lt;-s.getDoneChan():
                return ErrServerClosed
            default:
            }
            fmt.Printf(&quot;accept fail, err: %v\n&quot;, e)
            continue
        }
        c := s.newConn(rw)
        go c.serve(ctx)
    }
    return nil
}</code></pre>
<p>包装 <code>net.Conn</code> 为 <code>tcp.conn</code> </p>
<pre><code class="language-go">type conn struct {
    server     *TcpServer   // 反引用TcpServer
    remoteAddr string       // 发送端地址
    rwc        net.Conn
}

func (s *TcpServer) newConn(rwc net.Conn) *conn {
   c := &amp;conn{
      server: s,
      rwc:    rwc,
   }
   // 设置参数
   if d := c.server.ReadTimeout; d != 0 {
      c.rwc.SetReadDeadline(time.Now().Add(d))
   }
   if d := c.server.WriteTimeout; d != 0 {
      c.rwc.SetWriteDeadline(time.Now().Add(d))
   }
   if d := c.server.KeepAliveTimeout; d != 0 {
      if tcpConn, ok := c.rwc.(*net.TCPConn); ok {
         tcpConn.SetKeepAlive(true)
         tcpConn.SetKeepAlivePeriod(d)
      }
   }
   return c
}</code></pre>
<p>由 <code>tcp.conn.Server(ctx)</code> 调用回调函数进行服务处理</p>
<pre><code class="language-go">func (c *conn) serve(ctx context.Context) {
   defer func() {
      if err := recover(); err != nil &amp;&amp; err != ErrAbortHandler {
         const size = 64 &lt;&lt; 10
         buf := make([]byte, size)
         buf = buf[:runtime.Stack(buf, false)]
         fmt.Printf(&quot;tcp: panic serving %v: %v\n%s&quot;, c.remoteAddr, err, buf)
      }
      c.close()
   }()
   c.remoteAddr = c.rwc.RemoteAddr().String()
   ctx = context.WithValue(ctx, LocalAddrContextKey, c.rwc.LocalAddr())
   if c.server.Handler == nil {
      panic(&quot;handler empty&quot;)
   }
   c.server.Handler.ServeTCP(ctx, c.rwc)
}</code></pre>
<p>这样，一个简单易用的TCP服务框架就搭建完成了，其中一些<code>close()</code> 等方法在此处没有展示出来，更多详细代码可在我的代码仓库中查看：<a href="https://github.com/Kirov7/fayUtils/net/tcp">https://github.com/Kirov7/fayUtils/net/tcp</a></p>
<h2>扩展中间件的实现</h2>
<p>扩展中间件功能的实现思路</p>
<ul>
<li>方法构建
<ul>
<li>构建中间件URL路由</li>
<li>构建URL的中间件方法数组</li>
<li>使用Use方法整合路由与方法数组</li>
</ul>
</li>
<li>方法调用
<ul>
<li>构建方法请求逻辑</li>
<li>封装TCPHandler接口与TcpServer整合</li>
</ul>
</li>
</ul>
<p><code>TcpSliceRouter.Group(path)</code> 方法，初始化路由分组（默认只能全局）</p>
<pre><code class="language-go">// 创建 Group
func (g *TcpSliceRouter) Group(path string) *TcpSliceGroup {
   if path != &quot;/&quot; {
      panic(&quot;only accept path=/&quot;)
   }
   return &amp;TcpSliceGroup{
      TcpSliceRouter: g,
      path:           path,
   }
}</code></pre>
<p><code>TcpSliceGroup.Use(middlewares ...TcpHandlerFunc)</code> 构造回调方法</p>
<p>调用 <code>Use</code> 方法传入中间件集合，添加到切片 <code>c.handlers</code> 中</p>
<pre><code class="language-go">// 构造回调方法
func (g *TcpSliceGroup) Use(middlewares ...TcpHandlerFunc) *TcpSliceGroup {
   g.handlers = append(g.handlers, middlewares...)
   existsFlag := false
   for _, oldGroup := range g.TcpSliceRouter.groups {
      if oldGroup == g {
         existsFlag = true
      }
   }
   if !existsFlag {
      g.TcpSliceRouter.groups = append(g.TcpSliceRouter.groups, g)
   }
   return g
}</code></pre>
<p>通过 <code>NewTcpSliceRouterHandler</code> 方法传入最后调用的逻辑方法<code>coreFunc</code>并传入已经 <code>Use</code> 了中间件的， <code>TcpSliceRouter</code> </p>
<pre><code class="language-go">func NewTcpSliceRouterHandler(coreFunc func(*TcpSliceRouterContext) tcp_server.TCPHandler, router *TcpSliceRouter) *TcpSliceRouterHandler {
   return &amp;TcpSliceRouterHandler{
      coreFunc: coreFunc,
      router:   router,
   }
}</code></pre>
<p>最终的回调函数 <code>ServeTCP((ctx context.Context, conn net.Conn)</code>，初始化 <code>context</code> 之后将 <code>coreFunc</code> 追加到 <code>c.handlers</code>，重置执行光标，从第一个 <code>c.handlers</code> 开始执行中间件</p>
<pre><code class="language-go">func (w *TcpSliceRouterHandler) ServeTCP(ctx context.Context, conn net.Conn) {
   c := newTcpSliceRouterContext(conn, w.router, ctx)
   c.handlers = append(c.handlers, func(c *TcpSliceRouterContext) {
      w.coreFunc(c).ServeTCP(ctx, conn)
   })
   c.Reset()
   c.Next()
}</code></pre>
<p>在中间件中自行调用<code>Next()</code>、<code>Abort()</code>等中间件逻辑，最后所有中间件执行完毕之后执行 <code>coreFunc</code>（已经被追加到<code>c.handlers</code>的最后位置）</p>
<pre><code class="language-go">// 从最先加入中间件开始回调
func (c *TcpSliceRouterContext) Next() {
   c.index++
   for c.index &lt; int8(len(c.handlers)) {
      c.handlers[c.index](c)
      c.index++
   }
}

// 跳出中间件方法
func (c *TcpSliceRouterContext) Abort() {
    c.index = abortIndex
}</code></pre>
<p><a rel="nofollow" href="https://www.crazyfay.com/2022/08/16/golang%e5%ae%9e%e7%8e%b0%e6%94%af%e6%8c%81%e4%b8%ad%e9%97%b4%e4%bb%b6%e7%9a%84%e7%ae%80%e6%98%93tcp%e6%a1%86%e6%9e%b6/">Golang实现支持中间件的简易TCP框架</a>最先出现在<a rel="nofollow" href="https://www.crazyfay.com">枫阿雨&#039;s blog</a>。</p>
]]></content:encoded>
					
					<wfw:commentRss>https://www.crazyfay.com/2022/08/16/golang%e5%ae%9e%e7%8e%b0%e6%94%af%e6%8c%81%e4%b8%ad%e9%97%b4%e4%bb%b6%e7%9a%84%e7%ae%80%e6%98%93tcp%e6%a1%86%e6%9e%b6/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
			</item>
		<item>
		<title>Golang实现四种负载均衡算法</title>
		<link>https://www.crazyfay.com/2022/08/10/golang%e5%ae%9e%e7%8e%b0%e5%9b%9b%e7%a7%8d%e8%b4%9f%e8%bd%bd%e5%9d%87%e8%a1%a1%e7%ae%97%e6%b3%95/</link>
					<comments>https://www.crazyfay.com/2022/08/10/golang%e5%ae%9e%e7%8e%b0%e5%9b%9b%e7%a7%8d%e8%b4%9f%e8%bd%bd%e5%9d%87%e8%a1%a1%e7%ae%97%e6%b3%95/#respond</comments>
		
		<dc:creator><![CDATA[crazyfay]]></dc:creator>
		<pubDate>Wed, 10 Aug 2022 02:56:32 +0000</pubDate>
				<category><![CDATA[代码实战]]></category>
		<category><![CDATA[Golang]]></category>
		<category><![CDATA[负载均衡]]></category>
		<guid isPermaLink="false">http://net.crazyfay.xyz/?p=158</guid>

					<description><![CDATA[<p>负载均衡算法实现 在这篇文章中，我将实现最常见的四种负载均衡算法，即随机负载、轮询负载、加权负载和一致性has [&#8230;]</p>
<p><a rel="nofollow" href="https://www.crazyfay.com/2022/08/10/golang%e5%ae%9e%e7%8e%b0%e5%9b%9b%e7%a7%8d%e8%b4%9f%e8%bd%bd%e5%9d%87%e8%a1%a1%e7%ae%97%e6%b3%95/">Golang实现四种负载均衡算法</a>最先出现在<a rel="nofollow" href="https://www.crazyfay.com">枫阿雨&#039;s blog</a>。</p>
]]></description>
										<content:encoded><![CDATA[<h1>负载均衡算法实现</h1>
<p>在这篇文章中，我将实现最常见的四种负载均衡算法，即随机负载、轮询负载、加权负载和一致性hash负载</p>
<h2>随机负载</h2>
<p>随机挑选目标服务器ip</p>
<pre><code class="language-go">type RandomBalance struct {
   curIndex int
   rss      []string
}

// 添加新的服务ip
func (r *RandomBalance) Add(params ...string) error {
   if len(params) == 0 {
      return errors.New(&quot;param len 1 at least&quot;)
   }
   addr := params[0]
   r.rss = append(r.rss, addr)
   return nil
}

// 采用rand.Intn随机取一个服务ip
func (r *RandomBalance) Next() string {
   if len(r.rss) == 0 {
      return &quot;&quot;
   }
   r.curIndex = rand.Intn(len(r.rss))
   return r.rss[r.curIndex]
}

func (r *RandomBalance) Get(key string) (string, error) {
   return r.Next(), nil
}</code></pre>
<h2>轮询负载</h2>
<p>ABC三台服务器,A B C A B C依次轮询</p>
<pre><code class="language-go">type RoundRobinBalance struct {
   curIndex int
   rss      []string
}

func (r *RoundRobinBalance) Add(params ...string) error {
   if len(params) == 0 {
      return errors.New(&quot;param len 1 at least&quot;)
   }
   addr := params[0]
   r.rss = append(r.rss, addr)
   return nil
}

func (r *RoundRobinBalance) Next() string {
   if len(r.rss) == 0 {
      return &quot;&quot;
   }
   lens := len(r.rss)
   if r.curIndex &gt;= lens {
      r.curIndex = 0
   }
    // 保存一个服务ip的游标
   curAddr := r.rss[r.curIndex]
   r.curIndex = (r.curIndex + 1) % lens
   return curAddr
}

func (r *RoundRobinBalance) Get(key string) (string, error) {
   return r.Next(), nil
}</code></pre>
<h2>加权负载</h2>
<p>给目标设置访问权重,按照权重轮询</p>
<p><strong>nginx的加权负载均衡策略</strong></p>
<p>计算策略：</p>
<ol>
<li>currentWeight += effectiveWeight</li>
<li>选出最大的currentWeight节点作为选中节点</li>
<li>currentWeight -= totalWeight
<ul>
<li>其中effectiveWeight的值不变，只有当服务异常的时候减少</li>
</ul>
</li>
</ol>
<p><img decoding="async" src="https://img-1307890592.cos.ap-chengdu.myqcloud.com/typroa/image-20221123201123507.png" alt="image-20221123201123507" /></p>
<pre><code class="language-go">type WeightRoundRobinBalance struct {
   curIndex int
   rss      []*WeightNode
   rsw      []int
}

type WeightNode struct {
   addr            string
   weight          int //权重值
   currentWeight   int //节点当前权重
   effectiveWeight int //有效权重
}

func (r *WeightRoundRobinBalance) Add(params ...string) error {
   if len(params) != 2 {
      return errors.New(&quot;param len need 2&quot;)
   }
   parInt, err := strconv.ParseInt(params[1], 10, 64)
   if err != nil {
      return err
   }
   node := &amp;WeightNode{addr: params[0], weight: int(parInt)}
   node.effectiveWeight = node.weight
   r.rss = append(r.rss, node)
   return nil
}

// Next 参考了 nginx 的加权负载均衡的策略
func (r *WeightRoundRobinBalance) Next() string {
   total := 0
   var best *WeightNode
   for i := 0; i &lt; len(r.rss); i++ {
      w := r.rss[i]
      //step 1 统计所有有效权重之和
      total += w.effectiveWeight

      //step 2 变更节点临时权重为的节点临时权重+节点有效权重
      w.currentWeight += w.effectiveWeight

      //step 3 有效权重默认与权重相同，通讯异常时-1, 通讯成功+1，直到恢复到weight大小
      if w.effectiveWeight &lt; w.weight {
         w.effectiveWeight++
      }
      //step 4 选择最大临时权重点节点
      if best == nil || w.currentWeight &gt; best.currentWeight {
         best = w
      }
   }
   if best == nil {
      return &quot;&quot;
   }
   //step 5 变更临时权重为 临时权重-有效权重之和
   best.currentWeight -= total
   return best.addr
}

func (r *WeightRoundRobinBalance) Get(key string) (string, error) {
   return r.Next(), nil
}</code></pre>
<h2>一致性hash负载</h2>
<p>请求固定URL访问指定IP</p>
<pre><code class="language-go">type Hash func(data []byte) uint32

type UInt32Slice []uint32

func (s UInt32Slice) Len() int {
   return len(s)
}

func (s UInt32Slice) Less(i, j int) bool {
   return s[i] &lt; s[j]
}

func (s UInt32Slice) Swap(i, j int) {
   s[i], s[j] = s[j], s[i]
}

type ConsistentHashBanlance struct {
   mux      sync.RWMutex
   hash     Hash
   replicas int               // 复制因子
   keys     UInt32Slice       // 已排序的节点hash切片
   hashMap  map[uint32]string // 节点哈希和Key的map,键是hash值，值是节点key
}

func NewConsistentHashBanlance(replicas int, fn Hash) *ConsistentHashBanlance {
   m := &amp;ConsistentHashBanlance{
      replicas: replicas,
      hash:     fn,
      hashMap:  make(map[uint32]string),
   }
   if m.hash == nil {
      //最多32位,保证是一个2^32-1环
      m.hash = crc32.ChecksumIEEE
   }
   return m
}

// 验证是否为空
func (c *ConsistentHashBanlance) IsEmpty() bool {
   return len(c.keys) == 0
}

// Add 方法用来添加缓存节点，参数为节点key，比如使用IP
func (c *ConsistentHashBanlance) Add(params ...string) error {
   if len(params) == 0 {
      return errors.New(&quot;param len 1 at least&quot;)
   }
   addr := params[0]
   c.mux.Lock()
   defer c.mux.Unlock()
   // 结合复制因子计算所有虚拟节点的hash值，并存入m.keys中，同时在m.hashMap中保存哈希值和key的映射
   for i := 0; i &lt; c.replicas; i++ {
      hash := c.hash([]byte(strconv.Itoa(i) + addr))
      c.keys = append(c.keys, hash)
      c.hashMap[hash] = addr
   }
   // 对所有虚拟节点的哈希值进行排序，方便之后进行二分查找
   sort.Sort(c.keys)
   return nil
}

// Get 方法根据给定的对象获取最靠近它的那个节点
func (c *ConsistentHashBanlance) Get(key string) (string, error) {
   if c.IsEmpty() {
      return &quot;&quot;, errors.New(&quot;node is empty&quot;)
   }
   hash := c.hash([]byte(key))

   // 通过二分查找获取最优节点，第一个&quot;服务器hash&quot;值大于&quot;数据hash&quot;值的就是最优&quot;服务器节点&quot;
   idx := sort.Search(len(c.keys), func(i int) bool { return c.keys[i] &gt;= hash })

   // 如果查找结果 大于 服务器节点哈希数组的最大索引，表示此时该对象哈希值位于最后一个节点之后，那么放入第一个节点中
   if idx == len(c.keys) {
      idx = 0
   }
   c.mux.RLock()
   defer c.mux.RUnlock()
   return c.hashMap[c.keys[idx]], nil
}

func (c *ConsistentHashBanlance) SetConf(conf LoadBalanceConf) {
   c.conf = conf
}</code></pre>
<p><a rel="nofollow" href="https://www.crazyfay.com/2022/08/10/golang%e5%ae%9e%e7%8e%b0%e5%9b%9b%e7%a7%8d%e8%b4%9f%e8%bd%bd%e5%9d%87%e8%a1%a1%e7%ae%97%e6%b3%95/">Golang实现四种负载均衡算法</a>最先出现在<a rel="nofollow" href="https://www.crazyfay.com">枫阿雨&#039;s blog</a>。</p>
]]></content:encoded>
					
					<wfw:commentRss>https://www.crazyfay.com/2022/08/10/golang%e5%ae%9e%e7%8e%b0%e5%9b%9b%e7%a7%8d%e8%b4%9f%e8%bd%bd%e5%9d%87%e8%a1%a1%e7%ae%97%e6%b3%95/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
			</item>
	</channel>
</rss>
