<?xml version="1.0" encoding="utf-8" standalone="yes"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
  <channel>
    <title>清水泥沙</title>
    <link>https://869413421.github.io/</link>
    <description>Recent content on 清水泥沙</description>
    <generator>Hugo -- gohugo.io</generator>
    <language>en-zh</language>
    <lastBuildDate>Fri, 19 Apr 2024 17:35:18 +0800</lastBuildDate><atom:link href="https://869413421.github.io/index.xml" rel="self" type="application/rss+xml" />
    <item>
      <title>Transformer 学习之路 - BitFit 实战与深入解析</title>
      <link>https://869413421.github.io/post/transformer/chatbot_bitfit/</link>
      <pubDate>Fri, 19 Apr 2024 17:35:18 +0800</pubDate>
      
      <guid>https://869413421.github.io/post/transformer/chatbot_bitfit/</guid>
      <description>BitFit 实战与深入解析 BitFit 是一种参数高效微调（PEFT, Parameter-Efficient Fine-Tuning）方法，特别适用于大型语言模型的微调。它的核心思想是只微调模型的偏置项（bias），而冻结其他参数。这样可以减少微调所需的参数数量和存储成本，同时保持较高的性能。
BitFit 方法的基本概念 在深度学习模型中，每一层都有权重参数（weights）和偏置参数（biases）。在 BitFit 中：
只更新偏置参数：冻结模型的权重（weights），只调整偏置（biases）。 减少存储和计算需求：由于偏置项的数量远少于权重，这种方法大大降低了训练过程中的计算量和存储需求。 BitFit 的优缺点 优点 节省存储空间和计算资源：只更新偏置项，节省了大量的计算和存储成本。 迁移性强：由于权重参数不变，可以将微调后的偏置项应用到不同模型实例上，便于模型部署和版本管理。 适用于少量数据的任务：BitFit 在数据量较少的任务上表现尤为出色，能显著提高效率。 减少过拟合风险：只微调少量参数（偏置），可以避免模型在小数据集上过度拟合。 缺点 性能上限：BitFit 仅调整偏置参数，限制了模型的微调能力，因此在高复杂度或需要更深层次调整的任务中效果可能不及全面微调。 任务依赖性较高：BitFit 对某些任务（如高复杂度分类）可能不如全参数微调效果好。 局限性：BitFit 对需要大幅度知识迁移的任务（如极度不同的领域任务）效果有限，适合与预训练任务较相似的场景。 示例：使用 BitFit 微调情感分析模型 假设我们有一个预训练的 BERT 模型，并希望用少量的计算资源微调它来完成情感分类任务。以下是 BitFit 的实现步骤示例：
加载预训练模型，并将所有权重冻结。 解冻偏置项：只解冻每一层的偏置（bias），权重保持冻结。 训练模型：在情感分类数据集上训练，但只更新偏置项。 具体代码（使用 PyTorch 和 Hugging Face 的 Transformers 库）如下：
import torch from transformers import BertTokenizer, BertForSequenceClassification # 加载预训练模型和分词器 model = BertForSequenceClassification.from_pretrained(&amp;#34;bert-base-uncased&amp;#34;, num_labels=2) tokenizer = BertTokenizer.from_pretrained(&amp;#34;bert-base-uncased&amp;#34;) # 冻结模型的所有权重 for param in model.parameters(): param.requires_grad = False # 解冻偏置项（仅对偏置项设置 requires_grad 为 True） for name, param in model.</description>
    </item>
    
    <item>
      <title>Transformer 学习之路 - Prefix-Tuning 实战</title>
      <link>https://869413421.github.io/post/transformer/chatbot_prefix_tuning/</link>
      <pubDate>Fri, 19 Apr 2024 17:35:18 +0800</pubDate>
      
      <guid>https://869413421.github.io/post/transformer/chatbot_prefix_tuning/</guid>
      <description>Transformer 学习之路 - Prefix-Tuning 实战 在深度学习领域，Transformer 模型因其强大的表现力而广受欢迎。然而，随着模型规模的增大，如何高效地微调这些模型以适应特定任务成为了一个挑战。本文将深入探讨一种高效的微调方法——Prefix-Tuning，并展示其在实际应用中的操作步骤。
什么是 Prefix-Tuning？ Prefix-Tuning 是一种针对大型预训练语言模型的高效微调方法。它通过优化特定任务的“前缀”嵌入而不是调整整个模型的参数来实现模型的自适应。在 Prefix-Tuning 中，前缀并不是传统意义上的文本提示词，而是可学习的嵌入向量序列。这些前缀嵌入会与输入文本拼接在一起，引导模型在生成或预测时倾向于某种任务特性。
Prefix-Tuning 的核心思想 与 Prompt Tuning 和 P-tuning 类似，Prefix-Tuning 通过在模型的输入前加入可训练的前缀嵌入来引导模型完成特定任务。然而，Prefix-Tuning 的设计更关注条件生成（例如对话生成、摘要生成等）任务，通过仅优化前缀来保留模型的原始能力，同时减少训练成本。
Prefix-Tuning 的工作流程 初始化前缀嵌入：
Prefix-Tuning 会初始化一组前缀嵌入向量，这些向量的维度与模型的输入嵌入维度相同。 前缀嵌入与输入拼接：
这些前缀嵌入与输入文本的嵌入表示拼接在一起，构成模型的输入序列。例如，对于输入 “I love this product!” 以及前缀嵌入 [Prefix1]、[Prefix2] 等，最终的输入会是： [Prefix1] [Prefix2] ... [PrefixN] I love this product! 冻结模型参数：
为了减少训练开销，Prefix-Tuning 通常会冻结模型原本的参数，仅优化前缀嵌入向量的权重。 训练与优化前缀嵌入：
在训练过程中，前缀嵌入会逐步更新，以便适应特定的任务需求。通过这种方式，Prefix-Tuning 能引导模型生成或预测时，更加符合目标任务的上下文和期望输出。 Prefix-Tuning 的优势 参数高效：Prefix-Tuning 仅优化前缀嵌入而不修改模型的主体参数，这极大地降低了存储和计算成本。 任务迁移方便：只需为每个任务训练和保存相应的前缀嵌入，因此适合需要多任务切换的场景。 保留模型原始能力：由于不微调整个模型的权重，Prefix-Tuning 在执行特定任务时能保留模型原始的生成能力。 应用场景 Prefix-Tuning 特别适合条件生成任务，如：
对话生成：通过前缀嵌入向量引导模型生成具有特定风格或语境的对话。 摘要生成：在摘要生成任务中使用前缀嵌入来帮助模型生成简洁准确的摘要。 机器翻译：帮助模型适应特定的语言或语境转换需求。 示例 假设有一个对话生成任务，用户输入 “What is the weather like today?</description>
    </item>
    
    <item>
      <title>Transformer 学习之路 - Prompt Tuning 实战</title>
      <link>https://869413421.github.io/post/transformer/chatbot_prompt_tuning/</link>
      <pubDate>Fri, 19 Apr 2024 17:35:18 +0800</pubDate>
      
      <guid>https://869413421.github.io/post/transformer/chatbot_prompt_tuning/</guid>
      <description>Transformer 学习之路 - Prompt Tuning 实战 在深度学习的浩瀚宇宙中，Transformer 模型以其强大的表现力和广泛的应用场景，成为了自然语言处理（NLP）领域的明星。然而，随着模型规模的不断膨胀，如何高效地微调这些庞然大物以适应特定任务，成为了一个亟待解决的问题。今天，我们就来聊聊一种名为 Prompt Tuning 的参数高效微调方法，它如何在大型语言模型（如 GPT、BERT 等）中大显身手。
Prompt Tuning 的基本原理 传统的微调方法通常需要调整整个模型的参数，这不仅计算资源消耗巨大，而且在大规模模型上实施起来颇为困难。Prompt Tuning 则另辟蹊径，它通过在输入前面加入一组可学习的提示（prompt）来引导模型产生期望的输出，而不是微调模型的全部参数。
关键点 仅调整提示部分的参数：Prompt Tuning 引入了一组可训练的嵌入向量，作为输入的提示词，将它们放置在输入文本的前面，以调整模型的输出行为。 冻结模型权重：模型的主干参数保持不变，只有提示词的嵌入参数在训练中被优化。 Prompt Tuning 的优点 减少计算资源和存储需求：相比调整整个模型，Prompt Tuning 只更新提示词的嵌入向量，因而需要的计算和存储资源少得多。 迁移性好：在不同的任务上可以快速部署。尤其适合在大型模型上快速进行多任务适应。 提升少样本学习能力：Prompt Tuning 通过定制化提示引导模型，使得模型在少量样本任务中表现更为优异。 Prompt Tuning 与微调的对比 特性 传统微调 Prompt Tuning 调整的参数范围 整个模型参数 仅提示嵌入参数 计算资源需求 高 低 适用场景 大数据、复杂任务 少样本、特定任务 适用的模型大小 中小型模型 大型模型效果尤为显著 示例：Prompt Tuning 的应用场景 例如，如果我们要用 Prompt Tuning 来训练一个大型语言模型完成情感分类任务，可以采用以下步骤：
定义提示向量：给定一个输入句子“这个电影真棒！”，可以在前面加上一组初始的提示词，生成输入格式为： [提示词1] [提示词2] ... [提示词n] 这个电影真棒！ 训练提示词：冻结模型参数，仅更新这些提示词，使得模型能够从中识别出情感信息。 微调优化：在情感分类数据集上优化这些提示词，以便在推理时引导模型准确地输出“正面”或“负面”情感分类。 Soft Prompt 和 Hard Prompt 的对比 在 Prompt Tuning 中，Soft Prompt 和 Hard Prompt 是两种不同的提示方式，用于引导模型执行特定任务。</description>
    </item>
    
    <item>
      <title>Transformer 学习之路 - Tokenizer 处理流程详解</title>
      <link>https://869413421.github.io/post/transformer/tokenizer/</link>
      <pubDate>Fri, 19 Apr 2024 17:35:18 +0800</pubDate>
      
      <guid>https://869413421.github.io/post/transformer/tokenizer/</guid>
      <description>Tokenizer 处理流程详解 在 Transformer 模型中，Tokenizer（分词器）是将原始文本数据转换为模型可接受的输入格式的关键组件。本文将详细解析 Tokenizer 的处理流程，并结合代码示例帮助读者深入理解其工作原理。
1. 分词与词典构建 Step1 分词 Tokenizer 的第一步是将文本数据进行分词。分词的方式可以是按字、按词或按子词（subword）进行。分词的目的是将连续的文本分割成离散的单元，以便后续处理。
from transformers import AutoTokenizer sen = &amp;#34;往者不可谏,来者犹可追&amp;#34; tokenizer = AutoTokenizer.from_pretrained(&amp;#34;uer/roberta-base-finetuned-dianping-chinese&amp;#34;) tokens = tokenizer.tokenize(sen) print(tokens) Step2 构建词典 分词后，Tokenizer 会根据分词结果构建一个词典，将每个词或子词映射到一个唯一的 ID。词典的构建方式取决于是否使用预训练的词向量。如果使用预训练词向量，词典会根据词向量文件进行处理。
# 查看词典详情 print(tokenizer.vocab) print(tokenizer.vocab_size) 2. 数据转换与填充截断 Step3 数据转换 Tokenizer 将分词后的文本序列转换为数字序列，因为神经网络只接受数字序列作为输入。
# 将分词结果转换为 ID 序列 ids = tokenizer.convert_tokens_to_ids(tokens) print(ids) # 将 ID 序列转换回分词结果 tokens = tokenizer.convert_ids_to_tokens(ids) print(tokens) # 将分词序列转换为原始字符串 str_sen = tokenizer.convert_tokens_to_string(tokens) print(str_sen) Step4 数据填充与截断 在以 batch 方式输入模型时，需要对过短的数据进行填充，对过长的数据进行截断，以确保所有数据的长度一致。
# 填充，不足 15 个字会自动填充 0 ids = tokenizer.</description>
    </item>
    
    <item>
      <title>Transformer 学习之路 - 从零开始理解文本分类</title>
      <link>https://869413421.github.io/post/transformer/pipe/</link>
      <pubDate>Fri, 19 Apr 2024 17:35:18 +0800</pubDate>
      
      <guid>https://869413421.github.io/post/transformer/pipe/</guid>
      <description>Transformer 学习之路 - 从零开始理解文本分类 Transformer 模型自 2017 年问世以来，彻底改变了自然语言处理（NLP）领域的格局。它不仅在机器翻译、文本生成等任务中表现出色，还在文本分类、情感分析等任务中展现了强大的能力。本文将带你从零开始理解 Transformer 在文本分类中的应用，并通过代码示例帮助你掌握如何使用 Hugging Face 的 transformers 库构建一个文本分类模型。
1. Transformer 的核心思想 Transformer 模型的核心思想是自注意力机制（Self-Attention）。与传统的循环神经网络（RNN）和卷积神经网络（CNN）不同，Transformer 通过自注意力机制捕捉输入序列中各个位置之间的依赖关系，从而避免了 RNN 在处理长序列时的梯度消失问题。
自注意力机制的计算过程可以简单描述为：对于输入序列中的每个位置，模型计算它与其他位置的关联程度，然后根据这些关联程度加权求和，得到该位置的表示。这种机制使得 Transformer 能够并行处理整个序列，大大提高了计算效率。
2. 文本分类中的 Transformer 文本分类是 NLP 中的一项基础任务，目标是将一段文本分配到预定义的类别中。Transformer 模型在文本分类中的应用通常包括以下几个步骤：
文本预处理：将原始文本转换为模型可以理解的输入格式。 模型加载：加载预训练的 Transformer 模型。 特征提取：通过模型提取文本的特征表示。 分类：将特征表示输入到分类器中进行分类。 3. 代码示例：使用 Hugging Face 进行文本分类 下面我们通过代码示例来详细讲解如何使用 Hugging Face 的 transformers 库进行文本分类。
3.1 加载预训练模型和分词器 首先，我们需要加载一个预训练的 Transformer 模型和对应的分词器。Hugging Face 提供了丰富的预训练模型，我们可以根据需要选择合适的模型。
from transformers import AutoModelForSequenceClassification, AutoTokenizer # 加载预训练的分词器 tokenizer = AutoTokenizer.from_pretrained(&amp;#34;hfl/chinese-macbert-base&amp;#34;) # 保存分词器到本地 tokenizer.save_pretrained(&amp;#34;tokenizer&amp;#34;) # 加载预训练的模型 model = AutoModelForSequenceClassification.</description>
    </item>
    
    <item>
      <title>Transformer 学习之路 - 使用 evaluate 库进行模型评估</title>
      <link>https://869413421.github.io/post/transformer/evaluate/</link>
      <pubDate>Fri, 19 Apr 2024 17:35:18 +0800</pubDate>
      
      <guid>https://869413421.github.io/post/transformer/evaluate/</guid>
      <description>Transformer 学习之路 - 使用 evaluate 库进行模型评估 在深度学习和自然语言处理（NLP）领域，模型的评估是至关重要的一环。无论是训练过程中的监控，还是最终模型的性能对比，评估指标都能帮助我们更好地理解模型的表现。本文将详细介绍如何使用 evaluate 库进行模型评估，并结合代码示例进行讲解。
1. 安装与导入 首先，我们需要安装 evaluate 库。可以通过以下命令进行安装：
!pip install evaluate 安装完成后，导入库：
import evaluate 2. 查看支持的评估函数 evaluate 库提供了丰富的评估函数，涵盖了分类、回归、文本生成等多个领域。我们可以通过以下命令查看支持的评估函数：
evaluate.list_evaluation_modules() 如果你只想查看特定类型的评估函数，比如比较类的评估函数，可以使用以下命令：
evaluate.list_evaluation_modules( module_type=&amp;#34;comparison&amp;#34;, include_community=False, with_details=True) 3. 加载评估函数 接下来，我们可以加载具体的评估函数。以准确率（accuracy）为例：
accuracy = evaluate.load(&amp;#34;accuracy&amp;#34;) 加载完成后，我们可以查看该函数的详细信息：
print(accuracy.description) 4. 评估指标计算 4.1 全局计算 全局计算是指一次性评估所有数据。我们可以通过以下代码计算准确率：
accuracy = evaluate.load(&amp;#34;accuracy&amp;#34;) results = accuracy.compute(references=[0, 1, 2, 0, 1, 2], predictions=[0, 1, 1, 2, 1, 0]) results 4.2 迭代计算 迭代计算是指在数据流中逐步计算评估指标。以下代码展示了如何逐步计算准确率：
accuracy = evaluate.load(&amp;#34;accuracy&amp;#34;) for ref, pred in zip([0,1,0,1], [1,0,0,1]): accuracy.</description>
    </item>
    
    <item>
      <title>Transformer 学习之路 - 向量召回与排序实战</title>
      <link>https://869413421.github.io/post/transformer/retrieval_chatbot2/</link>
      <pubDate>Fri, 19 Apr 2024 17:35:18 +0800</pubDate>
      
      <guid>https://869413421.github.io/post/transformer/retrieval_chatbot2/</guid>
      <description>Transformer 学习之路 - 向量召回与排序实战 在自然语言处理（NLP）领域，Transformer 模型已经成为一种革命性的技术。它不仅广泛应用于机器翻译、文本生成等任务，还在信息检索、问答系统等领域展现了强大的能力。本文将结合代码示例，深入探讨 Transformer 在向量召回与排序中的应用，帮助你理解其技术原理并实际应用。
1. 背景与问题 在问答系统中，如何快速准确地找到与用户问题最相关的答案是一个核心挑战。传统的基于关键词匹配的方法往往无法捕捉语义信息，导致召回效果不佳。而 Transformer 模型通过将文本转化为高维向量，能够更好地表达语义信息，从而实现更精准的召回与排序。
2. 技术原理 2.1 向量召回 向量召回的核心思想是将文本（如问题与答案）映射到高维向量空间，然后通过计算向量之间的相似度来找到最相关的结果。具体步骤如下：
文本向量化：使用预训练的 Transformer 模型（如 Sentence-BERT）将文本转化为固定长度的向量。 向量存储：将生成的向量存储到向量数据库（如 PostgreSQL 的 vector 扩展）。 相似度计算：通过计算查询向量与存储向量的余弦相似度，找到最相关的结果。 2.2 向量排序 在召回的结果中，可能存在多个相关但质量不同的答案。为了进一步提升结果质量，可以使用排序模型对召回结果进行二次排序。排序模型通过计算查询与候选答案的匹配分数，选择最合适的答案。
3. 代码实现 3.1 读取数据集 首先，我们加载一个法律问答数据集（law_faq.csv），其中包含问题（title）和答案（reply）。
import pandas as pd data = pd.read_csv(&amp;#39;law_faq.csv&amp;#39;, encoding=&amp;#39;utf-8&amp;#39;) documents = data[&amp;#39;title&amp;#39;].tolist() print(documents[0:5]) # 打印前5个问题 3.2 加载向量模型 使用 sentence-transformers 库加载预训练的向量模型，并将文本转化为向量。
from sentence_transformers import SentenceTransformer model = SentenceTransformer(&amp;#34;TencentBAC/Conan-embedding-v1&amp;#34;, cache_folder=&amp;#34;cache&amp;#34;) document_vectors = model.encode(documents[0:5], convert_to_numpy=True) print(document_vectors.shape) # 打印向量维度 3.</description>
    </item>
    
    <item>
      <title>Transformer 学习之路 - 因果语言模型训练实例</title>
      <link>https://869413421.github.io/post/transformer/causal_lm/</link>
      <pubDate>Fri, 19 Apr 2024 17:35:18 +0800</pubDate>
      
      <guid>https://869413421.github.io/post/transformer/causal_lm/</guid>
      <description>Transformer 学习之路 - 因果语言模型训练实例 在自然语言处理（NLP）领域，因果语言模型（Causal Language Model）和自回归模型（Autoregressive Model）是生成任务中的核心概念。它们通过利用已生成的内容来预测下一个单词，从而实现连贯的文本输出。本文将深入解析因果语言模型的技术原理，并结合代码示例，带你一步步实现一个完整的训练流程。
什么是因果语言模型？ 因果模型关注的是因果关系，旨在理解一个变量对另一个变量的影响。在 NLP 中，因果语言模型通常指的是模型如何根据先前的输入生成后续的输出。换句话说，模型在生成每个单词时，只依赖于之前生成的单词，而不考虑后续单词。
自回归模型的工作原理 自回归模型是一种生成模型，其中每个输出单词（或 token）的生成依赖于前面生成的单词。这种模型的核心特点是顺序生成，即生成当前单词时只考虑过去的单词。这种结构确保了生成过程的因果性。
例如，生成句子 &amp;quot;今天天气不错，我想去公园。&amp;quot; 的过程如下：
初始输入：&amp;quot;今天天气&amp;quot; 模型生成下一个单词：&amp;quot;不错&amp;quot; 当前输入变为：&amp;quot;今天天气不错&amp;quot; 模型再次生成下一个单词：&amp;quot;我想去公园。&amp;quot; 在这个过程中，模型在生成每个单词时只依赖于之前生成的单词，这体现了因果性和自回归性。
结合因果与自回归 因果自回归模型是一种强大的文本生成工具，广泛应用于对话生成、故事创作等任务。最著名的因果自回归模型是 GPT（Generative Pre-trained Transformer）系列。GPT 模型使用自回归机制生成文本，在输入序列中，模型根据之前的单词生成下一个单词，直到达到指定的长度或遇到结束标记。
代码实现：训练一个因果语言模型 接下来，我们将通过代码示例，详细讲解如何训练一个因果语言模型。
Step 1: 导入相关包 from datasets import Dataset from transformers import AutoModelForCausalLM, AutoTokenizer, DataCollatorForLanguageModeling, Trainer, TrainingArguments Step 2: 加载数据集 ds = Dataset.load_from_disk(&amp;#34;/content/drive/MyDrive/ai-learning/2.NLP Task/07-language_model/wiki_cn_filtered/&amp;#34;) Step 3: 数据预处理 tokenizer = AutoTokenizer.from_pretrained(&amp;#34;Langboat/bloom-389m-zh&amp;#34;) def process_func(examples): contents = [e + tokenizer.eos_token for e in examples[&amp;#34;completion&amp;#34;]] return tokenizer(contents, max_length=384, truncation=True) tokenized_ds = ds.</description>
    </item>
    
    <item>
      <title>Transformer 学习之路 - 基于 T5 的文本摘要</title>
      <link>https://869413421.github.io/post/transformer/summarizatioin/</link>
      <pubDate>Fri, 19 Apr 2024 17:35:18 +0800</pubDate>
      
      <guid>https://869413421.github.io/post/transformer/summarizatioin/</guid>
      <description>Transformer 学习之路 - 基于 T5 的文本摘要 文本摘要是一种自然语言处理（NLP）任务，旨在将长文本信息提炼为简洁的摘要，同时保留关键内容和语义。本文将深入探讨基于 T5 模型的文本摘要技术，涵盖其原理、实现步骤以及实际应用。
文本摘要的分类 文本摘要主要分为两类：
抽取式摘要：直接从原文中挑选出关键句子或短语，并组合成摘要。这种方法不会生成新内容，但效果依赖于原文的句子组织。
生成式摘要：使用生成算法基于原文创建新的句子和段落，可以进行词语重组和总结，适合灵活、内容丰富的摘要任务。生成式摘要往往能提供更自然、流畅的摘要。
序列到序列（Seq2Seq）模型 序列到序列（Seq2Seq）模型是解决生成式摘要任务的一种常用架构，适用于输入和输出都是序列的数据，例如机器翻译、文本摘要、对话生成等。
Seq2Seq 基本结构 Seq2Seq模型一般由编码器（Encoder）和解码器（Decoder）组成：
编码器：接收输入文本的序列，将其转换成隐含状态表示。
解码器：根据编码器的输出和自身生成的上一步输出，逐步生成目标序列。
Attention机制 在处理长文本摘要时，Seq2Seq模型的注意力机制（Attention）非常关键。Attention允许解码器在生成摘要的过程中动态关注输入文本中的重要部分，使摘要更精准，内容的连贯性更好。
常见的Seq2Seq模型 RNN-based Seq2Seq：早期的Seq2Seq模型多基于循环神经网络（RNN）或长短期记忆（LSTM）网络，但这些模型在长序列文本处理中性能不足。
Transformer-based Seq2Seq：目前最主流的是基于Transformer架构的Seq2Seq模型，如BERT、GPT等模型的变种。Transformer使用自注意力机制，可以在训练中更有效地捕获全局上下文，效果显著优于传统RNN模型。
经典文本摘要模型 BERTSUM：基于BERT的抽取式模型，设计了适合摘要任务的文本表示方式，适合抽取式摘要。
T5（Text-to-Text Transfer Transformer）：生成式Seq2Seq模型，能将各种文本处理任务（包括摘要）转换为统一的输入-输出格式。
BART：一种变换编码-解码的模型结构，可以针对文本摘要任务进行微调，擅长处理不规则的输入文本。
基于 T5 的文本摘要实现 安装依赖 !pip install datasets rouge_chinese 导入相关包 import torch from datasets import Dataset from transformers import AutoTokenizer, AutoModelForSeq2SeqLM, DataCollatorForSeq2Seq, Seq2SeqTrainer, Seq2SeqTrainingArguments 加载数据集 ds = Dataset.load_from_disk(&amp;#34;/content/drive/MyDrive/ai-learning/2.NLP Task/08-text_summarization/nlpcc_2017/&amp;#34;) ds = ds.train_test_split(100, seed=42) 数据预处理 tokenizer = AutoTokenizer.from_pretrained(&amp;#34;Langboat/mengzi-t5-base&amp;#34;) def process_func(examples): contents = [&amp;#34;摘要生成: \n&amp;#34; + e for e in examples[&amp;#34;content&amp;#34;]] inputs = tokenizer(contents, max_length=384, truncation=True) labels = tokenizer(text_target=examples[&amp;#34;title&amp;#34;], max_length=64, truncation=True) inputs[&amp;#34;labels&amp;#34;] = labels[&amp;#34;input_ids&amp;#34;] return inputs tokenized_ds = ds.</description>
    </item>
    
    <item>
      <title>Transformer 学习之路 - 基于滑动窗口策略的机器阅读理解任务实现</title>
      <link>https://869413421.github.io/post/transformer/mrc_slide_version/</link>
      <pubDate>Fri, 19 Apr 2024 17:35:18 +0800</pubDate>
      
      <guid>https://869413421.github.io/post/transformer/mrc_slide_version/</guid>
      <description>Transformer 学习之路 - 基于滑动窗口策略的机器阅读理解任务实现 在自然语言处理（NLP）领域，机器阅读理解（Machine Reading Comprehension, MRC）是一个重要的任务，其目标是通过理解给定的上下文文本，回答用户提出的问题。本文将深入探讨如何利用 Transformer 模型，并结合滑动窗口策略来实现这一任务。
1. 背景与问题 机器阅读理解任务的核心挑战在于如何有效地处理长文本。传统的 Transformer 模型由于输入长度的限制，往往无法直接处理过长的上下文文本。为了解决这一问题，滑动窗口策略应运而生。通过将长文本分割成多个片段，并逐步滑动窗口进行处理，模型能够在不损失信息的情况下处理长文本。
2. 滑动窗口策略的实现 滑动窗口策略的核心思想是将长文本分割成多个重叠的片段，每个片段都包含一部分上下文信息。这样，模型可以在每个片段上独立地进行处理，最终将所有片段的结果进行整合。
2.1 数据预处理 首先，我们需要对数据进行预处理。以下代码展示了如何加载数据集，并使用滑动窗口策略对数据进行编码：
from datasets import load_dataset from transformers import AutoTokenizer # 加载数据集 datasets = load_dataset(&amp;#34;cmrc2018&amp;#34;, cache_dir=&amp;#34;data&amp;#34;) # 加载预训练的 tokenizer tokenizer = AutoTokenizer.from_pretrained(&amp;#34;hfl/chinese-macbert-base&amp;#34;) # 对数据进行编码，使用滑动窗口策略 tokenized_examples = tokenizer( text=sample_dataset[&amp;#34;context&amp;#34;], text_pair=sample_dataset[&amp;#34;question&amp;#34;], return_offsets_mapping=True, max_length=384, return_overflowing_tokens=True, # 设置滑动窗口策略 stride=128, # 覆盖策略，每次移动128个字符 truncation_strategy=&amp;#34;only_second&amp;#34;, ) 2.2 滑动窗口的处理 在处理滑动窗口时，我们需要确保每个片段都能够正确地定位答案的位置。以下代码展示了如何在滑动窗口的每个片段中定位答案的起始和结束位置：
for idx, _ in enumerate(sample_mapping): answer = sample_dataset[&amp;#34;answers&amp;#34;][sample_mapping[idx]] start_char = answer[&amp;#34;answer_start&amp;#34;][0] end_char = start_char + len(answer[&amp;#34;text&amp;#34;][0]) # 定位答案在token中的起始位置和结束位置 context_start = tokenized_examples.</description>
    </item>
    
    <item>
      <title>Transformer 学习之路 - 掩码语言模型训练与实战</title>
      <link>https://869413421.github.io/post/transformer/masked_lm/</link>
      <pubDate>Fri, 19 Apr 2024 17:35:18 +0800</pubDate>
      
      <guid>https://869413421.github.io/post/transformer/masked_lm/</guid>
      <description>Transformer 学习之路 - 掩码语言模型训练与实战 掩码语言模型（Masked Language Model，MLM）是自然语言处理（NLP）领域中的一项重要技术，广泛应用于文本生成、文本分类、问答系统等任务。本文将从原理、应用及实战训练三个方面，深入解析 MLM 技术，并结合代码示例帮助读者理解并应用。
掩码语言模型的工作原理 1. 输入处理 MLM 的核心思想是通过随机掩盖句子中的部分单词，训练模型来预测这些被掩盖的单词。例如，给定句子 &amp;ldquo;The cat sat on the mat.&amp;quot;，模型可能会将其处理为 &amp;ldquo;The cat [MASK] on the mat.&amp;quot;，然后尝试预测被掩盖的单词 &amp;ldquo;sat&amp;rdquo;。
2. 模型训练 模型接收包含掩码的句子作为输入，并输出预测的单词。通过这种方式，模型能够学习到单词之间的上下文关系和语义。
3. 损失计算 模型的预测结果与实际被掩盖的单词进行比较，计算损失函数，并通过反向传播优化模型参数。
掩码语言模型的应用 MLM 在许多 NLP 任务中都有广泛应用：
文本生成：生成缺失的文本部分。 文本分类：为文本赋予标签。 问答系统：根据上下文回答问题。 最著名的 MLM 模型是 BERT（Bidirectional Encoder Representations from Transformers），它通过 MLM 技术进行训练，能够有效捕捉语言的上下文信息。
实战训练：从数据到模型 接下来，我们将通过代码示例，一步步实现一个 MLM 模型的训练过程。
Step1 导入相关包 from datasets import load_dataset, Dataset from transformers import AutoTokenizer, AutoModelForMaskedLM, DataCollatorForLanguageModeling, TrainingArguments, Trainer Step2 加载数据集 ds = Dataset.</description>
    </item>
    
    <item>
      <title>Transformer 学习之路 - 数据处理与加载实战</title>
      <link>https://869413421.github.io/post/transformer/datasets/</link>
      <pubDate>Fri, 19 Apr 2024 17:35:18 +0800</pubDate>
      
      <guid>https://869413421.github.io/post/transformer/datasets/</guid>
      <description>Transformer 学习之路 - 数据处理与加载实战 在 Transformer 的学习过程中，数据处理是一个至关重要的环节。为了高效地加载和处理数据，我们通常会使用 datasets 库。本文将深入探讨如何使用 datasets 库进行数据加载、处理、映射和保存，并结合代码示例进行详细讲解。
1. datasets 库的基本使用 datasets 库是 Hugging Face 提供的一个强大工具，专门用于加载和处理各种数据集。它可以轻松地从线上或线下加载数据集，并提供了丰富的数据处理功能。
1.1 安装与导入 首先，我们需要安装 datasets 库：
!pip install datasets 然后导入库：
from datasets import * 1.2 加载线上数据集 使用 load_dataset 函数可以轻松加载线上数据集。例如，加载一个中文新闻标题数据集：
datasets = load_dataset(&amp;#34;madao33/new-title-chinese&amp;#34;) datasets 1.3 加载特定任务的数据集 有时我们只需要加载数据集中的某一项任务数据。例如，加载 super_glue 数据集中的 boolq 任务：
boolq_dataset = load_dataset(&amp;#34;super_glue&amp;#34;, &amp;#34;boolq&amp;#34;) boolq_dataset 2. 数据集的切分与查看 在数据处理过程中，我们经常需要对数据集进行切分和查看。datasets 库提供了多种方式来实现这些操作。
2.1 切分数据集 我们可以通过 split 参数来切分数据集。例如，只加载训练数据：
dataset = load_dataset(&amp;#34;madao33/new-title-chinese&amp;#34;, split=&amp;#34;train&amp;#34;) dataset 还可以加载训练数据的特定部分，例如下标为 10 到 100 的数据：</description>
    </item>
    
    <item>
      <title>Transformer 学习之路 - 数据集加载与预处理</title>
      <link>https://869413421.github.io/post/transformer/datasets-checkpoint/</link>
      <pubDate>Fri, 19 Apr 2024 17:35:18 +0800</pubDate>
      
      <guid>https://869413421.github.io/post/transformer/datasets-checkpoint/</guid>
      <description>Transformer 学习之路 - 数据集加载与预处理 在 Transformer 模型的训练过程中，数据集的加载和预处理是至关重要的一步。本文将详细介绍如何使用 datasets 库来加载、处理和管理数据集，确保数据能够高效地输入到模型中。
1. 安装与导入 datasets 库 首先，我们需要安装并导入 datasets 库。这个库提供了丰富的功能，可以轻松地加载和处理各种数据集。
!pip install datasets from datasets import * 2. 加载数据集 datasets 库支持从线上和线下加载数据集。无论是公开的数据集还是自定义的数据集，都可以通过简单的代码实现加载。
2.1 加载线上数据集 datasets = load_dataset(&amp;#34;madao33/new-title-chinese&amp;#34;) datasets 2.2 加载特定任务的数据集 boolq_dataset = load_dataset(&amp;#34;super_glue&amp;#34;, &amp;#34;boolq&amp;#34;) boolq_dataset 3. 数据集切分 在实际应用中，我们通常需要将数据集划分为训练集、验证集和测试集。datasets 库提供了多种切分方式，满足不同的需求。
3.1 加载部分数据 # 只加载训练数据 dataset = load_dataset(&amp;#34;madao33/new-title-chinese&amp;#34;, split=&amp;#34;train&amp;#34;) dataset # 只加载训练数据的10到100下标的数据 dataset = load_dataset(&amp;#34;madao33/new-title-chinese&amp;#34;, split=&amp;#34;train[10:100]&amp;#34;) dataset # 取训练集中后50%数据 dataset = load_dataset(&amp;#34;madao33/new-title-chinese&amp;#34;, split=&amp;#34;train[:50%]&amp;#34;) dataset # 先取后50%再取前50% dataset = load_dataset(&amp;#34;madao33/new-title-chinese&amp;#34;, split=[&amp;#34;train[:50%]&amp;#34;, &amp;#34;train[50%:]&amp;#34;]) dataset 4.</description>
    </item>
    
    <item>
      <title>Transformer 学习之路 - 文本分类实战</title>
      <link>https://869413421.github.io/post/transformer/classification_demo/</link>
      <pubDate>Fri, 19 Apr 2024 17:35:18 +0800</pubDate>
      
      <guid>https://869413421.github.io/post/transformer/classification_demo/</guid>
      <description>Transformer 学习之路 - 文本分类实战 在这篇文章中，我们将通过一个完整的文本分类任务，深入解析 Transformer 技术的应用。我们将从数据集的准备开始，逐步完成模型的训练、评估和预测，并结合代码示例详细讲解每一步的实现细节。
1. 准备数据集 1.1 下载并保存数据集 首先，我们需要一个用于训练和测试的数据集。这里我们使用 ChnSentiCorp_htl_all.csv 数据集，它包含了酒店评论及其对应的情感标签（正面或负面）。
import os import requests # 创建 &amp;#39;dataset&amp;#39; 文件夹（如果不存在） os.makedirs(&amp;#34;dataset&amp;#34;, exist_ok=True) # 文件 URL url = &amp;#34;https://github.com/SophonPlus/ChineseNlpCorpus/raw/master/datasets/ChnSentiCorp_htl_all/ChnSentiCorp_htl_all.csv&amp;#34; # 发出 GET 请求获取文件内容 response = requests.get(url) # 文件保存路径 file_path = os.path.join(&amp;#34;dataset&amp;#34;, &amp;#34;ChnSentiCorp_htl_all.csv&amp;#34;) # 将内容保存到指定目录的文件中 with open(file_path, &amp;#34;wb&amp;#34;) as file: file.write(response.content) 1.2 查看数据集 下载完成后，我们可以使用 pandas 来查看数据集的基本信息。
import pandas as pd # 读取保存在 &amp;#39;dataset&amp;#39; 文件夹中的 CSV 文件 df = pd.read_csv(file_path) # 查看表头（列名） print(&amp;#34;表头（列名）：&amp;#34;) print(df.</description>
    </item>
    
    <item>
      <title>Transformer 学习之路 - 文本相似度实例</title>
      <link>https://869413421.github.io/post/transformer/cross_model/</link>
      <pubDate>Fri, 19 Apr 2024 17:35:18 +0800</pubDate>
      
      <guid>https://869413421.github.io/post/transformer/cross_model/</guid>
      <description>Transformer 学习之路 - 文本相似度实例 Transformer 模型自 2017 年提出以来，已经成为了自然语言处理（NLP）领域的核心技术之一。它不仅改变了机器翻译、文本生成等任务的处理方式，还在文本相似度计算、情感分析等任务中展现了强大的能力。本文将基于 Transformer 技术，详细讲解如何实现文本相似度计算，并结合代码示例进行深入解析。
1. 背景与问题 文本相似度计算是 NLP 中的一个重要任务，它旨在衡量两段文本在语义上的相似程度。传统的文本相似度计算方法通常基于词袋模型或 TF-IDF，但这些方法无法捕捉到文本的深层语义信息。Transformer 模型通过自注意力机制（Self-Attention）和多层编码器，能够更好地理解文本的上下文关系，从而在文本相似度计算中取得了显著的效果提升。
2. 环境准备 在开始之前，我们需要安装一些必要的 Python 库，包括 transformers、datasets 和 evaluate。这些库将帮助我们加载预训练模型、处理数据集以及评估模型性能。
!pip install evaluate datasets 3. 加载数据集 我们使用 datasets 库加载一个 JSON 格式的文本相似度数据集。该数据集包含两段文本及其相似度标签。
from datasets import load_dataset dataset = load_dataset(&amp;#34;json&amp;#34;, data_files=&amp;#34;/content/drive/MyDrive/ai-learning/2.NLP Task/05-sentence_similarity/train_pair_1w.json&amp;#34;, split=&amp;#34;train&amp;#34;) 为了训练和评估模型，我们将数据集划分为训练集和测试集。
datasets = dataset.train_test_split(test_size=0.2) 4. 数据预处理 在将数据输入模型之前，我们需要对文本进行预处理。这里我们使用 AutoTokenizer 加载一个中文预训练模型的分词器，并对文本进行分词和截断。
from transformers import AutoTokenizer tokenizer = AutoTokenizer.from_pretrained(&amp;#34;hfl/chinese-macbert-base&amp;#34;) def preprocess_function(examples): tokenized_examples = tokenizer(examples[&amp;#34;sentence1&amp;#34;], examples[&amp;#34;sentence2&amp;#34;], truncation=True, max_length=128) tokenized_examples[&amp;#34;labels&amp;#34;] = [float(example) for example in examples[&amp;#34;label&amp;#34;]] return tokenized_examples tokenized_datasets = datasets.</description>
    </item>
    
    <item>
      <title>Transformer 学习之路 - 文本相似度实战</title>
      <link>https://869413421.github.io/post/transformer/dual_model/</link>
      <pubDate>Fri, 19 Apr 2024 17:35:18 +0800</pubDate>
      
      <guid>https://869413421.github.io/post/transformer/dual_model/</guid>
      <description>Transformer 学习之路 - 文本相似度实战 Transformer 模型在自然语言处理（NLP）领域取得了巨大的成功，尤其是在文本相似度任务中表现尤为突出。本文将结合代码示例，深入解析如何利用 Transformer 技术实现文本相似度计算，并详细分析其背后的技术原理。
1. 背景与问题 文本相似度任务是 NLP 中的一项基础任务，旨在衡量两段文本在语义上的相似程度。常见的应用场景包括问答系统、信息检索、文本匹配等。传统的文本相似度计算方法（如 TF-IDF、余弦相似度等）往往难以捕捉文本的深层语义信息，而 Transformer 模型通过自注意力机制（Self-Attention）能够更好地理解文本的上下文关系，从而提升相似度计算的准确性。
2. 技术原理 2.1 Transformer 模型 Transformer 模型的核心是自注意力机制，它能够动态地计算输入序列中每个词与其他词的相关性，从而捕捉长距离依赖关系。Transformer 模型由编码器（Encoder）和解码器（Decoder）两部分组成，但在文本相似度任务中，我们通常只使用编码器部分。
2.2 文本相似度计算 在文本相似度任务中，我们需要将两段文本分别输入到 Transformer 模型中，获取它们的向量表示，然后通过余弦相似度等方法来衡量它们的相似程度。为了实现这一目标，我们可以使用预训练的 Transformer 模型（如 BERT）作为基础模型，并在其基础上进行微调。
3. 代码实现 3.1 环境准备 首先，我们需要安装必要的库，并加载数据集。
!pip install evaluate datasets import sys from google.colab import drive drive.mount(&amp;#39;/content/drive&amp;#39;) sys.path.append(&amp;#39;/content/drive/MyDrive/ai-learning/2.NLP Task/05-sentence_similarity&amp;#39;) 3.2 加载数据集 我们使用 datasets 库加载一个包含句子对和标签的数据集。
from datasets import load_dataset dataset = load_dataset(&amp;#34;json&amp;#34;, data_files=&amp;#34;/content/drive/MyDrive/ai-learning/2.NLP Task/05-sentence_similarity/train_pair_1w.json&amp;#34;, split=&amp;#34;train&amp;#34;) datasets = dataset.train_test_split(test_size=0.2) 3.3 数据预处理 使用预训练的 hfl/chinese-macbert-base 分词器对文本进行预处理。</description>
    </item>
    
    <item>
      <title>Transformer 学习之路 - 机器阅读理解任务实现</title>
      <link>https://869413421.github.io/post/transformer/mrc_simple_version-checkpoint/</link>
      <pubDate>Fri, 19 Apr 2024 17:35:18 +0800</pubDate>
      
      <guid>https://869413421.github.io/post/transformer/mrc_simple_version-checkpoint/</guid>
      <description>Transformer 学习之路 - 机器阅读理解任务实现 在自然语言处理（NLP）领域，机器阅读理解（MRC）是一个核心任务，它要求模型能够理解文本内容并回答相关问题。近年来，基于 Transformer 的模型在这一任务上取得了显著进展。本文将深入解析如何利用 Transformer 技术实现机器阅读理解任务，并结合代码示例详细讲解技术原理及其应用。
1. 导入相关包 首先，我们需要导入必要的 Python 包。这些包包括 datasets 用于加载数据集，transformers 用于加载预训练模型和分词器，以及 Trainer 和 TrainingArguments 用于训练模型。
from datasets import load_dataset from transformers import AutoTokenizer, AutoModelForQuestionAnswering, Trainer, TrainingArguments, DefaultDataCollator 2. 加载数据集 接下来，我们加载一个公开的中文机器阅读理解数据集 cmrc2018。这个数据集包含了大量的上下文、问题以及对应的答案。
datasets = load_dataset(&amp;#34;cmrc2018&amp;#34;, cache_dir=&amp;#34;data&amp;#34;) datasets 我们可以查看数据集中的第一个样本，了解数据的结构。
datasets[&amp;#34;train&amp;#34;][0] 3. 数据预处理 在将数据输入模型之前，我们需要对数据进行预处理。首先，我们加载一个预训练的分词器 hfl/chinese-macbert-base，用于将文本转换为模型可以理解的 token。
tokenizer = AutoTokenizer.from_pretrained(&amp;#34;hfl/chinese-macbert-base&amp;#34;) tokenizer 然后，我们对数据进行编码。编码后的数据将包含 input_ids、attention_mask、token_type_ids 等信息。
tokenized_examples = tokenizer( text=sample_dataset[&amp;#34;context&amp;#34;], text_pair=sample_dataset[&amp;#34;question&amp;#34;], return_offsets_mapping=True, truncation=True, max_length=512, padding=&amp;#34;max_length&amp;#34;, truncation_strategy=&amp;#34;only_second&amp;#34;, ) tokenized_examples.keys() offset_mapping 是一个重要的字段，它记录了每个 token 在原始文本中的位置。我们可以通过这个字段来定位答案在 token 中的起始和结束位置。</description>
    </item>
    
    <item>
      <title>Transformer 学习之路 - 检索机器人实战</title>
      <link>https://869413421.github.io/post/transformer/retrieval_chatbot/</link>
      <pubDate>Fri, 19 Apr 2024 17:35:18 +0800</pubDate>
      
      <guid>https://869413421.github.io/post/transformer/retrieval_chatbot/</guid>
      <description>Transformer 学习之路 - 检索机器人实战 在自然语言处理（NLP）领域，Transformer 模型已经成为许多任务的核心工具。今天，我们将通过一个实际案例——构建一个检索机器人，来深入探讨 Transformer 的应用。这个机器人能够根据用户的问题，从 FAQ 数据集中检索出最相关的答案。
1. 读取 FAQ 数据 首先，我们需要准备好 FAQ 数据集。这里我们使用了一个法律相关的 FAQ 数据集 law_faq.csv。
import pandas as pd data = pd.read_csv(&amp;#34;/content/drive/MyDrive/ai-learning/2.NLP Task/06-retrieval_chatbot/law_faq.csv&amp;#34;) data.head() 为什么需要这一步？ FAQ 数据集是我们机器人的“知识库”，它包含了常见问题及其对应的答案。读取数据后，我们才能进行后续的处理和检索。
2. 加载预训练模型 接下来，我们需要加载预训练的 Transformer 模型。这里我们使用了 DualModel 和 AutoTokenizer。
from dual_model import DualModel from transformers import AutoTokenizer dual_model = DualModel.from_pretrained(dual_model_path) dual_model.cuda() dual_model.eval() tokenizer = AutoTokenizer.from_pretrained(&amp;#34;hfl/chinese-macbert-base&amp;#34;) 为什么需要这一步？ 预训练模型已经在大规模数据上进行了训练，能够捕捉到丰富的语言特征。加载这些模型后，我们可以直接使用它们来对文本进行编码，而不需要从头开始训练。
3. 将问题编码转换为向量 为了进行检索，我们需要将 FAQ 中的问题转换为向量表示。这里我们使用了 DualModel 的 BERT 模型来生成向量。
import torch from tqdm import tqdm questions = data[&amp;#34;title&amp;#34;].</description>
    </item>
    
    <item>
      <title>Transformer 学习之路 - 法律标题文本分类实战</title>
      <link>https://869413421.github.io/post/transformer/classification/</link>
      <pubDate>Fri, 19 Apr 2024 17:35:18 +0800</pubDate>
      
      <guid>https://869413421.github.io/post/transformer/classification/</guid>
      <description>Transformer 学习之路 - 法律标题文本分类实战 Transformer 模型自 2017 年问世以来，迅速成为自然语言处理（NLP）领域的基石。它通过自注意力机制（Self-Attention）实现了对长距离依赖的高效建模，广泛应用于文本分类、机器翻译、问答系统等任务。本文将以法律标题文本分类为例，详细讲解如何使用 Transformer 技术解决实际问题。
1. 项目背景与目标 在法律领域，文本分类是一项基础但重要的任务。例如，将法律标题分类为“合同纠纷”、“知识产权”、“劳动法”等类别，有助于快速归档和检索。我们的目标是利用 Transformer 模型，实现法律标题的自动分类。
2. 环境准备与数据加载 首先，我们需要安装必要的 Python 库，并加载数据集。以下是代码示例：
! pip install transformers datasets evaluate peft accelerate gradio optimum sentencepiece ! pip install jupyterlab scikit-learn pandas matplotlib tensorboard nltk rouge import evaluate from datasets import load_dataset from transformers import AutoModelForSequenceClassification, AutoTokenizer, TrainingArguments, Trainer, DataCollatorWithPadding # 加载数据集 dataset = load_dataset(&amp;#34;csv&amp;#34;, data_files=&amp;#34;./data.csv&amp;#34;, split=&amp;#34;train&amp;#34;, encoding=&amp;#34;utf-8&amp;#34;) dataset = dataset.filter(lambda x: x[&amp;#34;title&amp;#34;] is not None) 3.</description>
    </item>
    
    <item>
      <title>Transformer 学习之路 - 深入解析 Pipeline 的实现与应用</title>
      <link>https://869413421.github.io/post/transformer/1/</link>
      <pubDate>Fri, 19 Apr 2024 17:35:18 +0800</pubDate>
      
      <guid>https://869413421.github.io/post/transformer/1/</guid>
      <description>Transformer 学习之路 - 深入解析 Pipeline 的实现与应用 在自然语言处理（NLP）领域，Transformer 模型已经成为了主流。而 Hugging Face 的 transformers 库为我们提供了简单易用的工具，尤其是 Pipeline，它让我们能够轻松地完成各种 NLP 任务。本文将深入探讨 Pipeline 的实现原理及其应用，并结合代码示例进行详细讲解。
什么是 Pipeline？ Pipeline 是 Hugging Face transformers 库中的一个高级 API，它封装了模型加载、数据预处理、模型推理和结果后处理等步骤，使得我们能够通过简单的几行代码完成复杂的 NLP 任务。无论是文本分类、问答系统，还是零样本图片检测，Pipeline 都能轻松应对。
Pipeline 支持的任务类型 首先，我们可以通过以下代码查看 Pipeline 支持的任务类型：
from transformers.pipelines import SUPPORTED_TASKS from pprint import pprint pprint(SUPPORTED_TASKS.keys()) 这些任务包括但不限于文本分类、问答、文本生成、翻译等。每个任务类型都有对应的预训练模型和数据处理流程。
Pipeline 的创建与使用 创建默认的 Pipeline 我们可以通过指定任务类型来创建一个默认的 Pipeline。例如，创建一个文本分类的 Pipeline：
from transformers import pipeline pipe = pipeline(&amp;#34;text-classification&amp;#34;) 这个 Pipeline 默认使用的是英文模型，我们可以直接对文本进行分类：
pipe([&amp;#34;very good!&amp;#34;, &amp;#34;vary bad!&amp;#34;]) 使用中文模型 如果我们想使用中文模型，可以指定模型名称：
pipe = pipeline(&amp;#34;text-classification&amp;#34;, model=&amp;#34;uer/roberta-base-finetuned-dianping-chinese&amp;#34;) 然后对中文文本进行分类：</description>
    </item>
    
    <item>
      <title>Transformer 学习之路 - 深入解析模型架构与应用</title>
      <link>https://869413421.github.io/post/transformer/model/</link>
      <pubDate>Fri, 19 Apr 2024 17:35:18 +0800</pubDate>
      
      <guid>https://869413421.github.io/post/transformer/model/</guid>
      <description>Transformer 学习之路 - 深入解析模型架构与应用 Transformer 模型自 2017 年提出以来，已经成为自然语言处理（NLP）领域的基石。它通过注意力机制（Attention Mechanism）实现了对序列数据的强大建模能力。本文将从模型类型、模型头（Model Head）的作用以及模型的加载与调用等方面，深入解析 Transformer 技术。
1. 模型的类型 1.1 Encoder-only 模型（如 BERT） 原理：
Encoder-only 模型使用双向注意力机制，能够同时关注句子中每个词的前后文，从而理解词语在不同上下文中的含义。它就像在读句子时，同时关注所有词，理解它们彼此的关系。
工作方式：
输入的句子会被编码为数字表示，模型通过“关注”句子中的所有词，理解它们的全局语境，最后输出一个表示每个词含义的向量。这些向量可以用于各种任务，比如分类、命名实体识别（NER）。
应用场景：
文本分类、命名实体识别（NER）、问答系统中的句子匹配。
总结：
Encoder-only 模型擅长理解句子的意思，因为它能“看两边”（上下文）。
1.2 Decoder-only 模型（如 GPT） 原理：
Decoder-only 模型使用单向注意力机制，只能从前往后看，就像我们逐字写文章一样。模型根据前面出现的词来生成下一个词。这种“自回归”方式让它在生成新词时，只能依赖之前生成的内容。
工作方式：
输入的文本被编码为向量，然后模型每次根据前面的词生成下一个词，直到生成完整句子或段落。每生成一个词，它就会把这个词加入到上下文中，继续生成下一个。
应用场景：
文本生成、对话生成、语言建模。
总结：
Decoder-only 模型擅长生成句子，通过“接龙”方式，每次只看前面的词，生成后续内容。
1.3 Encoder-Decoder 模型（如 T5, BART） 原理：
Encoder-Decoder 模型结合了 Encoder 和 Decoder 的优点，先用编码器理解输入文本，再用解码器生成输出文本。它像是翻译任务：先理解原文，再生成目标语言的翻译。
工作方式：
编码器负责理解输入文本，把它转化为一系列向量表示。 解码器根据编码器的输出，逐步生成新的句子，比如翻译、摘要或生成其他序列。 应用场景：
机器翻译、文本摘要、自动问答系统。
总结：
Encoder-Decoder 模型擅长转换序列，比如把英文翻译成中文，因为它既能理解又能生成。
1.4 多模态模型（如 CLIP） 原理：
多模态模型能同时处理文本和图像。它把图像和文本都转化为向量，然后通过比较这些向量的相似度，来判断文本和图像是否匹配。
工作方式：
图像部分：模型把图像分成小块，分别处理这些小块的信息，再把它们组合成一个整体表示。 文本部分：模型用类似 BERT 的编码器来处理文本，生成文本的表示。 最后，它比较图像和文本的表示，判断它们是否相关。 应用场景：</description>
    </item>
    
    <item>
      <title>基于关联规则实现的智能推荐算法</title>
      <link>https://869413421.github.io/post/%E5%9F%BA%E4%BA%8E%E5%85%B3%E8%81%94%E8%A7%84%E5%88%99%E5%AE%9E%E7%8E%B0%E7%9A%84%E6%99%BA%E8%83%BD%E6%8E%A8%E8%8D%90%E7%AE%97%E6%B3%95/%E5%9F%BA%E4%BA%8E%E5%85%B3%E8%81%94%E8%A7%84%E5%88%99%E5%AE%9E%E7%8E%B0%E7%9A%84%E6%99%BA%E8%83%BD%E6%8E%A8%E8%8D%90%E7%AE%97%E6%B3%95/</link>
      <pubDate>Fri, 19 Apr 2024 17:35:18 +0800</pubDate>
      
      <guid>https://869413421.github.io/post/%E5%9F%BA%E4%BA%8E%E5%85%B3%E8%81%94%E8%A7%84%E5%88%99%E5%AE%9E%E7%8E%B0%E7%9A%84%E6%99%BA%E8%83%BD%E6%8E%A8%E8%8D%90%E7%AE%97%E6%B3%95/%E5%9F%BA%E4%BA%8E%E5%85%B3%E8%81%94%E8%A7%84%E5%88%99%E5%AE%9E%E7%8E%B0%E7%9A%84%E6%99%BA%E8%83%BD%E6%8E%A8%E8%8D%90%E7%AE%97%E6%B3%95/</guid>
      <description>基于关联规则实现的智能推荐算法 在最近工作上偶然的发现了接触到关联规则这项技术，了解到这个算法适用于实现智能推荐算法，所以打算对其深入了解一下，觊觎一下CURD仔没曾看见过的算法世界。 作为一个偏向业务的程序员，我们最常接触的到智能推荐的应用场景无疑是商城中的推荐商品，以及咨询中的咨询推荐。
常见的推荐系统分类有：
基于应用领域: 电子商务/社交好友推荐等 基于设计思想: 基于协同过滤的推荐等 基于使用数据: 基于用户标签的推荐等 比如在商品详情页下推荐类似的商品，在新闻详情页下推送类似的新闻。
关联规则是什么？ 关联规则通常指的是关联规则学习（Association Rule Learning），这是数据挖掘和机器学习领域的一个技术，用于发现数据集中项之间的有趣关系。 关联规则主要用于在大规模数据集中识别项之间的频繁关联或规律，常用于市场篮子分析、商品推荐等应用场景。最常见的关联规则算法是 Apriori 和 FP-growth。
从上面的解析我们可以得出，我们可以使用关联规则，商品和商品之间的关系。比如在用户的订单中经常会出现牛奶和面包这两件商品，那么这时候就会形成一个关联规则，由此我们可以推断出购买牛奶的用户也极大的 几率会去购买面包。
关联规则的三种关系 当我们在计算出商品的关联规则时，我们需要了解到这些规则中存在三种关系。
强关联：强关联的商品彼此间会互相存在，比如商品A剃须刀，商品B剃须膏。如果购买了剃须膏就必须购买剃须刀，购买了剃须刀就比徐购买剃须刀。这种关系是双向的，称之为双向关联。单向关联，比如香烟和打火机，购买了香烟的人可能需要购买打火机，但是购买了打火机的人不一定需要购买香烟。 弱关联：两个关联程度不高的商品，两种可能存在关联的商品，可以考虑将他们摆放一起，如果后续关联度提高，那么意味着他们会存在强关联关系，目前关联度不高可能是因为陈列原因造成。 排斥：指两个商品不会同时出现。 商品关联分析三度 支持度 支持度 = （同时包含商品A和商品B的订单总数） / 总订单数 * 100% 支持度主要表示，在所有交易中出现关联商品的概率。即是有多少用户会同时购买关联的商品
可信度 可信度 = （同时包含商品A和商品B的订单总数） / 包含商品A的总订单数 * 100% 表示在购买了A商品的订单中有多少包含了商品B
提升度 提升度 = 可信度 / 商品B在总订单中出现的概率 * 100 提升度用于表示商品A对商品B销量提升的影响
可以这样理解： 支持度代表这组关联商品的份额是否够大 置信度(可信度)代表关联度的强弱 而提升度则是看该关联规则是否有利用价值和值得推广，用了(客户购买后推荐)比没用(客户自然而然的购买)要提高多少。
基础数据预览 下面我们导入实现算法所需要的数据，以及取出前十五畅销的商品
import pandas as pd # 基础绘图库 import matplotlib.pyplot as plt import seaborn as sns # %matplotlib inline # 各种细节配置如 文字大小，图例文字等杂项 large = 22 med = 16 small = 12 params = {&amp;#39;axes.</description>
    </item>
    
    <item>
      <title>Redis（底层和应用）</title>
      <link>https://869413421.github.io/post/redis/</link>
      <pubDate>Tue, 07 Feb 2023 17:06:10 +0800</pubDate>
      
      <guid>https://869413421.github.io/post/redis/</guid>
      <description>参考 https://mp.weixin.qq.com/s/_W9ny6l3-JqQ_SWm9ACtqg
Redis的数据类型 String 用途 简单的key-value储存 setnx key value 配合set ex 实现分布式锁 计数器（原子性） 分布式全局唯一ID 底层 C语言中的String用char[]数组表示，源码中的用SDS封装char数组，这是Redis最少的存储单元，一个SDS可以最大存储512兆信息 Redis对SDS再次封装生成了RedisObject，主要有两个核心作用 声明存储的是那种类型的数据 存储指向SDS的指针 当你执行set key value 时，其实redis会创建两个RedisObject 对象，一个是key的redisObject,一个是value的RedisObject，并且指定type为REDIS_STRING，而SDS分别存储key的值，和value的值。 Redis底层对SDS进行了优化 SDS修改后大小&amp;gt;1M时，系统会进行一个空间预分配 SDS是惰性释放空间的，不会马上释放内存，下次进行写操作时，会利用已开辟空间，不会重新申请 List list的底层是一个双向链表，最大长度为2^32-1。常用的组合有
lpush+lpop=stack 先进后出的栈 lpush+rpop=queue 先进先出的队列 lpush+ltrim =capped collection 有限集合 lpush+brpop = message queue 消息队列 一般可以用来做一些简单的消息队列，数据量小的的时候可以用独有的压缩队列来提升性能
Hash hash适合将一些关联的聚合数据放在一起，比如用户信息，用户的购物车等一些数据。
hash的底层是一个字典集合，整体是层层封装的。从下到上的层级顺序为
dictEntry 这是真正存储数据的节点，包含key-value和next节点,是一个链表节点 dictht 一个dictEntry类型的数组 数组的长度size sizemask等于size-1 当前数组中含多少个节点 dict dictType 类型，包括一些自定义函数，这些函数使得 key 和 value 能够存储 rehashidx 其实是一个标志量，如果为-1说明当前没有扩容，如果不为 -1 则记录扩容位置； dictht数组，两个Hash表； iterators 记录了当前字典正在进行中的迭代器。 整体结构 渐进式扩容 dictht为何存在两个？</description>
    </item>
    
    <item>
      <title>linux常用命令</title>
      <link>https://869413421.github.io/post/linux_cmd/</link>
      <pubDate>Tue, 07 Feb 2023 16:52:58 +0800</pubDate>
      
      <guid>https://869413421.github.io/post/linux_cmd/</guid>
      <description>常用命令 (1) 安全类型
① sudo 使用root 用户来执行命令
② su 使用指定用户来执行命令
③ chmod 修改文件权限
④ setfacl 修改文件权限，设置文件访问列表
(2) 进程管理
① w 显示已经登陆用户列表
② top 电脑性能分析工具
③ ps 显示系统进程
④ kill 发送信号杀死进程
⑤ pkill	根据进程名称杀死进程
⑥ pstree 显示进程树木
⑦ killall 指定名称杀死所有进程
(3) 用户管理
① id 显示当前用户ID以及分组的ID
② usermod 修改账号信息包括分组权限
③ useradd	增加用户
④ groupadd	增加分组
⑤ userdel 删除用户
(4) 文件系统
① mount 挂载文件系统
② umount 取消挂载
③ fsck 文件修复
④ df 查看磁盘信息</description>
    </item>
    
    <item>
      <title>c#操作redis</title>
      <link>https://869413421.github.io/post/c_redis/</link>
      <pubDate>Tue, 07 Feb 2023 15:52:40 +0800</pubDate>
      
      <guid>https://869413421.github.io/post/c_redis/</guid>
      <description>Redis是什么？ redis是一个开源的，面向键/值对的NOSQL的分布式数据库系统
NOSQL指的是非关系型的数据，简单直白地讲就是在非关系型的数据库中不存在表的概念，而是以键值对的方式，
即一个KEY关联一个值的方式进行存储。
redis是一个纯粹为应用而生的高性能数据库系统，非常适合用于持久储存，适应高并发等业务情景。
顺便提一下，redis是一个单线程的程序
redis是单线程的程序，为什么会这么快 1.大量的线程导致的线程切换开销
2.不存在非必要的内存浪费（因为redis是即使申请内存的，数据多大申请存储的内存就多大）
3.数据结构多样但只做自己的事情。（这样说有点模糊。。）
redis能存储的五种数据类型 1.string（字符串）
public ActionResult Index() {
//创建一个指向服务器Redis连接var Client = new RedisClient(&amp;quot;127.0.0.1&amp;quot;, 6370);//将一个集合存储在服务器上，存储的类型为string//因为在向服务器存储的过程中Redis会将存储的数据序列化为JSON数据，所以在Redis中存储的数据本质是一个字符串var UserList = UserInfoService.LoadEntities(u =&amp;gt; u.DelFlag == 1).ToList();Client.Set&amp;lt;List&amp;lt;UserInfo&amp;gt;&amp;gt;(&amp;quot;UserList&amp;quot;, UserList);//获取一个key中的值，和存储的时候一样，读取的时候会对Redis中的数据反序列化。List&amp;lt;UserInfo&amp;gt; List = Client.Get&amp;lt;List&amp;lt;UserInfo&amp;gt;&amp;gt;(&amp;quot;UserList&amp;quot;);var temp = from s in Listselect new{Id=s.ID,Name=s.UName,CreateTime=s.SubTime};return Json(temp);}从代码中可以推断当redis内部进行存取所做的序列化和反序列化步骤必定会造成一定的性能损耗，虽然对redis来说影响微乎其微，
但对于某些特殊业务场景下可能造成更加量级的影响，所以我们可以使用hash来进行无需序列化的存储。（仅仅是一个菜鸡的认知，如果大神有幸读到本篇文章请批评我的无知。。）
2.hash（哈希）
3.list（包含队列，栈的双向链表） 数据结构
Client.PushItemToList(&amp;ldquo;Item&amp;rdquo;, &amp;ldquo;111&amp;rdquo;); ///redis中的栈操作，和队列操作无异
后面会做一个分布式缓存的列子。 4.set（无序列表）* 使用并集和交集能满足的一些业务场景，列如新浪微博中两个用户共同的粉丝。
5.zset(有序列表) 两种持久化存储方式 1.</description>
    </item>
    
    <item>
      <title>ElasticSearch（基础操作）</title>
      <link>https://869413421.github.io/post/elastice2/</link>
      <pubDate>Tue, 07 Feb 2023 15:52:40 +0800</pubDate>
      
      <guid>https://869413421.github.io/post/elastice2/</guid>
      <description>设置索引分片 PUT /blogs{&amp;#34;settings&amp;#34; : {//设置3个主分片&amp;#34;number_of_shards&amp;#34; : 3,//设置1个副分片&amp;#34;number_of_replicas&amp;#34; : 1}} 一个分片保存所有数据的一部分 副分片是主分片的一个拷贝备份，同时用于搜索和返回文档 主分片在索引创建时指定，不能被修改，副分片可以被修改
使用自定义ID索引文档 PUT /{index}/{type}/{id}{&amp;#34;field&amp;#34;: &amp;#34;value&amp;#34;,...} 使用ElasticSearch生成ID索引文档 POST /{index}/{_type}/{&amp;#34;title&amp;#34;: &amp;#34;My second blog entry&amp;#34;,&amp;#34;text&amp;#34;: &amp;#34;Still trying this out...&amp;#34;,&amp;#34;date&amp;#34;: &amp;#34;2014/01/01&amp;#34;} 将请求修改为POST,URL不指定ID，Es会为文档自动生成ID
获取一个文档 GET /{index}/{_type}/{id} 获取文档的部分字段 GET /{index}/{_type}/{id}?_source={filed}，{filed} 获取文档source GET /{index}/{_type}/{id}/_source 检测文档是否存在 XHEAD /{index}/{_type}/{id} 文档如果存在，Es会返回200 ok的响应码 如果不存在，会返回404
更新整个文档 PUT /{index}/{_type}/{id}{&amp;#34;title&amp;#34;: &amp;#34;My first blog entry&amp;#34;,&amp;#34;text&amp;#34;: &amp;#34;I am starting to get the hang of this.</description>
    </item>
    
    <item>
      <title>ElasticSearch系列（集群内部原理）</title>
      <link>https://869413421.github.io/post/elastice1/</link>
      <pubDate>Tue, 07 Feb 2023 15:52:40 +0800</pubDate>
      
      <guid>https://869413421.github.io/post/elastice1/</guid>
      <description>空集群 ElasticSearch集群是什么？ 一个运行中的Es实例我们称之一个节点，一个集群是指由一个或多个有相同cluster.name的节点组合而成，集群中所有节点会共同负载和分担所有压力。当集群内新增或者删除节点时，集群会重新平均分配所有的数据到每个节点。
集群的主节点 当一个运行中的节点被选举为主节点的时候，他会负责整个集群内的所有变更。例如索引的增加和删除，或者增加或删除节点。任何节点都可以成为主节点，但是主节点不会负责文档级别的管理。所以即使系统的压力怎么增加，主节点都不会成为性能的瓶颈。
操作时需要将请求发送到集群的哪一个节点？ 因为每个节点都知道需要操作的文档所在的节点，并且节点会帮我们将请求发送到文档所在的节点当中。所以我们需要对Es进行操作时，我们可以对集群中任意一个节点进行请求。
集群健康 有时候我们需要对集群做一些监控,命令如下 GET /_cluster/health {&amp;#34;cluster_name&amp;#34;: &amp;#34;elasticsearch&amp;#34;,&amp;#34;status&amp;#34;: &amp;#34;green&amp;#34;, &amp;#34;timed_out&amp;#34;: false,&amp;#34;number_of_nodes&amp;#34;: 1,&amp;#34;number_of_data_nodes&amp;#34;: 1,&amp;#34;active_primary_shards&amp;#34;: 0,&amp;#34;active_shards&amp;#34;: 0,&amp;#34;relocating_shards&amp;#34;: 0,&amp;#34;initializing_shards&amp;#34;: 0,&amp;#34;unassigned_shards&amp;#34;: 0} status 字段指示着当前集群在总体上是否工作正常。它的三种颜色含义如下：
green 所有的主分片和副本分片都正常运行 yellow 所有的主分片都正常运行，但不是所有的副本分片都正常运行。 red 有主分片没能正常运行 </description>
    </item>
    
    <item>
      <title>go-micro开发运维实践(业务架构)</title>
      <link>https://869413421.github.io/post/go-micro%E5%BC%80%E5%8F%91%E8%BF%90%E7%BB%B4%E5%AE%9E%E8%B7%B5/%E4%B8%9A%E5%8A%A1%E6%9E%B6%E6%9E%84/</link>
      <pubDate>Tue, 07 Feb 2023 15:52:40 +0800</pubDate>
      
      <guid>https://869413421.github.io/post/go-micro%E5%BC%80%E5%8F%91%E8%BF%90%E7%BB%B4%E5%AE%9E%E8%B7%B5/%E4%B8%9A%E5%8A%A1%E6%9E%B6%E6%9E%84/</guid>
      <description>业务准备 一个传统web系统，用户模块永远是不可或缺的一环，也作为一个系统的基石。下列的教程中将使用go-micro来编写一个用户服务，以此作为开发的基础。下面是一个用户服务中暴露的api以及内部调用rpc方法规划。
api /user POST 注册 /user/:id GET 获取用户信息 /user/token POST 认证获取token /user GET 获取用户列表 /user/:id PUT 更新用户 /user/:id DELETE 删除用户 /user/password POST 发起密码重置 /user/password PUT 重置密码 rpc Get 根据ID获取用户信息 Pagination 获取分页数据 Create 创建用户 Update 更新用户 Delete 删除用户 Auth 认证获取token Validate 验证token CreatePasswordReset 创建密码重置记录 ResetPassword 密码重置 架构设计 技术选型 注册中心：etcd api网关：micro-api v2 api服务：gin 微服务：go-micro v2 数据库：mysql 服务追踪：opentracing/jaeger 服务监控：prometheus + grafana 消息队列：rabbit-mq 缓存系统：redis 搜索服务：elasticsearch 日志系统：ELK 上述中所有描述的组件，在单机阶段我们都使用docker-compose来进行实践。后续我完成编码以及单机部署后再基于k8s进行部署
总结一下上图中用户请求到响应的整个流程，用户在前端发起请求，请求到达服务器后通过nginx或其他的负载均衡器中，通过反向代理把请求转发到micro-api统一网关。关于micro-api网关，你同样可以把他理解为一个分发路由，micro-api启动后会通过服务发现找到所有已经注册的api服务，然后解析路由规则将请求分发到到我们指定的api服务。而api服务会通过grpc向service请求，实际中api服务并不参与过多的密集计算或IO处理，最终处理压力交由service来承担。service处理完成将响应返回给api服务，api再返回响应给到接入层（nginx）,从而完成整个请求响应的闭环。至于上图中出现的服务治理，服务监控，链路追踪等细节，我们后续在执行到相关知识时再详细了解。</description>
    </item>
    
    <item>
      <title>go-micro开发运维实践(初始化用户API项目)</title>
      <link>https://869413421.github.io/post/go-micro%E5%BC%80%E5%8F%91%E8%BF%90%E7%BB%B4%E5%AE%9E%E8%B7%B5/%E5%88%9D%E5%A7%8B%E5%8C%96%E7%94%A8%E6%88%B7api%E9%A1%B9%E7%9B%AE/</link>
      <pubDate>Tue, 07 Feb 2023 15:52:40 +0800</pubDate>
      
      <guid>https://869413421.github.io/post/go-micro%E5%BC%80%E5%8F%91%E8%BF%90%E7%BB%B4%E5%AE%9E%E8%B7%B5/%E5%88%9D%E5%A7%8B%E5%8C%96%E7%94%A8%E6%88%B7api%E9%A1%B9%E7%9B%AE/</guid>
      <description>服务与API分离 在开篇前头，我们已经明确过，用户服务与用户API的用途与关系，用户服务是基于内网调用不对外暴露的，用户API是暴露到外网提供给外部访问的，用户API的实际用途可以归结为以下几点。
作为用户服务的客户端，提供对外访问路由，提高系统安全性 作为中间层，对web客户端提交信息进行验证过滤。 作为中间层，基于限流熔断等机制，控制访问流量。 初始化项目 mkdir user-apicd user-apigo mod init github.com/869413421/micro-service/user-apigo get -u github.com/gin-gonic/gin 编写web服务注册代码 touch main.go package mainimport (&amp;#34;github.com/micro/go-micro/v2/web&amp;#34;&amp;#34;log&amp;#34;&amp;#34;time&amp;#34;)func main() {var serviceName = &amp;#34;micro.api.user&amp;#34;service := web.NewService(web.Name(serviceName),web.Address(&amp;#34;:81&amp;#34;),// 指定服务注册信息在注册中心的有效期。 默认为一分种web.RegisterTTL(time.Minute*2),// 指定服务主动向注册中心报告健康状态的时间间隔,默认为 30 秒。web.RegisterInterval(time.Minute*1),)err := service.Init()if err != nil {log.Fatal(&amp;#34;Init api error:&amp;#34;, err)}err = service.Run()if err != nil {log.</description>
    </item>
    
    <item>
      <title>go-micro开发运维实践(初始化项目，安装micro)</title>
      <link>https://869413421.github.io/post/go-micro%E5%BC%80%E5%8F%91%E8%BF%90%E7%BB%B4%E5%AE%9E%E8%B7%B5/%E5%88%9D%E5%A7%8B%E5%8C%96%E9%A1%B9%E7%9B%AE%E5%AE%89%E8%A3%85micro/</link>
      <pubDate>Tue, 07 Feb 2023 15:52:40 +0800</pubDate>
      
      <guid>https://869413421.github.io/post/go-micro%E5%BC%80%E5%8F%91%E8%BF%90%E7%BB%B4%E5%AE%9E%E8%B7%B5/%E5%88%9D%E5%A7%8B%E5%8C%96%E9%A1%B9%E7%9B%AE%E5%AE%89%E8%A3%85micro/</guid>
      <description>初始化git项目 进入工作目录，按照go规范，我们定义一个工作目录应，这是我在windows环境中的定义的路径D:\go\src\github.com\869413421，在工作磁盘下的go/src中创建，后续加上仓库类型如github.com,gitee等，最后加上该站点账号。
创建项目文件夹 mkdir micro-service 关联github仓库 cd micro-servicegit initgit remote add origin https://github.com/869413421/micro-service.gitgit pull origin main 安装micro 在安装前，我们首先明确了解go-micro和micro具体是什么东西。避免后续因为这两项有关联的技术产生一些混淆。
go-micro:一款微服务开发框架，它是所有开发的核心，开发者可以利用它编码快速开发出服务。 micro:一个基于go-micro实现的微服务命令行工具包，它对于微服务开发是非必要的。但是能给开发提供很多便利，例如生成模板项目，提供web仪表盘，提供API网关，查看服务状态，调用服务等等。 拉取micro镜像 docker pull micro/micro:v2.9.3 生成micro生成项目模板 windows :::warning 在windows下执行命令要使用CMD执行 :::
docker run --rm -v D:\go\src\github.com\869413421\micro-service:/www -w /www micro/micro:v2.9.3 new --namespace=micro --type=service user linux
docker run --rm -v $(pwd):/www -w /www micro/micro:v2.9.3 new --namespace=micro --type=service user 安装protobuf 在执行生成模板命令后，我们可以等如下提示
Creating service micro.service.user in user.├── main.go├── generate.go├── plugin.</description>
    </item>
    
    <item>
      <title>go-micro开发运维实践(基于jwt实现登录验证接口)</title>
      <link>https://869413421.github.io/post/go-micro%E5%BC%80%E5%8F%91%E8%BF%90%E7%BB%B4%E5%AE%9E%E8%B7%B5/%E5%9F%BA%E4%BA%8Ejwt%E5%AE%9E%E7%8E%B0%E7%99%BB%E5%BD%95%E9%AA%8C%E8%AF%81%E6%8E%A5%E5%8F%A3/</link>
      <pubDate>Tue, 07 Feb 2023 15:52:40 +0800</pubDate>
      
      <guid>https://869413421.github.io/post/go-micro%E5%BC%80%E5%8F%91%E8%BF%90%E7%BB%B4%E5%AE%9E%E8%B7%B5/%E5%9F%BA%E4%BA%8Ejwt%E5%AE%9E%E7%8E%B0%E7%99%BB%E5%BD%95%E9%AA%8C%E8%AF%81%E6%8E%A5%E5%8F%A3/</guid>
      <description>定义登录验证protobuf 修改proto/user/user.proto syntax = &amp;#34;proto3&amp;#34;;package micro.service.user;option go_package = &amp;#34;proto/user&amp;#34;;service UserService {rpc Pagination(PaginationRequest) returns(PaginationResponse){}rpc Get(GetRequest) returns(UserResponse){}rpc Create(CreateRequest) returns(UserResponse){}rpc Update(UpdateRequest) returns(UserResponse){}rpc Delete(DeleteRequest) returns(UserResponse){}rpc Auth(AuthRequest) returns(TokenResponse){}rpc ValidateToken(TokenRequest) returns(TokenResponse){}}message User{uint64 id = 1;string name = 3;string email = 4;string real_name = 6;string avatar = 7;string create_at = 9;string update_at = 10;}//UserResponse 单个用户响应message UserResponse{User user = 1;}//PaginationResponse 用户分页数据响应message PaginationResponse{repeated User users = 1;uint64 total = 2;}//PaginationRequest 用户分页请求message PaginationRequest{uint64 page = 1;uint32 perPage = 2;}//GetRequest 获取单个用户请求message GetRequest{uint64 id = 1;}//CreateRequest 创建用户请求message CreateRequest{string name = 1;string password = 2;string email = 3;string real_name = 4;string avatar = 5;}//UpdateRequest 更新用户请求message UpdateRequest{uint64 id = 1;string name = 2;string email = 3;string real_name = 4;string avatar = 6;}//DeleteRequest 删除用户请求message DeleteRequest{uint64 id = 1;}//AuthRequest 登录请求message AuthRequest{string email = 1;string password = 2;}//TokenRequest token验证接口message TokenRequest{string token = 1;}//TokenResponse token响应接口message TokenResponse{string token = 1;bool valid = 2;} 生成protobuf代码 make proto 引用jwt编写获取token业务 获取jwt生成包 go get -u github.</description>
    </item>
    
    <item>
      <title>go-micro开发运维实践(安装etcd集群，部署注册中心)</title>
      <link>https://869413421.github.io/post/go-micro%E5%BC%80%E5%8F%91%E8%BF%90%E7%BB%B4%E5%AE%9E%E8%B7%B5/%E5%AE%89%E8%A3%85etcd%E9%9B%86%E7%BE%A4%E9%83%A8%E7%BD%B2%E6%B3%A8%E5%86%8C%E4%B8%AD%E5%BF%83/</link>
      <pubDate>Tue, 07 Feb 2023 15:52:40 +0800</pubDate>
      
      <guid>https://869413421.github.io/post/go-micro%E5%BC%80%E5%8F%91%E8%BF%90%E7%BB%B4%E5%AE%9E%E8%B7%B5/%E5%AE%89%E8%A3%85etcd%E9%9B%86%E7%BE%A4%E9%83%A8%E7%BD%B2%E6%B3%A8%E5%86%8C%E4%B8%AD%E5%BF%83/</guid>
      <description>etcd集群安装 在微服务架构中，注册中心作为基础设施，承担着服务注册以及服务发现的重要功能。etcd作为一个分布式一致性的KV存储系统，按照etcd官网给出的性能测试, 在2CPU，1.8G内存，SSD磁盘这样的配置下，单节点的写性能可以达到16K QPS, 而先写后读也能达到12K QPS，这个性能相当可观。而在go-micro中etcd作为注册中心默认驱动，得益于其灵活的拓展机制，要在go-micro中使用etcd相对简单，下面我们使用docker-compose部署一个etcd集群。
编写docker-compose 创建yaml和配置文件 touch docker-compose.yamltouch .env 为etcd持久化提供挂载目录 mkdir -p data/etcd1mkdir -p data/etcd2mkdir -p data/etcd3 .env添加通用参数 # 设置时区TZ=Asia/Shanghai# 设置etcd镜像版本ETCD_VERSION=3.5# 设置e3w镜像版本E3W_VERSION=latest 编写docker-compose.yaml :::info 这里我们主要通过 environment 配置项设置 etcd启动参数来定义集群配置，在启动过程中需要确保三个 etcd节点可以相互连接并通信。 :::
# docker-compose.ymlversion: &amp;#39;3.3&amp;#39;services:etcd1:image: bitnami/etcd:${ETCD_VERSION}environment:TZ: ${TZ}ALLOW_NONE_AUTHENTICATION: &amp;#34;yes&amp;#34;ETCD_NAME: &amp;#34;etcd1&amp;#34;ETCD_INITIAL_ADVERTISE_PEER_URLS: &amp;#34;http://etcd1:2380&amp;#34;ETCD_LISTEN_PEER_URLS: &amp;#34;http://0.0.0.0:2380&amp;#34;ETCD_LISTEN_CLIENT_URLS: &amp;#34;http://0.0.0.0:2379&amp;#34;ETCD_ADVERTISE_CLIENT_URLS: &amp;#34;http://etcd1:2379&amp;#34;ETCD_INITIAL_CLUSTER_TOKEN: &amp;#34;etcd-cluster&amp;#34;ETCD_INITIAL_CLUSTER: &amp;#34;etcd1=http://etcd1:2380,etcd2=http://etcd2:2380,etcd3=http://etcd3:2380&amp;#34;ETCD_INITIAL_CLUSTER_STATE: &amp;#34;new&amp;#34;volumes:- .</description>
    </item>
    
    <item>
      <title>go-micro开发运维实践(实现用户CURD服务)</title>
      <link>https://869413421.github.io/post/go-micro%E5%BC%80%E5%8F%91%E8%BF%90%E7%BB%B4%E5%AE%9E%E8%B7%B5/%E5%AE%9E%E7%8E%B0%E7%94%A8%E6%88%B7curd%E6%9C%8D%E5%8A%A1/</link>
      <pubDate>Tue, 07 Feb 2023 15:52:40 +0800</pubDate>
      
      <guid>https://869413421.github.io/post/go-micro%E5%BC%80%E5%8F%91%E8%BF%90%E7%BB%B4%E5%AE%9E%E8%B7%B5/%E5%AE%9E%E7%8E%B0%E7%94%A8%E6%88%B7curd%E6%9C%8D%E5%8A%A1/</guid>
      <description>定义protobuf,生成代码 修改proto/user/user.proto syntax = &amp;#34;proto3&amp;#34;;package micro.service.user;option go_package = &amp;#34;proto/user&amp;#34;;service UserService {rpc Pagination(PaginationRequest) returns(PaginationResponse){}rpc Get(GetRequest) returns(UserResponse){}rpc Create(CreateRequest) returns(UserResponse){}rpc Update(UpdateRequest) returns(UserResponse){}rpc Delete(DeleteRequest) returns(UserResponse){}}message User{uint64 id = 1;string name = 3;string email = 4;string real_name = 6;string avatar = 7;string create_at = 9;string update_at = 10;}//UserResponse 单个用户响应message UserResponse{User user = 1;}//PaginationResponse 用户分页数据响应message PaginationResponse{repeated User users = 1;uint64 total = 2;}//PaginationRequest 用户分页请求message PaginationRequest{uint64 page = 1;uint32 perPage = 2;}//GetRequest 获取单个用户请求message GetRequest{uint64 id = 1;}//CreateRequest 创建用户请求message CreateRequest{string name = 1;string password = 2;string email = 3;string real_name = 4;string avatar = 5;}//UpdateRequest 更新用户请求message UpdateRequest{uint64 id = 1;string name = 2;string email = 3;string real_name = 4;string avatar = 6;}//DeleteRequest 删除用户请求message DeleteRequest{uint64 id = 1;} 执行生成命令 make命令能帮我们执行在makefile中预定义好的命令，在开发当中能给我们带来便利。</description>
    </item>
    
    <item>
      <title>go-micro开发运维实践(封装gin编写api接口)</title>
      <link>https://869413421.github.io/post/go-micro%E5%BC%80%E5%8F%91%E8%BF%90%E7%BB%B4%E5%AE%9E%E8%B7%B5/%E5%B0%81%E8%A3%85gin%E7%BC%96%E5%86%99api%E6%8E%A5%E5%8F%A3/</link>
      <pubDate>Tue, 07 Feb 2023 15:52:40 +0800</pubDate>
      
      <guid>https://869413421.github.io/post/go-micro%E5%BC%80%E5%8F%91%E8%BF%90%E7%BB%B4%E5%AE%9E%E8%B7%B5/%E5%B0%81%E8%A3%85gin%E7%BC%96%E5%86%99api%E6%8E%A5%E5%8F%A3/</guid>
      <description>封装用户服务客户端 打开common项目修改pkg/container/service.go
package containerimport (userPb &amp;#34;github.com/869413421/micro-service/user/proto/user&amp;#34;&amp;#34;github.com/micro/go-micro/v2&amp;#34;&amp;#34;github.com/micro/go-micro/v2/broker&amp;#34;)var service micro.Servicevar userServiceClient userPb.UserService// SetService 设置服务实例func SetService(srv micro.Service) {service = srv}// GetService 返回服务实例func GetService() micro.Service {return service}// GetServiceBroker 返回服务Broker实例func GetServiceBroker() broker.Broker {return service.Options().Broker}// SetUserServiceClient 设置客户端实例func SetUserServiceClient(userService userPb.UserService) {userServiceClient = userService}// GetUserServiceClient 获取客户端实例func GetUserServiceClient() userPb.UserService {return userServiceClient} 打开user-api</description>
    </item>
    
    <item>
      <title>go-micro开发运维实践(引入rabbitmq作为消息驱动)</title>
      <link>https://869413421.github.io/post/go-micro%E5%BC%80%E5%8F%91%E8%BF%90%E7%BB%B4%E5%AE%9E%E8%B7%B5/%E5%BC%95%E5%85%A5rabbitmq%E4%BD%9C%E4%B8%BA%E6%B6%88%E6%81%AF%E9%A9%B1%E5%8A%A8/</link>
      <pubDate>Tue, 07 Feb 2023 15:52:40 +0800</pubDate>
      
      <guid>https://869413421.github.io/post/go-micro%E5%BC%80%E5%8F%91%E8%BF%90%E7%BB%B4%E5%AE%9E%E8%B7%B5/%E5%BC%95%E5%85%A5rabbitmq%E4%BD%9C%E4%B8%BA%E6%B6%88%E6%81%AF%E9%A9%B1%E5%8A%A8/</guid>
      <description>为什么需要异步通信？ 在我们预设定的接口中，我们需要完成一个重置密码的功能。基本流程为，用户提交需要重置密码的邮箱，系统接收到后向邮箱发送一则消息，用户点击邮箱中带有加密信息的邮件再次向系统发起请求，系统通过验证后重置用户的密码。在这一个流程当中，发送邮件是一个耗时操作，如果采用同步的方式，一方面这会导致大量的请求浪费（因为要监听状态需要发起轮询请求），另一方面会导致接口数量不断增长变得臃肿，另外，对一些耗时操作同步请求会影响用户体验。基于上面的种种原因，我们有必要为系统接入基于事件异步通信，这样不仅为系统带来解耦，同时可以基于消息队列进行多个订阅处理，从而提高系统的运行效率。在go-micro中，我们可以通过broker组件来实现上述的异步通信。这里我们选择go-micro插件支持rabbitmq作为broker的驱动。
docker-compose安装rabbitmq .env中添加配置信息 ...#设置rabbitmq镜像版本RABBITMQ_VERSION=3.8.3-management#rabbitmq默认用户名称RABBITMQ_USER=root#rabbitmq默认密码RABBTIMQ_PASSWORD=root... 修改docker-compose.yaml micro-rabbitmq:image: rabbitmq:${RABBITMQ_VERSION}restart: alwaysports:- 15672:15672- 5672:5672environment:- RABBITMQ_DEFAULT_USER=${RABBITMQ_USER}- RABBITMQ_DEFAULT_PASS=${RABBTIMQ_PASSWORD}networks:- micro-network 检查rabbitmq是否正常运行 检查容器是否正常运行 访问rabbitmq可视化管理界面 打开http://127.0.0.1:15672输入配置的用户名密码
编写重置密码服务 创建重置密码记录模型 touch pkg/model/password.go package modelimport (db &amp;#34;github.com/869413421/micro-service/common/pkg/db&amp;#34;)// PasswordReset 重置密码模型type PasswordReset struct {db.BaseModelToken string `gorm:&amp;#34;column:token;type:varchar(255) not null;index&amp;#34; `Email string `gorm:&amp;#34;column:email;type:varchar(255) not null;index&amp;#34; valid:&amp;#34;email&amp;#34;`Verify int8 `gorm:&amp;#34;column:verify;type:tinyint(1);not null;default:0&amp;#34;`}// Store 创建重置记录func (model *PasswordReset) Store() (err error) {result := db.</description>
    </item>
    
    <item>
      <title>go-micro开发运维实践(注册第一个微服务)</title>
      <link>https://869413421.github.io/post/go-micro%E5%BC%80%E5%8F%91%E8%BF%90%E7%BB%B4%E5%AE%9E%E8%B7%B5/%E6%B3%A8%E5%86%8C%E7%AC%AC%E4%B8%80%E4%B8%AA%E5%BE%AE%E6%9C%8D%E5%8A%A1/</link>
      <pubDate>Tue, 07 Feb 2023 15:52:40 +0800</pubDate>
      
      <guid>https://869413421.github.io/post/go-micro%E5%BC%80%E5%8F%91%E8%BF%90%E7%BB%B4%E5%AE%9E%E8%B7%B5/%E6%B3%A8%E5%86%8C%E7%AC%AC%E4%B8%80%E4%B8%AA%E5%BE%AE%E6%9C%8D%E5%8A%A1/</guid>
      <description>修改用户服务代码 前面我们已经安装好了微服务的一些基础设施，现在我们需要开始编写微服务代码，构建容器，启动服务并将其注册到注册中心中。
更正引用错误 打开micro生成的用户服务代码模板的入口文件main.go,我们发现因为我们修改了go.mod文件所以导致一些引用失效，所以我们需要将这些文件的引用更正
修改main.go package mainimport (&amp;#34;github.com/869413421/micro-service/user/handler&amp;#34;&amp;#34;github.com/869413421/micro-service/user/subscriber&amp;#34;&amp;#34;github.com/micro/go-micro/v2&amp;#34;log &amp;#34;github.com/micro/go-micro/v2/logger&amp;#34;proto &amp;#34;github.com/869413421/micro-service/user/proto/user&amp;#34;)func main() {// New Serviceservice := micro.NewService(micro.Name(&amp;#34;micro.service.user&amp;#34;),micro.Version(&amp;#34;latest&amp;#34;),)// Initialise serviceservice.Init()// Register Handlerproto.RegisterUserHandler(service.Server(), new(handler.User))// Register Struct as Subscribermicro.RegisterSubscriber(&amp;#34;micro.service.user&amp;#34;, service.Server(), new(subscriber.User))// Run serviceif err := service.Run(); err != nil {log.Fatal(err)}} package handlerimport (&amp;#34;context&amp;#34;&amp;#34;github.com/869413421/micro-service/user/proto/user&amp;#34;log &amp;#34;github.com/micro/go-micro/v2/logger&amp;#34;proto &amp;#34;github.com/869413421/micro-service/user/proto/user&amp;#34;)type User struct{}// Call is a single request handler called via client.</description>
    </item>
    
    <item>
      <title>go-micro开发运维实践(行文初衷)</title>
      <link>https://869413421.github.io/post/go-micro%E5%BC%80%E5%8F%91%E8%BF%90%E7%BB%B4%E5%AE%9E%E8%B7%B5/%E8%A1%8C%E6%96%87%E5%88%9D%E8%A1%B7/</link>
      <pubDate>Tue, 07 Feb 2023 15:52:40 +0800</pubDate>
      
      <guid>https://869413421.github.io/post/go-micro%E5%BC%80%E5%8F%91%E8%BF%90%E7%BB%B4%E5%AE%9E%E8%B7%B5/%E8%A1%8C%E6%96%87%E5%88%9D%E8%A1%B7/</guid>
      <description>写在前头 考虑到近期耗费了不少时间在微服务开发上，在拜读完学院君的《微服务从入门到实践》在趟了很多坑后获得了一些自己的理解和实践。为了巩固近期学习到的知识，以及为后来者作些许贡献，打算利用空闲时间来书写一写我这段时间内从零到一的实现方案。作为一名有实用原则的程序员，行文中我可能并不会对概念方面进行详细讲解。只希望能抛砖引玉让大家对整个方案的实现有所理解，点到点之间做好链接，从而达到摸清整个微服务的轮廓的目的。
阅读本文所需知识 具备golang基础，了解grpc 对微服务概念有所理解，使用过go-micro，至少能完成go-micro入门案例 使用过docker，docker-compose,熟悉docker的基本操作 会使用linux 开发工具 以下是作者使用到的开发工具，可以根据自身实际情况进行调整
win10 docker-desktop goland mysqlWorkbeanch vmware 项目案例 为了展示教学，我们展示不考虑编写过于复杂的业务。但为了对于知识点有所覆盖，我们选用比较经典的电商项目进行编码。主要划分为三个模块，用户服务，商品服务，订单服务 。方便我们展示在微服务中如何实现定时调度，分布式事务，链路追踪，服务治理，分布式日志，异步消息等方案。微服务框架选择 这里我们选择使用go-micro v2版本，至于为什么使用go-micro，因为它除了提供基本的RPC远程调用外，还提供了需要实现微服务的各种基础支持，包括注册中心、服务发现、负载均衡、API 网关、异步消息队列、多种通信协议和数据序列化格式等，不需要开发者额外编写代码。还可以基于go-micro的插件机制，对这些功能的驱动进行替换。如注册中心，可以基于热拔插机制替换成etcd,consul,k8s,异步消息驱动可以替换成市面上比较流行的各种中间件，如NATS,RabbitMq。相对来说go-micro是一款灵活拓展性高且功能完备的开发框架。
为什么不是micro v3? 至作者行文当天，micro v3依然处于商业化探索阶段，大部分功能开发测试当中。且v3版本除了一些思想上的延续，与v2基本上不再相同。v2已经独立出一个仓库维护了，且[micro](https://micro.dev)官网大部分文档已经下架更新中，导致学习框架的成本更高。所以这里选择v2，因为v2的使用在[github](https://github.com/asim/go-micro)仓库中依然有大部分的使用案例。</description>
    </item>
    
    <item>
      <title>go-micro开发运维实践(部署用户数据库，封装gorm)</title>
      <link>https://869413421.github.io/post/go-micro%E5%BC%80%E5%8F%91%E8%BF%90%E7%BB%B4%E5%AE%9E%E8%B7%B5/%E9%83%A8%E7%BD%B2%E7%94%A8%E6%88%B7%E6%95%B0%E6%8D%AE%E5%BA%93%E5%B0%81%E8%A3%85gorm/</link>
      <pubDate>Tue, 07 Feb 2023 15:52:40 +0800</pubDate>
      
      <guid>https://869413421.github.io/post/go-micro%E5%BC%80%E5%8F%91%E8%BF%90%E7%BB%B4%E5%AE%9E%E8%B7%B5/%E9%83%A8%E7%BD%B2%E7%94%A8%E6%88%B7%E6%95%B0%E6%8D%AE%E5%BA%93%E5%B0%81%E8%A3%85gorm/</guid>
      <description>微服务数据库拆分原则 数据库拆分是微服务中的一个关键点，在进行拆分时需要遵循一些原则。
每个微服务都拥有属于自己的数据库，且只允许当前服务调用。 微服务中，依赖数据（如主表依赖从表，用户与用户订单这种关系）应该通过服务进行调用。 共享数据（如国家，地区），可能需要被许多微服务进行访问，将其拆分后虽然起到了解耦的作用，如果通过服务来进行访问对性能会有损耗。这种情况下就需要斟酌处理了，其中一种方式是直接对数据异构解耦。比如一个地区表，用户服务需要直接对其join进行访问，订单服务也需要对其join进行访问。这时候我们在两个服务的数据库中都建立一个地区表，再通过binlog或者mq的方式让这两个表的数据进行同步。推荐一下chanl,阿里开源的一种binlog同步方案，支持多种语言客户端。 docker-compose安装用户数据库 修改.env ...#数据库版本MYSQL_VERSION=latest#用户数据库用户名USER_DB_USER=&amp;#34;micro_user&amp;#34;#用户数据库密码USER_DB_PASSWORD=&amp;#34;micro_user&amp;#34;#用户数据库初始dbUSER_DB_DATABASE=&amp;#34;micro_user&amp;#34;#用户数据库root密码USER_DB_ROOT_PASSWORD=&amp;#34;root&amp;#34;#用户数据库映射端口USER_DB_PORT=33061#用户数据库最大链接数USER_DB_MAX_CONNECTIONS=200#用户数据库最大空闲链接数USER_DB_MAX_IDE_CONNECTIONS=50#用户数据库空闲链接最大存活时间，分USER_DB_CONNECTIONS_MAX_LIFE_TIME=5... 创建持久化挂载目录 mkdir -p data/user-db 修改docker-compose.yaml ...micro-user-db:image: mysql:${MYSQL_VERSION}ports:- ${USER_DB_PORT}:3306volumes:- ./data/user-db:/var/lib/mysqlrestart: alwaysenvironment:TZ: ${TZ}MYSQL_USER: ${USER_DB_USER} # 设置用户名MYSQL_PASSWORD: ${USER_DB_PASSWORD} # 设置用户民吗MYSQL_DATABASE: ${USER_DB_DATABASE} # 初始数据库MYSQL_ROOT_PASSWORD: ${USER_DB_ROOT_PASSWORD} # root用户密码networks:- micro-network... 启动数据库 docker-compose up -d micro-user-db查看容器是否正常运行使用.</description>
    </item>
    
    <item>
      <title>golang基础(1.GOPATH和工作区)</title>
      <link>https://869413421.github.io/post/golang%E5%9F%BA%E7%A1%80/1.gopath%E5%92%8C%E5%B7%A5%E4%BD%9C%E5%8C%BA/</link>
      <pubDate>Tue, 07 Feb 2023 15:52:40 +0800</pubDate>
      
      <guid>https://869413421.github.io/post/golang%E5%9F%BA%E7%A1%80/1.gopath%E5%92%8C%E5%B7%A5%E4%BD%9C%E5%8C%BA/</guid>
      <description>官方命令文档 设置gopath的意义是什么？ gopath是系统的环境变量，它是由一个或者多个文件目录路径组成。每一个文件路径是一个GO语言的工作区（workspace）。这些工作区用于存放go项目的源代码文件（sourcefile），安装（go install）以后的归档文件，编译后的可执行文件(go build)。
go语言的源码的组织方式 在go语言中，包作为go语言的基本单位，这些包的名称和文件系统中目录名称一一对应，一个目录下可以有多个子目录，相对应的一个包下可以有多个子包 一个包下可以包含多个.go文件，这些源代码必须被声明为在同一包下。代码包的名称一般与源码所在的目录同名，如果不同名，那么在安装过程中会一代码文件中的包声明为准 每个包拥有自己的导入路径，在工作区中一个包的导入路径实际上就是从src子目录到该包存储位置的相对路径 了解源码安装后的后果 源码文件会被放到某个工作区的src目录下 如果安装后产生了归档文件，则会被放进该工作区的pkg子目录下 如果安装后产生了可执行文件，则会被放进该工作区的bin子目录下 理解构建和安装go程序的过程 构建命令使用go build,安装命令使用go install,构建和安装都会进行打包编译等操作，并且将这些操作生成的文件放到某个临时目录当中 如果构建的是库源码文件，这些文件只会被保存在临时目录当中，这里构建的意义在于检查和验证。 如果构建的是命令源码文件，那么操作的结果文件会被搬运到那个源码文件所在的目录中。 安装操作会先构建，然后把文件转运到指定的目录下。如果安装的是库源码文件，那么结果文件会被搬运到它所在工作区的 pkg 目录下的某个子目录 。 如果安装的是命令源码文件，那么结果文件会被搬运到它所在工作区的 bin 目录中，或者环境变量GOBIN指向的目录中。 go build 命令的一些可选项的用途和用法 在运行go build的时候，默认是不会编译目标代码所依赖的那些代码包。如果依赖代码包的归档文件不存在，或者源码发生了变化，那么它还是会被编译。如果要强制编译她们，可以在执行命令时加上选项 -a,此时目标代码包以及所依赖的代码包都会被编译，哪怕是标准库中的代码包也是如此。另外，如果不但要编译依赖的代码包，还要安装它们的归档文件，那么可以加入标记-i（新版本已抛弃）。如何确认那些包被编译了？
运行go build时候加上-x,加上-n参数可以只看具体操作不执行他们 运行go build的时候加上-v,这样可以看到编译代码包的名称。 go get命令常用选项 go get 命令会自动帮我们从主流的公用代码仓库下载代码，并且将他们安装到环境变量gopath包含的第一工作区对应的目录中。
-u : 下载并安装代码，不论工作区中是否已经存在 -d: 只下载代码包，不安装代码包 -fix: 在下载代码包后先运行一个用于根据当前版本GO语言的修正工具，然后再安装代码包 -t: 同时下载测试所需的代码包 -insecure：允许通过非安全的网络协议下载和安装代码包。HTTP 就是这样的协议。 </description>
    </item>
    
    <item>
      <title>golang基础(10.浮点型与复数类型)</title>
      <link>https://869413421.github.io/post/golang%E5%9F%BA%E7%A1%80/10.%E6%B5%AE%E7%82%B9%E5%9E%8B%E4%B8%8E%E5%A4%8D%E6%95%B0%E7%B1%BB%E5%9E%8B/</link>
      <pubDate>Tue, 07 Feb 2023 15:52:40 +0800</pubDate>
      
      <guid>https://869413421.github.io/post/golang%E5%9F%BA%E7%A1%80/10.%E6%B5%AE%E7%82%B9%E5%9E%8B%E4%B8%8E%E5%A4%8D%E6%95%B0%E7%B1%BB%E5%9E%8B/</guid>
      <description>浮点型也叫做浮点数，用于表示包含小数点的数据，比如3.14,1.00
浮点数表示 在go中浮点数定义了两种类型，分别是float32和float64,其中float32是单精度，精确到小数点后面7位数。float64为双精度（double），精确到小数点后15位。在实际开发中，应该尽可能地使用 float64 类型，因为 math 包中所有有关数学运算的函数都会要求接收这个类型。
var a float32 = 0.1var b float64 = 0.2c := 8.0 //需要加小数点，否则会自动推导为整 :::tips 浮点数的运算和整型一样，也要保证操作数的类型一致，float32 和 float64 类型数据不能混合运算，需要手动进行强制转化才可以 :::
浮点数的精度问题 浮点数不是一个精确的表达，因为二进制无法表达所有十进制小数，在双精度的时候会存在精度丢失问题。
package mainimport &amp;#34;fmt&amp;#34;func main() {var a float64= 0.7var b float64 = 0.1c := a + bfmt.Printf(&amp;#34;a+b=%v &amp;#34;, c) // 输出0.7999999999999999fmt.Printf(&amp;#34;0.8 == 0.1+0.7 ? %v&amp;#34;, (0.8 == a+b)) // 输出false} 浮点数运算 浮点数可以通过算术运算符四则运算加减乘除，也可以进行比较运算（两个值得类型相等）。但在进行相等比较时，看起来相等的两个十进制浮点数，在底层转化为二进制时会丢失精度，因此不能被表象蒙蔽。如果需要比较可以使用以下方案。
package mainimport (&amp;#34;fmt&amp;#34;&amp;#34;math&amp;#34;)func main() {var a float64 = 0.</description>
    </item>
    
    <item>
      <title>golang基础(11.字符串和字符类型)</title>
      <link>https://869413421.github.io/post/golang%E5%9F%BA%E7%A1%80/11.%E5%AD%97%E7%AC%A6%E4%B8%B2%E5%92%8C%E5%AD%97%E7%AC%A6%E7%B1%BB%E5%9E%8B/</link>
      <pubDate>Tue, 07 Feb 2023 15:52:40 +0800</pubDate>
      
      <guid>https://869413421.github.io/post/golang%E5%9F%BA%E7%A1%80/11.%E5%AD%97%E7%AC%A6%E4%B8%B2%E5%92%8C%E5%AD%97%E7%AC%A6%E7%B1%BB%E5%9E%8B/</guid>
      <description>字符串 在go中字符串作为一个基本类型，默认使用utf8编码。当字符为ASCII码时占用一个字节，其他字符串需要2-4个自己，中文占用3个字节。
声明和初始化 var str string str = &amp;#34;test&amp;#34;str2 := &amp;#34;test2&amp;#34; 获取单个字符 获取单个字符可以使用数组下标的方式
str := &amp;#34;test&amp;#34;ch := str[0]print(ch) // 输出t 格式化输出 还可以通过 Go 语言内置的 len() 函数获取指定字符串的长度，以及通过 fmt 包提供的 Printf 进行字符串格式化输出（用法和 PHP 中的 printf 类似）
fmt.Printf(&amp;#34;The length of \&amp;#34;%s\&amp;#34; is %d &amp;#34;, str, len(str)) fmt.Printf(&amp;#34;The first character of \&amp;#34;%s\&amp;#34; is %c.&amp;#34;, str, ch) 转义字符 与PHP不同，go只允许使用双引号来定义字符串字面值。如果要对特定字符进行转义，可以通过 \ 实现，就像我们上面在字符串中转义双引号和换行符那样，常见的需要转义的字符如下所示：
：换行符
\r ：回车符 \t ：tab 键 \u 或 \U ：Unicode 字符 \ ：反斜杠自身 所以，上述打印代码输出结果为：</description>
    </item>
    
    <item>
      <title>golang基础(12.基本数据类型之间的转换)</title>
      <link>https://869413421.github.io/post/golang%E5%9F%BA%E7%A1%80/12.%E5%9F%BA%E6%9C%AC%E6%95%B0%E6%8D%AE%E7%B1%BB%E5%9E%8B%E4%B9%8B%E9%97%B4%E7%9A%84%E8%BD%AC%E6%8D%A2/</link>
      <pubDate>Tue, 07 Feb 2023 15:52:40 +0800</pubDate>
      
      <guid>https://869413421.github.io/post/golang%E5%9F%BA%E7%A1%80/12.%E5%9F%BA%E6%9C%AC%E6%95%B0%E6%8D%AE%E7%B1%BB%E5%9E%8B%E4%B9%8B%E9%97%B4%E7%9A%84%E8%BD%AC%E6%8D%A2/</guid>
      <description>我们已经陆续介绍完了 Go 语言中的基本数据类型，分别是布尔类型、整型、浮点型、复数类型、字符串和字符类型，和 PHP 一样，Go 语言也支持这些基本数据类型之间的转化，但是不是像 PHP 那种可以自动转化，比如下面这些语句在 PHP 中都是合法的：
$a = 1;$b = 1.1;$c = &amp;#34;test&amp;#34;;$d = true;$sum = $a + $b; // 将 $a 和 $b 相加，会自动将 $a 转化为浮点型，结果是 2.1$sum = $a + $d; // 将 $a 和 $d 相加，会自动将 $d 转化为整型，结果是 2$str = $c . $b; // 将 $b 和 $c 相连接，$b 会被转化为字符串，结果是「test1.1」 数值类型之间的转换 关于数值类型之间的转化，我们前面在介绍运算符的时候已经提到过，在进行类型转化时只需要调用要转化的数据类型对应的函数即可：
v1:= uint(16) // 初始化 v1 类型为 unitv2 := int8(v1) // 将 v1 转化为 int8 类型并赋值给 v2v3 := uint16(v2) // 将 v2 转化为 uint16 类型并赋值给 v3 不过需要注意，在有符号与无符号以及高位数字向低位数字转化时，需要注意数字的溢出和截断，比如我们看这个例子：</description>
    </item>
    
    <item>
      <title>golang基础(13.数组及其使用)</title>
      <link>https://869413421.github.io/post/golang%E5%9F%BA%E7%A1%80/13.%E6%95%B0%E7%BB%84%E5%8F%8A%E5%85%B6%E4%BD%BF%E7%94%A8/</link>
      <pubDate>Tue, 07 Feb 2023 15:52:40 +0800</pubDate>
      
      <guid>https://869413421.github.io/post/golang%E5%9F%BA%E7%A1%80/13.%E6%95%B0%E7%BB%84%E5%8F%8A%E5%85%B6%E4%BD%BF%E7%94%A8/</guid>
      <description>数组的声明和初始化 数组是所有语言编程中最常用的数据结构之一，Go 语言也不例外，与 PHP、JavaScript 等弱类型动态语言不同，在 Go 语言中，数组是固定长度的、同一类型的数据集合。数组中包含的每个数据项被称为数组元素，一个数组包含的元素个数被称为数组的长度。和 PHP 一样，Go 语言也通过 [] 来标识数组类型，以下是一些常见的数组声明方法：
var a [8]byte // 长度为8的数组，每个元素为一个字节var b [3][3]int // 二维数组（9宫格）var c [3][3][3]float64 // 三维数组（立体的9宫格）var d = [3]int{1, 2, 3} // 声明时初始化var e = new([3]string) // 通过 new 初始化 数组可以是多维的，但是声明数组必须指定同一个数据类型，且要在声明时候指定长度。&amp;lt;br /&amp;gt;还可以通过 := 对数组进行声明和初始化：a := [5]int{1,2,3,4,5} 此外还可以通过&amp;hellip;省略号的方式忽略数组长度
a := [...]int{1,2,3} 这种情况go会在编译期间自动计算出数组长度数组初始化的时候，如果没填充慢，空位即是对应元素的初始值
a := [5]int{1, 2, 3}fmt.Println(a) 上述代码的打印结果是：
[1 2 3 0 0] 还可以初始化指定下标位置的元素值
a : = [5]int{1:3,2:4} 数组长度在定义后就不可更改，在声明时可以指定数组长度为一个常量或者一个常量表达式（常量表达式是指在编译期即可计算结果的表达式）。数组的长度是该数组类型的一个内置常量，可以用 Go 语言的内置函数 len() 来获取：</description>
    </item>
    
    <item>
      <title>golang基础(14.数组切片)</title>
      <link>https://869413421.github.io/post/golang%E5%9F%BA%E7%A1%80/14.%E6%95%B0%E7%BB%84%E5%88%87%E7%89%87/</link>
      <pubDate>Tue, 07 Feb 2023 15:52:40 +0800</pubDate>
      
      <guid>https://869413421.github.io/post/golang%E5%9F%BA%E7%A1%80/14.%E6%95%B0%E7%BB%84%E5%88%87%E7%89%87/</guid>
      <description>数组定义长度后是无法修改的，数组的长度是数组类型本身的一部分，长度是数组内的一个内置常量，因此我们不能对数组进行一个增删操作。显然数组这种不灵活的特性是不能满足日常开发需求的，因此golang提供了另一种数据类型（slice）数组切片来你补数组的不足。数组切片是一个能对元素进行增删的数组，它的底层就是基于数组实现的。
数组切片的定义 在go中，定义数组切片稍微与定义数组不同，数组是需要指定长度和类型的，数组切片只需要指定类型不需要指定长度。
package mainimport &amp;#34;fmt&amp;#34;func main() {// 数组var arr = [5]int{1, 2, 3, 4, 5}// 切片var _slice = []int{1, 2, 3, 4, 5}fmt.Println(arr)fmt.Println(_slice)} 切片是一个可变长度同一类型元素的集合，切片的长度可以随着元素增长而增长（不会因为减少而减少），不过数组底层管理依然使用数组来管理元素。切片是数组的一层封装，基于数组为其提供一系列管理功能，可以动态拓展存储空间。创建数组切片 创建数组切片的方法主要有三种 —— 基于数组、基于数组切片和直接创建。
基于数组 数组切片可以基于一个已经存在的数组创建。数组可以看做是切片的底层数组，切片则是其某个连续片段的引用。切片可以局域数组的一部分创建也可以基于一整个创建，甚至可以创建一个比原数组更大的切片。
package mainimport &amp;#34;fmt&amp;#34;func main() {// 创建一个数组nums := [...]int{1, 2, 3, 4, 5, 6, 7, 8, 9, 10}// 基于数组片段创建切片slice1 := nums[0:3]slice2 := nums[5:9]// 基于数组全部创建切片sliceAll := nums[:]fmt.</description>
    </item>
    
    <item>
      <title>golang基础(15.数组切片增删)</title>
      <link>https://869413421.github.io/post/golang%E5%9F%BA%E7%A1%80/15.%E6%95%B0%E7%BB%84%E5%88%87%E7%89%87%E5%A2%9E%E5%88%A0/</link>
      <pubDate>Tue, 07 Feb 2023 15:52:40 +0800</pubDate>
      
      <guid>https://869413421.github.io/post/golang%E5%9F%BA%E7%A1%80/15.%E6%95%B0%E7%BB%84%E5%88%87%E7%89%87%E5%A2%9E%E5%88%A0/</guid>
      <description>动态增加元素切片比数组强大之处在于能够动态增加元素，甚至可以在容量不足的情况下自动扩容。元素个数和元素可以分配的空间是两个不同的值，元素个数是长度，元素可分配空间是容量。一个切片容量的初始值可以根据创建方式改变
对于基于数组和切片创建的切片而言，默认容量是从切片起始索引到对应底层数组的结尾索引； 对于通过内置 make 函数创建的切片而言，在没有指定容量参数的情况下，默认容量和切片长度一致。 函数append()可以为数组末尾增加参数。如果追加的元素个数超出 原切片的的默认容量，则底层会自动进行扩容：
package mainimport &amp;#34;fmt&amp;#34;func main() {slice1 := make([]int, 4, 10)fmt.Println(len(slice1))fmt.Println(cap(slice1))slice2 := append(slice1, 1, 2, 3)fmt.Println(len(slice2)) // 长度7fmt.Println(cap(slice2)) // 容量10slice1 = append(slice1, slice2...)fmt.Println(slice1) fmt.Println(len(slice1)) // 长度11fmt.Println(cap(slice1)) // 容量20} 需要注意的是append方法并不会改变原来的切片，而是会生成一个新的容量 更大切片当中，将原有的元素和新增的元素一并拷贝到新的切片中一并放回。默认情况下，扩容后的`新切片是原切片容量的2倍`。如果还不足以容纳新元素则会再次进行扩容，直到新的容量足够容纳下所有的元素。但是，当原切片的长度大于或等于 `1024 `时，Go 语言将会以原容量的 `1.25 `倍作为新容量的基准。&amp;lt;br /&amp;gt;因此在开发阶段我们应该合理地分配容量值，减少内部因扩容重新分配内存和搬送内存的操作次数，提高程序性能。内容复制 go中拥有一个复制数组切片的函数copy，作用是讲一个数组切片的元素搬运到另一个数组切片。如果两个数组切片的元素个数不一致，会按其中较小的切片进行复制。
package mainimport &amp;#34;fmt&amp;#34;func main() {slice1 := []int{1, 2, 3, 4, 5}slice2 := []int{6, 7, 8}// 复制slice1到slice2//copy(slice2, slice1)//fmt.</description>
    </item>
    
    <item>
      <title>golang基础(16.字典类型声明，初始化，简单使用)</title>
      <link>https://869413421.github.io/post/golang%E5%9F%BA%E7%A1%80/16.%E5%AD%97%E5%85%B8%E7%B1%BB%E5%9E%8B%E5%A3%B0%E6%98%8E%E5%88%9D%E5%A7%8B%E5%8C%96%E7%AE%80%E5%8D%95%E4%BD%BF%E7%94%A8/</link>
      <pubDate>Tue, 07 Feb 2023 15:52:40 +0800</pubDate>
      
      <guid>https://869413421.github.io/post/golang%E5%9F%BA%E7%A1%80/16.%E5%AD%97%E5%85%B8%E7%B1%BB%E5%9E%8B%E5%A3%B0%E6%98%8E%E5%88%9D%E5%A7%8B%E5%8C%96%E7%AE%80%E5%8D%95%E4%BD%BF%E7%94%A8/</guid>
      <description>字典定义 go中支持字典类型的数据，所谓字典类型，指的是一个键值对的关系集合。一个键对应一个值，go中的字典是一个无序集合，不会根据键或值进行排序。在go中声明字典：
package mainimport (&amp;#34;fmt&amp;#34;&amp;#34;strconv&amp;#34;)func main() {var map1 map[string]intfmt.Println(map1)map2 := map[string]int{&amp;#34;one&amp;#34;: 1,&amp;#34;two&amp;#34;: 2,}fmt.Println(map2)key := &amp;#34;two&amp;#34;value, ok := map2[key]if ok {fmt.Printf(&amp;#34;map has key:&amp;#34; + key + &amp;#34; values is:&amp;#34; + strconv.Itoa(value))} else {fmt.Printf(&amp;#34;map no has key&amp;#34;)}} 字典声明 var map1 map[string]int 在go中声明字典需要指定键的数据类型以及值得数据类型
字典初始化 我们可以通过先声明再初始化的方式进行初始化，就像上面示例代码做的那样，也可以通过 := 将声明和初始化合并为一条语句：
map2 := map[string]int{&amp;#34;one&amp;#34;: 1,&amp;#34;two&amp;#34;: 2,} 还可以通过make函数来初始化一个字典，使用make函数创建的字典可以直接赋值。直接声明不允许这样操作，因为数据没被初始化为nil赋值会直接抛异常。</description>
    </item>
    
    <item>
      <title>golang基础(17.字典遍历排序)</title>
      <link>https://869413421.github.io/post/golang%E5%9F%BA%E7%A1%80/17.%E5%AD%97%E5%85%B8%E9%81%8D%E5%8E%86%E6%8E%92%E5%BA%8F/</link>
      <pubDate>Tue, 07 Feb 2023 15:52:40 +0800</pubDate>
      
      <guid>https://869413421.github.io/post/golang%E5%9F%BA%E7%A1%80/17.%E5%AD%97%E5%85%B8%E9%81%8D%E5%8E%86%E6%8E%92%E5%BA%8F/</guid>
      <description>字典遍历 使用for range 可以对字典进行遍历。
package mainimport &amp;#34;fmt&amp;#34;func main() {map1 := map[string]int{&amp;#34;one&amp;#34;: 1,&amp;#34;two&amp;#34;: 2,&amp;#34;three&amp;#34;: 3,}for k, v := range map1 {fmt.Println(k, v)}} 键值对调 键值对调指交换字典的键和值。
package mainimport &amp;#34;fmt&amp;#34;func main() {map1 := map[string]int{&amp;#34;one&amp;#34;: 1,&amp;#34;two&amp;#34;: 2,&amp;#34;three&amp;#34;: 3,}for k, v := range map1 {fmt.Println(k, v)}invMap := make(map[int]string, 3)for k, v := range map1 {invMap[v] = k}for k, v := range invMap {fmt.</description>
    </item>
    
    <item>
      <title>golang基础(18.指针的基本概念和使用)</title>
      <link>https://869413421.github.io/post/golang%E5%9F%BA%E7%A1%80/18.%E6%8C%87%E9%92%88%E7%9A%84%E5%9F%BA%E6%9C%AC%E6%A6%82%E5%BF%B5%E5%92%8C%E4%BD%BF%E7%94%A8/</link>
      <pubDate>Tue, 07 Feb 2023 15:52:40 +0800</pubDate>
      
      <guid>https://869413421.github.io/post/golang%E5%9F%BA%E7%A1%80/18.%E6%8C%87%E9%92%88%E7%9A%84%E5%9F%BA%E6%9C%AC%E6%A6%82%E5%BF%B5%E5%92%8C%E4%BD%BF%E7%94%A8/</guid>
      <description>指针概述 在go中我们可以通过变量来定义操作我们的物理存储空间，其本质是一块内存空间的定义。而指针的定义是指指向存储这些变量值的内存地址。
package mainimport &amp;#34;fmt&amp;#34;func main() {var a = 100var ptr *intptr = &amp;amp;afmt.Println(a) // 变量值fmt.Println(ptr) // 变量值存储地址} 上述代码定义了一个整形变量a值是一百，然后定义了一个整形指针。通过&amp;amp;符号，将变量a的变量值地址赋值给了指针ptr。我们可以通过 *ptr 获取指针指向内存地址存储的变量值（我们通常将这种引用称作「间接引用」），ptr 本身是一个内存地址值（通过 &amp;amp;a 可以获取变量 a 所在的内存地址）&amp;lt;br /&amp;gt;go语言引入指针类型，主要基于两点考虑。一个是为程序员提供操作变量对应内存数据结构的能力，一个是为了提供程序性能。（指针可以值直接传递某个变量的内存地址，可以在传递过程当中产生的值拷贝）&amp;lt;br /&amp;gt;指针在go中有两个使用场景类型指针 数组切片 作为类型指针时，允许对这个指针类型的数据直接进行修改指向其他内存地址，传递数据时如果使用指针则无须拷贝数据从而节省内存空间，此外和 C 语言中的指针不同，Go 语言中的类型指针不能进行偏移和运算，因此更为安全。数组切片，由指向起始元素的原始指针、元素数量和容量组成，所以切片与数组不同，是引用类型，而非值类型。
指针的基本使用 指针类型的声明和初始化 指针变量传值时之所以可以节省空间，因为指针指向的内存地址大小是固定的，在32位机器上占4个字节，在64位上占8个字节，与指向内存存储的值无关。
var ptr *intfmt.Println(ptr)a := 100ptr = &amp;amp;afmt.Println(ptr)fmt.Println(*ptr) 当指针被声明后，没有指向任何变量内存地址时，它的零值是 nil，然后我们可以通过在给定变量前加上取地址符 &amp;amp; 获取变量对应的内存地址将其赋值给声明的指针类型，这样，就是对指针的初始化了，然后我们可以通过在指针类型前加上间接引用符 * 获取指针指向内存空间存储的变量值。当然，我们也可以通过 := 对指针进行初始化：
a := 100ptr := &amp;amp;afmt.</description>
    </item>
    
    <item>
      <title>golang基础(19.条件语句)</title>
      <link>https://869413421.github.io/post/golang%E5%9F%BA%E7%A1%80/19.%E6%9D%A1%E4%BB%B6%E8%AF%AD%E5%8F%A5/</link>
      <pubDate>Tue, 07 Feb 2023 15:52:40 +0800</pubDate>
      
      <guid>https://869413421.github.io/post/golang%E5%9F%BA%E7%A1%80/19.%E6%9D%A1%E4%BB%B6%E8%AF%AD%E5%8F%A5/</guid>
      <description>流程控制主要用于设定计算执行的次序，建立程序的逻辑结构。Go 语言的流程控制和 PHP 类似，支持如下的几种流程控制语句：
条件语句，用于条件判断，对应关键词有if,else和else if 选择语句，用于分支选择，对应关键字有switch , case ,select(用于channel) 循环语句，用于迭代数据，对应关键词有for,range 跳转语句，用于跳转到指定逻辑，对应关键词goto // ifif condition { // do something }// if...else...if condition { // do something } else {// do something }// if...else if...else...if condition1 { // do something } else if condition2 {// do something else } else {// catch-all or default } 关于 Go 语言的条件语句，需要注意以下几点:
条件语句中不需要圆括号 语句体中有几条语句，花括号都必须存在 左花括号 { 必须与 if 或者 else 处于同一行 在 if 之后，条件语句之前，可以添加变量初始化语句，使用 ; 间隔，比如上述代码可以这么写 if score := 100; score &amp;gt; 90 { </description>
    </item>
    
    <item>
      <title>golang基础(2.命令源码文件)</title>
      <link>https://869413421.github.io/post/golang%E5%9F%BA%E7%A1%80/2.%E5%91%BD%E4%BB%A4%E6%BA%90%E7%A0%81%E6%96%87%E4%BB%B6/</link>
      <pubDate>Tue, 07 Feb 2023 15:52:40 +0800</pubDate>
      
      <guid>https://869413421.github.io/post/golang%E5%9F%BA%E7%A1%80/2.%E5%91%BD%E4%BB%A4%E6%BA%90%E7%A0%81%E6%96%87%E4%BB%B6/</guid>
      <description>命令源码文件怎样接收参数 flag.StringVar package mainimport (&amp;#34;flag&amp;#34;&amp;#34;fmt&amp;#34;)var name stringfunc init() {// 初始化参数flag.StringVar(&amp;amp;name, &amp;#34;name&amp;#34;, &amp;#34;everyone&amp;#34;, &amp;#34;user name&amp;#34;)}func main() {// 1.正式接收参数flag.Parse()fmt.Printf(&amp;#34;hello,%s! &amp;#34;, name)} 函数flag.StringVar接受 4 个参数。
第 1 个参数是用于存储该命令参数的值的地址，具体 到这里就是在前面声明的变量name的地址了，由表达式&amp;amp;name表示。 第 2 个参数是为了指定该命令参数的名称，这里是name。 第 3 个参数是为了指定在未追加该命 令参数时的默认值，这里是everyone。 至于第 4 个函数参数，即是该命令参数的简短说明了，这在打印命令说明时会用到。 flag.String 还有一个与flag.StringVar函数类似的函数，叫flag.String。这两个函数 的区别是，后者会直接返回一个已经分配好的用于存储命令参数值的地址。
package mainimport (&amp;#34;flag&amp;#34;&amp;#34;fmt&amp;#34;)func main() {// 1.正式接收参数name := flag.String(&amp;#34;name&amp;#34;, &amp;#34;everyone&amp;#34;, &amp;#34;username&amp;#34;)flag.</description>
    </item>
    
    <item>
      <title>golang基础(20.分支语句)</title>
      <link>https://869413421.github.io/post/golang%E5%9F%BA%E7%A1%80/20.%E5%88%86%E6%94%AF%E8%AF%AD%E5%8F%A5/</link>
      <pubDate>Tue, 07 Feb 2023 15:52:40 +0800</pubDate>
      
      <guid>https://869413421.github.io/post/golang%E5%9F%BA%E7%A1%80/20.%E5%88%86%E6%94%AF%E8%AF%AD%E5%8F%A5/</guid>
      <description>分支语句会根据条件的不同选择不同的分支代码进行执行，在go中不需要显式通过break语句跳出。
switch var1 {case a:...case b:...default:...} switch可以进行等值判断，也可以条件判断。如果是条件判断，不允许将变量放到switch关键词后。
package mainimport &amp;#34;fmt&amp;#34;func main() {// 等值判断num := 100switch num {case 90, 100:fmt.Println(1)case 80:fmt.Println(2)}// 条件判断switch {case num &amp;gt;= 90:fmt.Println(&amp;#34;a&amp;#34;)case num &amp;gt;= 90 &amp;amp;&amp;amp; num &amp;lt; 95:fmt.Println(&amp;#34;b&amp;#34;)}} 在go语言中使用逗号分隔不同的分支条件从而到达合并分支语句的目的，如 case 90,100。&amp;lt;br /&amp;gt;Go 分支语句中比较有意思的一点，那就是不需要显式通过 break 语句退出某个分支，上一个分支语句代码会在下一个 case 语句出现之前自动退出，如果你想要继续执行后续分支代码，可以通过一个 `fallthrough `语句来声明：package mainimport &amp;#34;fmt&amp;#34;func main() {num := 100switch {case num &amp;gt; 90:fmt.</description>
    </item>
    
    <item>
      <title>golang基础(21.循环语句)</title>
      <link>https://869413421.github.io/post/golang%E5%9F%BA%E7%A1%80/21.%E5%BE%AA%E7%8E%AF%E8%AF%AD%E5%8F%A5/</link>
      <pubDate>Tue, 07 Feb 2023 15:52:40 +0800</pubDate>
      
      <guid>https://869413421.github.io/post/golang%E5%9F%BA%E7%A1%80/21.%E5%BE%AA%E7%8E%AF%E8%AF%AD%E5%8F%A5/</guid>
      <description>基本使用 与其它编程语言不同的是，Go 语言中的循环语句只支持 for 关键字，而不支持 while 和 do-while 结构。关键字 for 的基本使用方法与 PHP 类似，只是循环条件不含括号，比如我们要计算 1 到 100 之间所有数字之后，可以这么做：
package mainimport &amp;#34;fmt&amp;#34;func main() {sum := 0for i := 0; i &amp;lt;= 100; i++ {sum += i}fmt.Println(sum) // 输出5050} 无限循环 go不支持while和do-while语句，如果需要无限循环，可以通过不带条件的for语句实现：
package mainimport &amp;#34;fmt&amp;#34;func main() {sum := 0i := 0for {i++if i &amp;gt; 100 {break}sum += i}fmt.</description>
    </item>
    
    <item>
      <title>golang基础(22.跳转语句)</title>
      <link>https://869413421.github.io/post/golang%E5%9F%BA%E7%A1%80/22.%E8%B7%B3%E8%BD%AC%E8%AF%AD%E5%8F%A5/</link>
      <pubDate>Tue, 07 Feb 2023 15:52:40 +0800</pubDate>
      
      <guid>https://869413421.github.io/post/golang%E5%9F%BA%E7%A1%80/22.%E8%B7%B3%E8%BD%AC%E8%AF%AD%E5%8F%A5/</guid>
      <description>break 与 continue 语句 Go 语言支持在循环语句中通过 break 语句跳出循环，通过 continue 语句进入下一个循环。break 的默认作用范围是该语句所在的最内部的循环体：
package mainimport &amp;#34;fmt&amp;#34;func main() {arr := [][]int{{1, 2, 3}, {4, 5, 6}, {7, 8, 9}}for i := 0; i &amp;lt; len(arr); i++ {for j := 0; j &amp;lt; len(arr[i]); j++ {num := arr[i][j]if j &amp;gt; 1 {break}fmt.Println(num)}}} continue 则用于忽略剩余的循环体而直接进入下一次循环的过程：
package mainimport &amp;#34;fmt&amp;#34;func main() {arr := [][]int{{1, 2, 3}, {4, 5, 6}, {7, 8, 9}}for i := 0; i &amp;lt; len(arr); i++ {for j := 0; j &amp;lt; len(arr[i]); j++ {num := arr[i][j]if j &amp;gt; 1 {break} else {continue}fmt.</description>
    </item>
    
    <item>
      <title>golang基础(23.函数的基本调用和定义)</title>
      <link>https://869413421.github.io/post/golang%E5%9F%BA%E7%A1%80/23.%E5%87%BD%E6%95%B0%E7%9A%84%E5%9F%BA%E6%9C%AC%E8%B0%83%E7%94%A8%E5%92%8C%E5%AE%9A%E4%B9%89/</link>
      <pubDate>Tue, 07 Feb 2023 15:52:40 +0800</pubDate>
      
      <guid>https://869413421.github.io/post/golang%E5%9F%BA%E7%A1%80/23.%E5%87%BD%E6%95%B0%E7%9A%84%E5%9F%BA%E6%9C%AC%E8%B0%83%E7%94%A8%E5%92%8C%E5%AE%9A%E4%B9%89/</guid>
      <description>在 Go 语言中，函数的基本组成为：关键字 func、函数名、参数列表、返回值、函数体和返回语句，作为强类型语言，无论是参数还是返回值，在定义函数时，都要声明其类型。函数主要分为三种类型
普通函数 匿名函数 类型系统方法 函数定义 func add(a, b int) int {return a + b} 如果函数的参数列表中包含若干个类型相同的参数，比如上面例子中的 a 和 b，则可以在参数列表中省略前面变量的类型声明，只保留最后一个。
函数调用 函数调用非常方便，如果是在同一个包中（即定义在同一个目录下的 Go 文件中），只需直接调用即可：
package mainimport &amp;#34;fmt&amp;#34;func add(a, b int) int {return a + b}func main() {fmt.Println(add(1, 2))} 需要先导入了该函数所在的包，然后才能调用该函数，比如，我们将 add 函数放到单独的 mymath 包中（函数名首字母改为大写）：package mymathfunc Add(a, b int) int {return a + b} 然后我们可以这样在 main 包中调用 Add 函数：</description>
    </item>
    
    <item>
      <title>golang基础(24.函数的传参和返回值)</title>
      <link>https://869413421.github.io/post/golang%E5%9F%BA%E7%A1%80/24.%E5%87%BD%E6%95%B0%E7%9A%84%E4%BC%A0%E5%8F%82%E5%92%8C%E8%BF%94%E5%9B%9E%E5%80%BC/</link>
      <pubDate>Tue, 07 Feb 2023 15:52:40 +0800</pubDate>
      
      <guid>https://869413421.github.io/post/golang%E5%9F%BA%E7%A1%80/24.%E5%87%BD%E6%95%B0%E7%9A%84%E4%BC%A0%E5%8F%82%E5%92%8C%E8%BF%94%E5%9B%9E%E5%80%BC/</guid>
      <description>按值传参和引用传参 go中默认按值来进行参数传递，就是传递传入参数的一个副本。函数只对副本操作，不会对原值有任何影响。
package mainimport &amp;#34;fmt&amp;#34;func add(a, b int) int {a *= 2b *= 3return a + b}func main() {x, y := 1, 2z := add(x, y)fmt.Printf(&amp;#34;add(%d, %d) = %d&amp;#34;, x, y, z)} 当我们把 x、y 变量作为参数传递到 add 函数时，这两个变量会拷贝出一个副本赋值给 a、b 变量作为参数，因此，在 add 函数中调整 a、b 变量的值并不会影响原变量 x、y 的值，所以上述代码的输出是：
add(1, 2) = 8 如果想要实在在函数中修改原有参数的值，可以通过引用传参来完成。传入函数内的不再是参数的服务。而是传递存储有变量值地址的指针。所以原变量的值也会被修改（这种情况下，传递的是变量地址值的拷贝，所以从本质上来说还是按值传参）：
package mainimport &amp;#34;fmt&amp;#34;func add(a, b *int) int {*a *= 2*b *= 3return *a + *b}func main() {x, y := 1, 2z := add(&amp;amp;x, &amp;amp;y)fmt.</description>
    </item>
    
    <item>
      <title>golang基础(25.变长函数)</title>
      <link>https://869413421.github.io/post/golang%E5%9F%BA%E7%A1%80/25.%E5%8F%98%E9%95%BF%E5%87%BD%E6%95%B0/</link>
      <pubDate>Tue, 07 Feb 2023 15:52:40 +0800</pubDate>
      
      <guid>https://869413421.github.io/post/golang%E5%9F%BA%E7%A1%80/25.%E5%8F%98%E9%95%BF%E5%87%BD%E6%95%B0/</guid>
      <description>所谓变长参数指的是函数参数的数量不确定，可以按照需要传递任意数量的参数到指定函数，比如前面演示过的 fmt.Printf 函数的参数显然就是变长参数。
Go 语言中的变长参数 合适地使用变长参数，可以让代码更简洁，尤其是输入输出类函数，比如日志函数。接下来，作为对比，我们来介绍下 Go 语言中的变长参数的用法，和 PHP 类似，只是把 &amp;hellip; 作用到类型上，这样就可以约束变长参数的类型：
package mainimport &amp;#34;fmt&amp;#34;func myFunc(numbers ...int) {for _, number := range numbers {fmt.Println(number)}}func main() {myFunc(1, 2, 3, 4, 5)} 或者还可以传递一个数组切片，传递切片时需要在末尾加上 &amp;hellip; 作为标识，表示对应的参数类型是变长参数：
package mainimport &amp;#34;fmt&amp;#34;func myFunc(numbers ...int) {for _, number := range numbers {fmt.Println(number)}}func main() {myFunc(1, 2, 3, 4, 5)slice1 := []int{7, 8, 9, 10}myFunc(slice1.</description>
    </item>
    
    <item>
      <title>golang基础(26.匿名函数与闭包)</title>
      <link>https://869413421.github.io/post/golang%E5%9F%BA%E7%A1%80/26.%E5%8C%BF%E5%90%8D%E5%87%BD%E6%95%B0%E4%B8%8E%E9%97%AD%E5%8C%85/</link>
      <pubDate>Tue, 07 Feb 2023 15:52:40 +0800</pubDate>
      
      <guid>https://869413421.github.io/post/golang%E5%9F%BA%E7%A1%80/26.%E5%8C%BF%E5%90%8D%E5%87%BD%E6%95%B0%E4%B8%8E%E9%97%AD%E5%8C%85/</guid>
      <description>匿名函数 匿名函数是一种不需要定义函数名称的声明方式，Go 语言中也提供了对匿名函数的支持，并且形式上和 PHP 类似，无非是要声明参数类型和返回值类型而已：
package mainimport &amp;#34;fmt&amp;#34;func main() {add := func(a, b int) int {return a + b}fmt.Println(add(1, 2)) // 赋值变量调用匿名函数 addc := func(a, b int) int {return a + b}(1, 2)fmt.Println(c) // 直接调用} 闭包 Go 语言的匿名函数是一个闭包（Closure），下面我们先来了解一下闭包的概念、价值和应用场景。
闭包的概念和价值 所谓闭包是指引用了自由变量的函数，被引用的自由变量将和这个函数一同存在，即使离开了创造它的上下文环境也不会被释放掉。或者通俗点说，「闭」的意思是「封闭外部状态」，即使外部状态已经失效，闭包内部依然保留了一份从外部引用的变量。闭包的价值在于可以作为函数对象或者匿名函数，对于类型系统而言，这意味着不仅要表示数据还要表示代码。支持闭包的多数语言都将函数作为第一类对象（firt-class object，有的地方也译作第一级对象，第一类公民等），就是说这些函数可以存储到变量中作为参数传递给其他函数，能够被函数动态创建和返回。 :::success 注：所谓第一类对象指的是运行期可以被创建并作为参数传递给其他函数或赋值给变量的实体，在绝大多数语言中，数值和基本类型都是第一类对象，在支持闭包的编程语言中（比如 Go、PHP、JavaScript、Python 等），函数也是第一类对象，而像 C、C++ 等不支持闭包的语言中，函数不能在运行期创建，所以在这些语言中，函数不是不是第一类对象。 :::
Go 语言中闭包的应用场景 Go 语言中的闭包同样也会引用函数外定义的变量，只要闭包还在被使用，那么被闭包引用的变量会一直存在。保证局部变量的安全性闭包内部声明的局部变量无法从外部修改，从而确保了安全性（类似类的私有属性）：
package mainimport &amp;#34;fmt&amp;#34;func main() {var j = 1f := func() {var i intfmt.</description>
    </item>
    
    <item>
      <title>golang基础(27.类型系统)</title>
      <link>https://869413421.github.io/post/golang%E5%9F%BA%E7%A1%80/27.%E7%B1%BB%E5%9E%8B%E7%B3%BB%E7%BB%9F/</link>
      <pubDate>Tue, 07 Feb 2023 15:52:40 +0800</pubDate>
      
      <guid>https://869413421.github.io/post/golang%E5%9F%BA%E7%A1%80/27.%E7%B1%BB%E5%9E%8B%E7%B3%BB%E7%BB%9F/</guid>
      <description>Go 语言面向对象编程设计得简洁而优雅。简洁之处在于，Go 语言并没有沿袭传统面向对象编程中的诸多概念，比如类的继承、接口的实现、构造函数和析构函数、隐藏的 this 指针等，也没有 public、protected、private 之类的可见性修饰符。顾名思义，类型系统是指一个语言的类型体系结构。一个典型的类型系统通常包含如下基本内容：
- 基础的数据类型，如byte,int,bool,float,string等等。- 复合类型，如数组，切片，字典，结构体，指针。- 可以指向任意对象的类型- 值语义和引用语义- 面向对象，具备面向对象的特征- 接口Go 语言中的大多数类型都是值语义，并且都可以包含对应的操作方法。在需要的时候，你可以给任何类型（包括内置类型）增加新方法。而在实现某个接口时，无需从该接口继承（事实上，Go 语言根本就不支持面向对象思想中的继承、实现语法），只需要实现该接口要求的所有方法即可。任何类型都可以被 Any 类型引用。在 Go 语言中，Any 类型就是空接口，即 interface{}。</description>
    </item>
    
    <item>
      <title>golang基础(28.类的定义，初始化和成员方法)</title>
      <link>https://869413421.github.io/post/golang%E5%9F%BA%E7%A1%80/28.%E7%B1%BB%E7%9A%84%E5%AE%9A%E4%B9%89%E5%88%9D%E5%A7%8B%E5%8C%96%E5%92%8C%E6%88%90%E5%91%98%E6%96%B9%E6%B3%95/</link>
      <pubDate>Tue, 07 Feb 2023 15:52:40 +0800</pubDate>
      
      <guid>https://869413421.github.io/post/golang%E5%9F%BA%E7%A1%80/28.%E7%B1%BB%E7%9A%84%E5%AE%9A%E4%B9%89%E5%88%9D%E5%A7%8B%E5%8C%96%E5%92%8C%E6%88%90%E5%91%98%E6%96%B9%E6%B3%95/</guid>
      <description>类的定义和初始化 Go 语言的面向对象编程与我们之前所熟悉的 PHP、Java 那一套完全不同，没有 class、extends、implements 之类的关键字和相应的概念，而是借助结构体来实现类的声明，比如要定义一个学生类，可以这么做：
package maintype Student struct {id uintname stringmale boolscore float32} 类名为 Student，并且包含了 id、name、male、score 四个属性，Go 语言中也不支持构造函数、析构函数，取而代之地，可以通过定义形如 NewXXX 这样的全局函数（首字母大写）作为类的初始化函数：
func NewStudent(id uint, name string, male bool, score float32) *Student {return &amp;amp;Student{id: id,name: name,male: male,score: score,}} 在这个函数中，我们通过传入的属性字段对 Student 类进行初始化并返回一个指向该类的指针，除此之外，还可以初始化指定字段。在 Go 语言中，未进行显式初始化的变量都会被初始化为该类型的零值，例如 bool 类型的零值为 false，int 类型的零值为 0，string 类型的零值为空字符串，float 类型的零值为 0.0。
为类添加成员方法 为go的结构体成员添加方法，需要在func和方法名之间添加所属类型的声明，以 Student 类为例，要为其添加返回 name 值的方法，可以这么做：</description>
    </item>
    
    <item>
      <title>golang基础(29.为基本类型添加成员方法)</title>
      <link>https://869413421.github.io/post/golang%E5%9F%BA%E7%A1%80/29.%E4%B8%BA%E5%9F%BA%E6%9C%AC%E7%B1%BB%E5%9E%8B%E6%B7%BB%E5%8A%A0%E6%88%90%E5%91%98%E6%96%B9%E6%B3%95/</link>
      <pubDate>Tue, 07 Feb 2023 15:52:40 +0800</pubDate>
      
      <guid>https://869413421.github.io/post/golang%E5%9F%BA%E7%A1%80/29.%E4%B8%BA%E5%9F%BA%E6%9C%AC%E7%B1%BB%E5%9E%8B%E6%B7%BB%E5%8A%A0%E6%88%90%E5%91%98%E6%96%B9%E6%B3%95/</guid>
      <description>在 Go 语言中，你可以给任意类型（包括基本类型，但不包括指针类型）添加成员方法，但是如果是基本类型的话，需要借助 type 关键字对类型进行再定义，例如：
package maintype Integer intfunc (a Integer) Equal(b Integer) bool {return a == b} :::success 注意，这个时候 Integer 已经是一个新的类型了，这与 type Integer = int 不同，后者只是为 int 类型设置一个别名。 ::: 在这个例子中，我们定义了一个新类型 Integer，它和 int 没有本质不同，只是它为内置的 int 类型增加了个新方法 Equal()。 这样一来，就可以让基本类型的整型像一个普通的类一样使用：
package mainimport &amp;#34;fmt&amp;#34;type Integer intfunc (a Integer) Equal(b Integer) bool {return a == b}func main() {var a Integer = 2if a.</description>
    </item>
    
    <item>
      <title>golang基础(3.安装go语言之旅)</title>
      <link>https://869413421.github.io/post/golang%E5%9F%BA%E7%A1%80/3.%E5%AE%89%E8%A3%85go%E8%AF%AD%E8%A8%80%E4%B9%8B%E6%97%85/</link>
      <pubDate>Tue, 07 Feb 2023 15:52:40 +0800</pubDate>
      
      <guid>https://869413421.github.io/post/golang%E5%9F%BA%E7%A1%80/3.%E5%AE%89%E8%A3%85go%E8%AF%AD%E8%A8%80%E4%B9%8B%E6%97%85/</guid>
      <description>go语言之旅是官方提供的线上实践指南，教程涵盖了基础，方法接口，并发编程等大部分重要特性。鉴于网络问题下载安装到本地直接运行相对来说更便捷。
开启 go modules go env -w GO111MODULE=on 下载运行 mkdir tourcd tourgo mod init tour go get -u github.com/Go-zh/tour tourtour </description>
    </item>
    
    <item>
      <title>golang基础(30.通过组合实现类的继承和方法重写)</title>
      <link>https://869413421.github.io/post/golang%E5%9F%BA%E7%A1%80/30.%E9%80%9A%E8%BF%87%E7%BB%84%E5%90%88%E5%AE%9E%E7%8E%B0%E7%B1%BB%E7%9A%84%E7%BB%A7%E6%89%BF%E5%92%8C%E6%96%B9%E6%B3%95%E9%87%8D%E5%86%99/</link>
      <pubDate>Tue, 07 Feb 2023 15:52:40 +0800</pubDate>
      
      <guid>https://869413421.github.io/post/golang%E5%9F%BA%E7%A1%80/30.%E9%80%9A%E8%BF%87%E7%BB%84%E5%90%88%E5%AE%9E%E7%8E%B0%E7%B1%BB%E7%9A%84%E7%BB%A7%E6%89%BF%E5%92%8C%E6%96%B9%E6%B3%95%E9%87%8D%E5%86%99/</guid>
      <description>GO不想其他类型的语言通过extends关键字来显式定义子类和父类的关系，而是通过组合的方式来实现类似的功能。显式定义继承关系的弊端有两个：一个是导致类的层级复杂，另一个是影响了类的扩展性，设计模式里面推荐的也是通过组合来替代继承提高类的扩展性。我们来看一个例子，现在有一个父类 Animal，有一个属性 name 用于表示名称，和三个成员方法，分别用来获取动物叫声、喜欢的食物和动物的名称：
package mainimport &amp;#34;fmt&amp;#34;type Animal struct {name string}func (a Animal) Call() string {return &amp;#34;动物的叫声&amp;#34;}func (a Animal) FavorFood() string {return &amp;#34;爱吃的食物&amp;#34;}func (a Animal) GetName() string {return a.name}type Dog struct {Animal}func (d Dog) FavorFood() string {return &amp;#34;骨头&amp;#34;}func (d Dog) Call() string {return &amp;#34;汪汪汪&amp;#34;}func main() {animal := Animal{&amp;#34;狗&amp;#34;}dog := Dog{animal}fmt.</description>
    </item>
    
    <item>
      <title>golang基础(31.接口的定义和实现)</title>
      <link>https://869413421.github.io/post/golang%E5%9F%BA%E7%A1%80/31.%E6%8E%A5%E5%8F%A3%E7%9A%84%E5%AE%9A%E4%B9%89%E5%92%8C%E5%AE%9E%E7%8E%B0/</link>
      <pubDate>Tue, 07 Feb 2023 15:52:40 +0800</pubDate>
      
      <guid>https://869413421.github.io/post/golang%E5%9F%BA%E7%A1%80/31.%E6%8E%A5%E5%8F%A3%E7%9A%84%E5%AE%9A%E4%B9%89%E5%92%8C%E5%AE%9E%E7%8E%B0/</guid>
      <description>接口在go中有这至关重要的地位，如果说goroutine和channel是撑起整个GO语言并发的基石，那么接口就是类型系统的基石。
传统的侵入式接口 什么是侵入式接口，接口作为不同类之间的抽象定义，它是一种契约方式的存在，只要契约存在，就必须要履行契约。通俗地讲只要继承了某一个接口，就必须去实现这个接口中的所有方法。
// 声明一个&amp;#39;iTemplate&amp;#39;接口interface iTemplate{public function setVariable($name, $var);public function getHtml($template);}// 实现接口// 下面的写法是正确的class Template implements iTemplate{private $vars = array();public function setVariable($name, $var){$this-&amp;gt;vars[$name] = $var;}public function getHtml($template){foreach($this-&amp;gt;vars as $name =&amp;gt; $value) {$template = str_replace(&amp;#39;{&amp;#39; . $name . &amp;#39;}&amp;#39;, $value, $template);}return $template;}} 这个时候如果有一个接口iTemplate2声明了与iTemplate完全一样的接口方法，甚至名字一致，只不过在不同的命名空间下，这时候编译器会认为上面的类只实现了其中某一个接口，而没有实现另一个接口。这些在我们的认知中是理所当然的，如果我们没有在代码中明确地指定接口的层级和继承自哪个接口，那么我们就没有实现这个接口。这个类和这个接口就完全没有任何关系，这些接口中的声明和实现都是显式的。我们把这种接口称之为侵入式接口，尤其是在设计标准库的时候，因为标准库必然涉及到接口设计，接口的需求方是业务实现类，只有具体编写业务实现类的时候才知道需要定义哪些方法，而在此之前，标准库的接口就已经设计好了，我们要么按照约定好的接口进行实现，如果没有合适的接口需要自己去设计，这里的问题就是接口的设计和业务的实现是分离的，接口的设计者并不能总是预判到业务方要实现哪些功能，这就造成了设计与实现的脱节。接口的过度设计会导致某些声明的方法完全不需要去实现，如果设计得太简单又无法满足业务需求。以 PHP 自带的 SessionHandlerInterface 接口为例，该接口声明的接口方法如下：
SessionHandlerInterface {/* 方法 */abstract public close ( void ) : boolabstract public destroy ( string $session_id ) : boolabstract public gc ( int $maxlifetime ) : intabstract public open ( string $save_path , string $session_name ) : boolabstract public read ( string $session_id ) : stringabstract public write ( string $session_id , string $session_data ) : bool} 用户自定义的 Session 管理器需要实现该接口，也就是要实现该接口声明的所有方法，但是实际在做业务开发的时候，某些方法其实并不需要实现，比如如果我们基于 Redis 或 Memcached 作为 Session 存储器的话，它们自身就包含了过期回收机制，所以 gc 方法根本不需要实现，又比如 close 方法对于大部分驱动来说，也是没有什么意义的。正是因为这种不合理的设计，所以在编写 PHP 类库中的每个接口时都需要纠结以下两个问题（Java 也类似）：</description>
    </item>
    
    <item>
      <title>golang基础(31.类属性和方法的可见性)</title>
      <link>https://869413421.github.io/post/golang%E5%9F%BA%E7%A1%80/31.%E7%B1%BB%E5%B1%9E%E6%80%A7%E5%92%8C%E6%96%B9%E6%B3%95%E7%9A%84%E5%8F%AF%E8%A7%81%E6%80%A7/</link>
      <pubDate>Tue, 07 Feb 2023 15:52:40 +0800</pubDate>
      
      <guid>https://869413421.github.io/post/golang%E5%9F%BA%E7%A1%80/31.%E7%B1%BB%E5%B1%9E%E6%80%A7%E5%92%8C%E6%96%B9%E6%B3%95%E7%9A%84%E5%8F%AF%E8%A7%81%E6%80%A7/</guid>
      <description>在 Go 语言中，没有类似 PHP 和 Java 那种命名空间的概念，不过 Go 语言也是通过包来管理源代码的，包往往与文件系统的目录结构存在映射关系，Go 语言在寻找变量、函数、类属性及方法的时候，会先查看 GOPATH 这个系统环境变量，然后根据该变量配置的路径列表依次去对应路径下的 src 目录下根据包名查找对应的目录，如果对应目录存在，则再到该目录下查找对应的变量、函数、类属性和方法，因此我们可以把归属于同一个目录的文件看作归属于同一个包，归属同一个包的代码具备以下特性：
归属于同一个包的代码包声明语句要一致，即同一级目录的源文件必须属于同一个包； 在同一个包下不同的不同文件中不能重复声明同一个变量、函数和类； 另外，需要注意的是 main 函数作为程序的入口函数，只能存在于 main 包中，main 包通常对应 src 目录，但也可以将其它子目录声明为 main 包。</description>
    </item>
    
    <item>
      <title>golang基础(32.接口赋值)</title>
      <link>https://869413421.github.io/post/golang%E5%9F%BA%E7%A1%80/32.%E6%8E%A5%E5%8F%A3%E8%B5%8B%E5%80%BC/</link>
      <pubDate>Tue, 07 Feb 2023 15:52:40 +0800</pubDate>
      
      <guid>https://869413421.github.io/post/golang%E5%9F%BA%E7%A1%80/32.%E6%8E%A5%E5%8F%A3%E8%B5%8B%E5%80%BC/</guid>
      <description>接口是不能直接实例化的，因为它只是一个契约的存在，只能通过具体类来实例化。但是在go中我们支持接口赋值操作，从而快速实现接口和示例的映射和转换。接口赋值有两种情况：
将实现的类实例化后赋值给接口 将一个接口赋值给另一个接口 将类赋值给接口 在go中，只要我们的某个类实现了某个接口，实例化后我们就可以将这个对象赋值给接口。
package mainimport &amp;#34;fmt&amp;#34;type Integer intfunc (i Integer) Add(a, b Integer) Integer {return a + b}func (i Integer) Multiply(b Integer) Integer {return i * b}type Math interface {Add(a, b Integer) IntegerMultiply(i Integer) Integer}func main() {var a Integer = 1var m Mathm = afmt.Println(m.Add(2, 1))} 按照 Go 语言的约定，Integer 类型实现了 Math 接口。然后我们可以这样将 Integer 类型的实例 a 直接赋值给 Math 接口类型的变量 m：</description>
    </item>
    
    <item>
      <title>golang基础(33.接口类型以及转化)</title>
      <link>https://869413421.github.io/post/golang%E5%9F%BA%E7%A1%80/33.%E6%8E%A5%E5%8F%A3%E7%B1%BB%E5%9E%8B%E4%BB%A5%E5%8F%8A%E8%BD%AC%E5%8C%96/</link>
      <pubDate>Tue, 07 Feb 2023 15:52:40 +0800</pubDate>
      
      <guid>https://869413421.github.io/post/golang%E5%9F%BA%E7%A1%80/33.%E6%8E%A5%E5%8F%A3%E7%B1%BB%E5%9E%8B%E4%BB%A5%E5%8F%8A%E8%BD%AC%E5%8C%96/</guid>
      <description>在其他编程语言中，我们可以使用instanceof关键字来判断某个对象是否属于某个类（包括他的父类），而再GO语言中也同样有这样的机制。
接口的断言 package mainimport &amp;#34;fmt&amp;#34;type String1 interface {func1(s string)func2(s string)}type String2 interface {func1(s string)}type String stringfunc (s String) func1(str string) {fmt.Println(str)}func (s String) func2(str string) {}func main() {var s String = &amp;#34;test&amp;#34;var s1 String2 = sif s2, ok := s1.(String2); ok {s2.func1(&amp;#34;test2&amp;#34;)}} 上述代码中我们定义了两个接口`String1`和`String2`,其中`String1`作为`String2`的子集。然后定义了一个String类型，实现了这两个接口。在main中我们实例化了一个`String`类型，然后讲其赋值给`String2`接口得到变量`s1`,通过`变量.(类型)`的方式断言变量是否属于`String2`接口。如果是，ok 值为 true，然后执行 if 语句块中的代码；否则 ok 值为 false，不执行 if 语句块中的代码。需要注意的是，类型断言是否成功要在运行期才能够确定，它不像接口赋值，编译器只需要通过静态类型检查即可判断赋值是否可行。结构体类型断言 结构体类型断言实现语法和接口类型断言一样，基本一致。由于类型断言语法 .</description>
    </item>
    
    <item>
      <title>golang基础(34.空接口，反射，泛型)</title>
      <link>https://869413421.github.io/post/golang%E5%9F%BA%E7%A1%80/34.%E7%A9%BA%E6%8E%A5%E5%8F%A3%E5%8F%8D%E5%B0%84%E6%B3%9B%E5%9E%8B/</link>
      <pubDate>Tue, 07 Feb 2023 15:52:40 +0800</pubDate>
      
      <guid>https://869413421.github.io/post/golang%E5%9F%BA%E7%A1%80/34.%E7%A9%BA%E6%8E%A5%E5%8F%A3%E5%8F%8D%E5%B0%84%E6%B3%9B%E5%9E%8B/</guid>
      <description>空接口的引入 Go 语言打破了传统面向对象编程中类与类之间继承的概念，而是通过组合实现方法和属性的复用，所以不存在类似的继承关系树，也就没有所谓的祖宗类，而且类与接口之间也不再通过 implements 关键字强制绑定实现关系，所以 Go 语言的面向对象编程非常灵活。在 Go 语言中，类与接口的实现关系是通过类所实现的方法在编译期推断出来的，如果我们定义一个空接口的话，那么显然所有的类都实现了这个接口，反过来，我们也可以通过空接口来指向任意类型，从而实现类似 Java 中 Object 类所承担的功能，而且显然 Go 的空接口实现更加简洁，通过一个简单的字面量即可完成：
interface{} :::warning 需要注意的是空接口和接口零值不是一个概念，前者是 interface{}，后者是 nil。 :::
空接口的基本使用 指向任意类型
package mainfunc main() {var a interface{} = 1var b interface{} = &amp;#34;sss&amp;#34;var c interface{} = 1.11var d interface{} = truevar e interface{} = []int{1, 2, 3}var f interface{} = [2]int{1, 2}var g interface{} = struct {id int}{id: 1}} 声明任意类型参数func test(args .</description>
    </item>
    
    <item>
      <title>golang基础(35.通过高阶函数来实现装饰器模式)</title>
      <link>https://869413421.github.io/post/golang%E5%9F%BA%E7%A1%80/35.%E9%80%9A%E8%BF%87%E9%AB%98%E9%98%B6%E5%87%BD%E6%95%B0%E6%9D%A5%E5%AE%9E%E7%8E%B0%E8%A3%85%E9%A5%B0%E5%99%A8%E6%A8%A1%E5%BC%8F/</link>
      <pubDate>Tue, 07 Feb 2023 15:52:40 +0800</pubDate>
      
      <guid>https://869413421.github.io/post/golang%E5%9F%BA%E7%A1%80/35.%E9%80%9A%E8%BF%87%E9%AB%98%E9%98%B6%E5%87%BD%E6%95%B0%E6%9D%A5%E5%AE%9E%E7%8E%B0%E8%A3%85%E9%A5%B0%E5%99%A8%E6%A8%A1%E5%BC%8F/</guid>
      <description>高阶函数 高阶函数是指，接收其他函数作为参数或者作为返回值的函数。将匿名函数作为参数传入或者将匿名函数作为返回值，这都是高阶函数的一种。
装饰器模式 装饰器模式，顾名思义其特征在于装饰。在编程语言中它代表着在程序原有的基础上，在不侵入其代码，为其添加更多的功能。
通过高阶函数实现装饰器 package mainimport (&amp;#34;fmt&amp;#34;&amp;#34;time&amp;#34;)// AddFunc 定义一个方法类型type AddFunc func(int, int) int//Add 基础的Add方法func Add(a, b int) int {return a + b}//AddDecorator 装饰器方法func AddDecorator(f AddFunc) AddFunc {return func(a, b int) int {start := time.Now() // 起始时间c := f(a, b) // 执行乘法运算函数end := time.Since(start) // 函数执行完毕耗时fmt.Printf(&amp;#34;--- 执行耗时: %v ---&amp;#34;, end)return c // 返回计算结果}}func main() {a := 8b := 10decorator := AddDecorator(Add)result := decorator(a, b)fmt.</description>
    </item>
    
    <item>
      <title>golang基础(36.error 接口及其使用)</title>
      <link>https://869413421.github.io/post/golang%E5%9F%BA%E7%A1%80/36.error-%E6%8E%A5%E5%8F%A3%E5%8F%8A%E5%85%B6%E4%BD%BF%E7%94%A8/</link>
      <pubDate>Tue, 07 Feb 2023 15:52:40 +0800</pubDate>
      
      <guid>https://869413421.github.io/post/golang%E5%9F%BA%E7%A1%80/36.error-%E6%8E%A5%E5%8F%A3%E5%8F%8A%E5%85%B6%E4%BD%BF%E7%94%A8/</guid>
      <description>PHP错误处理以及异常处理一直比较混乱，在 PHP 5 中是通过 error_reporting 函数设置错误报告级别，然后通过 set_error_handler 函数注册全局的错误处理器。PHP中抛弃了错误的报告方式，转而通过抛出将错误当做异常抛出，可以通过try catch语句进行捕获，还可以通过 set_exception_handler 注册全局异常处理器，将应用中未处理的异常统一兜底。
GO语言错误处理机制 GO语言的错误处理机制相对来说比较简洁，它提供了一个标准的error接口。
type error interface { Error() string } 其中`error`接口值只有一个标准方法，`Error()`,用于返回错误信息。在大多数函数中一般将一个`error`作为一个返回值，交由上级调用来进行判断。func Foo(param int) (n int, err error) { // ...} 然后在调用返回错误信息的函数/方法时，按照如下「卫述语句」模板编写处理代码即可：
n, err := Foo(0)if err != nil { // 错误处理 } else {// 使用返回值 n } 错误消息返回及处理 我们可以使用errors.new()方法返回我们需要自定的错误信息。
func add(a, b *int) (c int, err error) {if (*a &amp;lt; 0 || *b &amp;lt; 0) {err = errors.</description>
    </item>
    
    <item>
      <title>golang基础(37.defer 语句及使用示例)</title>
      <link>https://869413421.github.io/post/golang%E5%9F%BA%E7%A1%80/37.defer-%E8%AF%AD%E5%8F%A5%E5%8F%8A%E4%BD%BF%E7%94%A8%E7%A4%BA%E4%BE%8B/</link>
      <pubDate>Tue, 07 Feb 2023 15:52:40 +0800</pubDate>
      
      <guid>https://869413421.github.io/post/golang%E5%9F%BA%E7%A1%80/37.defer-%E8%AF%AD%E5%8F%A5%E5%8F%8A%E4%BD%BF%E7%94%A8%E7%A4%BA%E4%BE%8B/</guid>
      <description>在go中当我们使用完某个资源需要将其释放，比如(网络连接，文件句柄)，或者在代码运行过程中抛出错误时执行一段兜底逻辑，要怎么做呢？通过 defer 关键字声明兜底执行或者释放资源的语句可以轻松解决这个问题。比如我们看 Go 内置的 io/ioutil 包中提供的读取文件方法 ReadFile 实现源码，其中就有 defer 语句的使用：
func ReadFile(filename string) ([]byte, error) {f, err := os.Open(filename)if err != nil {return nil, err}defer f.Close()var n int64 = bytes.MinReadif fi, err := f.Stat(); err == nil {if size := fi.Size() + bytes.MinRead; size &amp;gt; n {n = size}}return readAll(f, n)} defer 修饰的 f.Close() 方法会在函数执行完成后或读取文件过程中抛出错误时执行，以确保已经打开的文件资源被关闭，从而避免内存泄露。如果一条语句干不完清理的工作，也可以在 defer 后加一个匿名函数来执行对应的兜底逻辑：</description>
    </item>
    
    <item>
      <title>golang基础(38.panic和recover)</title>
      <link>https://869413421.github.io/post/golang%E5%9F%BA%E7%A1%80/38.panic%E5%92%8Crecover/</link>
      <pubDate>Tue, 07 Feb 2023 15:52:40 +0800</pubDate>
      
      <guid>https://869413421.github.io/post/golang%E5%9F%BA%E7%A1%80/38.panic%E5%92%8Crecover/</guid>
      <description>GO语言通过error接口进行统一的错误处理，这些都是我们在编写代码时就可以预见并且返回的。但是面对一些我们不可知的，比如数组越界、除数为0、空指针引用，这些 Go 语言是怎么处理的呢？
panic Go 语言没有像 PHP 那样引入异常的概念，也没有提供 try&amp;hellip;catch 这样的语法对运行时异常进行捕获和处理，当代码运行时出错，而又没有在编码时显式返回错误时，Go 语言会抛出 panic，中文译作「运行时恐慌」，我们也可以将其看作 Go 语言版的异常。除了 Go 语言底层抛出 panic，我们还可以在代码中显式抛出 panic，以便对错误和异常信息进行自定义，仍然以上篇教程除数为0的示例代码为例，我们可以这样显式返回 panic 中断代码执行：
package mainimport &amp;#34;fmt&amp;#34;func main() {defer func() {fmt.Println(&amp;#34;代码清理逻辑&amp;#34;)}()var i = 1var j = 0if j == 0 {panic(&amp;#34;除数不能为0！&amp;#34;)}k := i / jfmt.Printf(&amp;#34;%d / %d = %d&amp;#34;, i, j, k)} 这样，当我们执行这段代码时，就会抛出 panic：panic 函数支持的参数类型是 interface{}：
func panic(v interface{}) 所以可以传入任意类型的参数：</description>
    </item>
    
    <item>
      <title>golang基础(39.多进程，多线程，携程)</title>
      <link>https://869413421.github.io/post/golang%E5%9F%BA%E7%A1%80/39.%E5%A4%9A%E8%BF%9B%E7%A8%8B%E5%A4%9A%E7%BA%BF%E7%A8%8B%E6%90%BA%E7%A8%8B/</link>
      <pubDate>Tue, 07 Feb 2023 15:52:40 +0800</pubDate>
      
      <guid>https://869413421.github.io/post/golang%E5%9F%BA%E7%A1%80/39.%E5%A4%9A%E8%BF%9B%E7%A8%8B%E5%A4%9A%E7%BA%BF%E7%A8%8B%E6%90%BA%E7%A8%8B/</guid>
      <description>为什么需要并发编程 在PHP中并不存在并发的概念，PHP中所有的操作都是串行执行，同步阻塞的。这就是很多人诟病PHP性能低下的原因。但串行执行虽然性能上存在问题，但是相对的也有它的好处。
保证了PHP的简单性，不需要考虑并发引入的线程安全问题 不需要考虑加锁来保证某个操作的原子性 不存在线程间的通讯问题 与并发相对的是串行，即按照代码顺序一步一步往下执行，当遇到某个IO操作时，比如发送邮件，读取文件，查询数据库。CPU会进行等待，等到IO操作完成后才会继续执行代码。这种情况在某些要求高并发高性能的业务场景显然是不合适的，从操作系统上来讲，多个任务是可以同时执行的，因为CPU本身就是多核的，能同时执行多任务的计算。哪怕是单核CPU，也可利用时间分片的方式在多个进程和线程之间来回切换执行。比如说当某个任务执行时遇到了IO操作，这个时候CPU不会一直傻傻等待，而是挂起这个任务，让出CPU时间片给到其他任务。然后等这个IO操作完成后，通知CPU恢复后续代码的执行。实际上CPU大部分时间都在做这种调度，并发编程就是最大程度的压榨CPU，从而提高程序的性能和效率 。
并发编程的常见实现 多进程。多进程是基于操作系统层面的并发基本模式，同时也是开销最大的模式。在linux上很多工具都采用这种模式在工作，比如PHP-FPM,他有专门的主进程监听端口以及管理连接，还有多个工作进程对具体请求进行处理。这种方式好处是在于简单，进程间互相不影响，不同进程间数据相互隔离。缺点是系统开销大，每个进程都是由内核管理的。 多线程。多线层是基于系统层面的并发模式，它是基于进程内的，也是使用较多也相对有效的方式。线程比进程开销更小，线程间会共享数据，线程切换和调度会加锁会造成额外的性能开销。线程比进程轻量，但在高并发的情况下效率依然有影响，例如C10K问题，即支持一万个并发需要一万个线程，这样对系统资源有较高的要求，而且CPU管理这些线程带来巨大负担 携程。一种用户态线程，可以交由程序员调度的，你可以将其看作是轻量级别的线程，不许要操作系统来进行抢占式调度，系统开销极小，携程内有自己独立的堆栈调度间没有线程的加锁开销。 基于回调的非阻塞IO/异步IO。为了解决C10K的问题，在很多高并发的开发实践中，都会通过事件驱动的方式来使用异步IO,在这种模式下，一个线程维护多个Socket连接，从而降低了系统的开销。 传统并发模式的缺陷 在串行化模式下执行的程序，所有的事务都具备确定性的，比如程序预设了123个步骤，代码会严格按照顺序执行下去，即使在某哥步骤中阻塞了，也会一直等待代码执行结束才会进行下一步。多线程的并发模式下，就彻底打破了这种缺定性。比如我们原先的123，第2步是一个耗时操作，这时候我们启动了一个新的线程对其进行处理，这时候我们无法确定的是，主线程拉起2的子线程后继续往下执行代码，我们无法确定是主线程先执行完毕退出程序，还是2的线程先完成。如果是主线程完成退出会导致2的子线程操作中断。或者我们在第3步的时候依赖第2步的某个返回结果，我们不知道啥时候能够返回这个结果，如果第2、3步有相互依赖的变量，甚至可能出现死锁，以及我们如何在主线程中获取新线程的异常和错误信息并进行相应的处理，等等，这种不确定性给程序的行为带来了意外和危害，也让程序变得不可控。不同的线程好比平行时空，我们需要通过线程间通信来告知不同线程目前各自运行的状态和结果，以便使程序可控，线程之间通信可以通过共享内存的方式（参考 Swoole 中的 Swoole Table），即在不同线程中操作的是同一个内存地址上存储的值。为了保证共享内存的有效性，需要采取很多措施，比如加锁来避免死锁或资源竞争，还是以上面的主线程和新线程为例，如果我们在第1步获取了一个中间结果，第2步和第3步都要对这个中间结果进行操作，如果不加锁保证操作的原子性，很有可能产生脏数据。诸如此类的问题在生产环境极有可能造成重大故障甚至事故，而且不易察觉和调试。我们可以将线程加共享内存的方式称为「共享内存系统」。为了解决共享内存系统存在的问题，计算机科学家们又提出了「消息传递系统」。「消息传递系统」指的是将线程间共享状态的各种操作都封装在线程之间传递的消息中，这通常要求发送消息时对状态进行复制，并且在消息传递的边界上交出这个状态的所有权。从表明上来看，这个操作与「共享内存系统」中执行的通过加锁实现原子更新操作相同，但从底层实现上来看则不同：一个对同一个内存地址持有的值进行操作，一个是从消息通道读取数据并处理。由于需要执行状态复制操作，所以大多数消息传递的实现在性能上并不优越，但线程中的状态管理工作则会变得更加简单，这就有点像我们在开篇讲 PHP 不支持并发编程提到的那样，如果想让编码简单，性能就要做牺牲，如果想追求性能，代码编写起来就比较费劲，这也是我们为什么通常不会直接通过事件驱动的异步 IO 来实现并发编程一样，因为这涉及到直接调用操作系统底层的库函数（select、epoll、libevent 等）来实现，非常复杂。
Go 语言协程支持 与传统的系统级线程和进程相比，协程的最大优势在于轻量级（可以看作用户态的轻量级线程），我们可以轻松创建上百万个协程而不会导致系统资源衰竭，而线程和进程通常最多也不能超过 1 万个（C10K问题）。多数语言在语法层面并不直接支持协程，而是通过库的方式支持，比如 PHP 的 Swoole 扩展库，但用库的方式支持的功能通常并不完整，比如仅仅提供轻量级线程的创建、销毁与切换等能力。如果在这样的轻量级线程中调用一个同步 IO 操作，比如网络通信、本地文件读写，都会阻塞其他的并发执行轻量级线程，从而无法真正达到轻量级线程本身期望达到的目标。Go 语言在语言级别支持协程，称之为 goroutine。Go 语言标准库提供的所有系统调用操作（当然也包括所有同步 IO 操作），都有协程的身影。协程间的切换管理不依赖于系统的线程和进程，也不依赖于 CPU 的核心数量，这让我们在 Go 语言中通过协程实现并发编程变得非常简单。</description>
    </item>
    
    <item>
      <title>golang基础(4.代码包)</title>
      <link>https://869413421.github.io/post/golang%E5%9F%BA%E7%A1%80/4.%E4%BB%A3%E7%A0%81%E5%8C%85/</link>
      <pubDate>Tue, 07 Feb 2023 15:52:40 +0800</pubDate>
      
      <guid>https://869413421.github.io/post/golang%E5%9F%BA%E7%A1%80/4.%E4%BB%A3%E7%A0%81%E5%8C%85/</guid>
      <description>程序入口包 在go程序中，包作为基本的代码组织单位，其中main包是程序的执行的入口。
导入包 使用import可以对代码包进行导入，导入后可以在当前包中使用导入包的公开代码
package mainimport (&amp;#34;fmt&amp;#34;&amp;#34;math&amp;#34;)func main() {fmt.Printf(&amp;#34;Now you have %g problems.&amp;#34;, math.Sqrt(7))} 导出包 一个包中如果包中的方法名或者变量或者其他成员的命名以大写开头，意味着它是可以被其他包导入访问的。如果以小写开头，只能在当前包中进行访问。
package mainimport (&amp;#34;fmt&amp;#34;&amp;#34;math&amp;#34;)func main() {//会报错，不可访问fmt.Println(math.pi)//正常，可以访问fmt.Println(math.Pi)} </description>
    </item>
    
    <item>
      <title>golang基础(40.GO携程实现原理)</title>
      <link>https://869413421.github.io/post/golang%E5%9F%BA%E7%A1%80/40.go%E6%90%BA%E7%A8%8B%E5%AE%9E%E7%8E%B0%E5%8E%9F%E7%90%86/</link>
      <pubDate>Tue, 07 Feb 2023 15:52:40 +0800</pubDate>
      
      <guid>https://869413421.github.io/post/golang%E5%9F%BA%E7%A1%80/40.go%E6%90%BA%E7%A8%8B%E5%AE%9E%E7%8E%B0%E5%8E%9F%E7%90%86/</guid>
      <description>GO并发编程原理 GO语言的携程实现称之为goroutine,由GO运行时管理。我们可以在一个处理进程中通过关键字go来启动多个携程，然后在携程中完成不同的子任务。这些用户在代码中穿件和维护的携程本质上是用户级别的线程，GO语言运行时会在底层通过调度器将用户线程交给系统级别的线程处理，如果在运行过程当中遇到某个IO操作暂停运行，调度器会将用户级线程和系统级线程剥离，以便让系统级别的线程去处理其他用户级别的线程。当用户级线程IO操作完成后，调度器又会调用其他空闲的系统级线程对其进行处理。从而达到并发处理多个携程的目的。此外调度器还会在系统级别线程数量不足的时候向操作系统申请创建新的系统级别线程，而在系统线程过多的情况下也会自动销毁一些空闲的系统级线程，实际上这也是很多进程/线程池管理器的工作机制，这样一来，可以保证对系统资源的高效利用，避免系统资源的浪费。</description>
    </item>
    
    <item>
      <title>golang基础(41.基于共享内存的携程通信)</title>
      <link>https://869413421.github.io/post/golang%E5%9F%BA%E7%A1%80/41.%E5%9F%BA%E4%BA%8E%E5%85%B1%E4%BA%AB%E5%86%85%E5%AD%98%E7%9A%84%E6%90%BA%E7%A8%8B%E9%80%9A%E4%BF%A1/</link>
      <pubDate>Tue, 07 Feb 2023 15:52:40 +0800</pubDate>
      
      <guid>https://869413421.github.io/post/golang%E5%9F%BA%E7%A1%80/41.%E5%9F%BA%E4%BA%8E%E5%85%B1%E4%BA%AB%E5%86%85%E5%AD%98%E7%9A%84%E6%90%BA%E7%A8%8B%E9%80%9A%E4%BF%A1/</guid>
      <description>package mainimport (&amp;#34;fmt&amp;#34;&amp;#34;runtime&amp;#34;&amp;#34;sync&amp;#34;&amp;#34;time&amp;#34;)var counter int = 0var lock *sync.Mutexfunc add(a, b int) {c := a + block.Lock()counter++lock.Unlock()fmt.Printf(&amp;#34;%d: %d + %d = %d&amp;#34;, counter, a, b, c)}func main() {startTime := time.Now()lock = &amp;amp;sync.Mutex{}for i := 0; i &amp;lt; 10; i++ {go add(1, i)}for {lock.Lock()c := counterlock.</description>
    </item>
    
    <item>
      <title>golang基础(42.基于消息的携程通信)</title>
      <link>https://869413421.github.io/post/golang%E5%9F%BA%E7%A1%80/42.%E5%9F%BA%E4%BA%8E%E6%B6%88%E6%81%AF%E7%9A%84%E6%90%BA%E7%A8%8B%E9%80%9A%E4%BF%A1/</link>
      <pubDate>Tue, 07 Feb 2023 15:52:40 +0800</pubDate>
      
      <guid>https://869413421.github.io/post/golang%E5%9F%BA%E7%A1%80/42.%E5%9F%BA%E4%BA%8E%E6%B6%88%E6%81%AF%E7%9A%84%E6%90%BA%E7%A8%8B%E9%80%9A%E4%BF%A1/</guid>
      <description>通道 Go 语言推荐使用消息传递实现并发通信，这种消息通信机制被称为 channel，中文译作「通道」，可理解为传递消息的通道。通道是 Go 语言在语言级别提供的协程通信方式，它是一种数据类型，本身是并发安全的，我们可以使用它在多个 goroutine 之间传递消息，而不必担心通道中的值被污染。通道是一种数据类型，和数组/切片类型类似，一个通道只能传递一种类型的值，这个类型需要在声明 通道时指定。在使用通道时，需要通过 make 进行声明，通道对应的类型关键字是 chan：
ch := make(chan int) 我们可以把通道看作是一个先进先出（FIFO）的队列，通道中的元素会严格按照发送顺序排列，继而按照排列顺序被接收，通道元素的发送和接收都可以通过 &amp;lt;- 操作符来实现，发送时元素值在右，通道变量在左：
ch &amp;lt;- 1 // 表示把元素 1 发送到通道 ch 接收时通道变量在右，可以通过指定变量接收元素值：
element := &amp;lt;-ch 也可以留空表示忽略：
&amp;lt;-ch package mainimport (&amp;#34;fmt&amp;#34;&amp;#34;time&amp;#34;)func channelAdd(a, b int, ch chan int) {c := a + bfmt.Printf(&amp;#34;%d+%d = %d &amp;#34;, a, b, c)ch &amp;lt;- c}func main() {start := time.Now()chs := make([]chan int, 10)for i := 0; i &amp;lt; 10; i++ {chs[i] = make(chan int)go channelAdd(1, i, chs[i])}for _, ch := range chs {&amp;lt;- ch}end := time.</description>
    </item>
    
    <item>
      <title>golang基础(43.通道基本语法和缓冲通道)</title>
      <link>https://869413421.github.io/post/golang%E5%9F%BA%E7%A1%80/43.%E9%80%9A%E9%81%93%E5%9F%BA%E6%9C%AC%E8%AF%AD%E6%B3%95%E5%92%8C%E7%BC%93%E5%86%B2%E9%80%9A%E9%81%93/</link>
      <pubDate>Tue, 07 Feb 2023 15:52:40 +0800</pubDate>
      
      <guid>https://869413421.github.io/post/golang%E5%9F%BA%E7%A1%80/43.%E9%80%9A%E9%81%93%E5%9F%BA%E6%9C%AC%E8%AF%AD%E6%B3%95%E5%92%8C%E7%BC%93%E5%86%B2%E9%80%9A%E9%81%93/</guid>
      <description>通道声明和初始化 可以通过 chan类型关键字来声明通道类型变量：
var ch chan int 上面这个表达式表示声明一个通道类型变量 ch，并且通道中只能传递 int 类型数据。与其他数据类型不同，通道类型变量除了声明通道类型本身外，还要声明通道中传递数据的类型，比如这里我们指定这个数据类型为 int 。通道是类型相关的，我们必须在声明通道的时候同时指定通道中传递数据的类型，并且一个通道只能传递一种类型的数据，这一点和数组/切片类似。我们还可以通过如下方式声明通道数组、切片、字典，以下声明方式表示chs中的元素都是 chan int 类型的通道：
var chs [10]chan intvar chs []chan intvar chs map[string]chan int 不过，实际编码时，我们更多使用的是下面这种快捷方式同时声明和初始化通道类型：
ch := make(chan int) 由于在 Go 语言中，通道也是引用类型（和切片、字典一样），所以可以通过 make函数进行初始化，在通过 make 函数初始化通道时，还可以传递第二个参数，表示通道的容量：
ch := make(chan int,10) 第二个参数是可选的，用于指定通道最多可以缓存多少个元素，默认值是 0，此时通道可以被称作非缓冲通道，`表示往通道中发送一个元素后，只有该元素被接收后才能存入下一个元素`，与之相对的，当缓存值大于 0 时，通道可以称作缓冲通道，即使通道元素没有被接收，也可以继续往里面发送元素，直到超过缓冲值，显然设置这个缓冲值可以提高通道的操作效率。通道操作符 通道类型变量只支持发送和接收操作，即往通道中写入数据和从通道中读取数据，对应的操作符都是 &amp;lt;-，我们判断是发送还是接收操作的依据是通道类型变量位于 &amp;lt;- 左侧还是右侧，位于左侧是发送操作，位于右侧是接收操作：
ch &amp;lt;- 1 // 往通道中写入数据 1x := &amp;lt;- ch // 从通道中读取数据并赋值给变量 当我们将数据发送到通道时，发送的是数据的副本，同理，从通道中接收数据时，接收的也是数据的副本。发送和接收操作都是原子操作，同时只能进行发送或接收操作，不存在数据发送一半被接收，或者接收一半发送新数据的情况，并且两者都是阻塞的，如果通道中没有数据，进行读取操作的话会导致读取操作所在的协程阻塞，直到通道中写入了数据；反过来，如果通道中已经有了数据，再往里面写入数据的话，也会导致写入操作所在的协程阻塞，直到其中的数据被其他协程接收。
使用缓冲通道提升性能 上面这种情况发生在非缓冲通道中，对于缓冲通道，情况略有不同，假设 ch 是通过 make(chan int, 10) 进行初始化的通道，则其缓冲区大小是 10，这意味着，在没有被任何其他协程接收的情况下，我们可以一直往 ch 通道中写入 10 个数据，超过 10 个数据才会阻塞当前协程，直到通道被其他协程读取，显然，合理设置缓冲区可以提高通道的操作效率，尤其是在需要持续传输大量数据的场景。我们可以通过如下示例代码简单测试下通道的缓冲机制：</description>
    </item>
    
    <item>
      <title>golang基础(44.单向通道)</title>
      <link>https://869413421.github.io/post/golang%E5%9F%BA%E7%A1%80/44.%E5%8D%95%E5%90%91%E9%80%9A%E9%81%93/</link>
      <pubDate>Tue, 07 Feb 2023 15:52:40 +0800</pubDate>
      
      <guid>https://869413421.github.io/post/golang%E5%9F%BA%E7%A1%80/44.%E5%8D%95%E5%90%91%E9%80%9A%E9%81%93/</guid>
      <description>通常，管道都是支持双向操作的：既可以往管道发送数据，也可以从管道接收数据。但在某些场景下，可能我们需要限制只能往管道发送数据，或者只能从管道接收数据，这个时候，就需要用到单向通道。通道本身还是要支持读写的，如果某个通道只支持写入操作，那么即便数据写进去了，不能被读取也毫无意义，同理，如果某个通道只支持读取操作，不能写入数据，那么通道永远是空的，从一个空的通道读取数据会导致协程的阻塞，无法执行后续代码。Go 语言支持的单向管道，实际上是在使用层面对通道进行限制，而不是语法层面：即我们在某个协程中只能对通道进行写入操作，而在另一个协程中只能对该通道进行读取操作。从这个层面来说，单向通道的作用是约束在生产协程中只能发送数据到通道，而在消费协程中只能从通道接收数据，从而让代码遵循「最小权限原则」，避免误操作和通道使用的混乱，让代码更加稳健。当我们将一个通道类型变量传递到一个函数时（通常是在另外一个协程中执行），如果这个函数只能发送数据到通道，可以通过如下将其指定为单向只写通道（发送通道）：
func test(ch chan&amp;lt;- int) 上述代码限定在 test 函数中只能写入 int 类型数据到通道 ch。反过来，如果我们将一个通道类型变量传递到一个只允许从该通道读取数据的函数，可以通过如下方式将通道指定为单向只读通道（接收通道）：
func test(ch &amp;lt;-chan int) 虽然我们也可以像声明正常通道类型那样声明单向通道，但我们一般不这么做，因为这样一来，就是从语法上限定通道的操作类型了，对于只读通道只能接收数据，对于只写通道只能发送数据：
var ch1 chan intvar ch2 chan&amp;lt;- int var ch3 &amp;lt;-chan int 单向通道的初始化和双向通道一样：ch1 := make(chan int)ch2 := make(chan&amp;lt;- int)ch3 := make(&amp;lt;-chan int) 此外，我们还可以通过如下方式实现双向通道和单向通道的转化：ch1 := make(chan int) ch2 := &amp;lt;-chan int(ch1)ch3 := chan&amp;lt;- int(ch1) 基于双向通道 ch1，我们通过类型转化初始化了两个单向通道：单向只读的 ch2 和单向只写的 ch3。注意这个转化是不可逆的，双向通道可以转化为任意类型的单向通道，但单向通道不能转化为双向通道，读写通道之间也不能相互转化。实际上，我们在将双向通道传递到限定通道参数操作类型的函数时，就应用到了类型转化。合理使用单向通道，可以有效约束不同业务对通道的操作，避免越权使用和滥用，此外，也提高了代码的可读性，一看函数参数就可以判断出业务对通道的操作类型。</description>
    </item>
    
    <item>
      <title>golang基础(45.使用select等待通道就绪)</title>
      <link>https://869413421.github.io/post/golang%E5%9F%BA%E7%A1%80/45.%E4%BD%BF%E7%94%A8select%E7%AD%89%E5%BE%85%E9%80%9A%E9%81%93%E5%B0%B1%E7%BB%AA/</link>
      <pubDate>Tue, 07 Feb 2023 15:52:40 +0800</pubDate>
      
      <guid>https://869413421.github.io/post/golang%E5%9F%BA%E7%A1%80/45.%E4%BD%BF%E7%94%A8select%E7%AD%89%E5%BE%85%E9%80%9A%E9%81%93%E5%B0%B1%E7%BB%AA/</guid>
      <description>Go 语言还支持通过 select 分支语句选择指定分支代码执行，select 语句和之前介绍的 switch 语句语法结构类似，不同之处在于 select 的每个 case 语句必须是一个通道操作，要么是发送数据到通道，要么是从通道接收数据，此外 select 语句也支持 default 分支：
select { case &amp;lt;-chan1:// 如果从 chan1 通道成功接收数据，则执行该分支代码case chan2 &amp;lt;- 1:// 如果成功向 chan2 通道成功发送数据，则执行该分支代码 default:// 如果上面都没有成功，则进入 default 分支处理流程 } :::warning 注：Go 语言的 select 语句借鉴自 Unix 的 select() 函数，在 Unix 中，可以通过调用 select() 函数来监控一系列的文件句柄，一旦其中一个文件句柄发生了 IO 动作，该 select() 调用就会被返回（C 语言中就是这么做的），后来该机制也被用于实现高并发的 Socket 服务器程序。Go 语言直接在语言级别支持 select 关键字，用于处理并发编程中通道之间异步 IO 通信问题。 ::: 可以看出，select 不像 switch，case 后面并不带判断条件，而是直接去查看 case 语句，每个 case 语句都必须是一个面向通道的操作，比如上面的示例代码中，第一个 case 试图从 chan1 接收数据并直接忽略读到的数据，第二个 case 试图向 chan2 通道发送一个整型数据 1，需要注意的是这两个 case 的执行不是 if&amp;hellip;else&amp;hellip; 那种先后关系，而是会并发执行，然后 select 会选择先操作成功返回的那个 case 分支去执行，如果两者同时返回，则随机选择一个执行，如果这两者都没有返回，则进入 default 分支，这里也不会出现阻塞，如果 chan1 通道为空，或者 chan2 通道已满，就会立即进入 default 分支，但是如果没有 default 语句，则会阻塞直到某个通道操作成功。这些通道操作是并发的，任何一个操作成功，就会进入该分支执行代码，否则程序就会处于挂起状态，如果要实现非阻塞操作，可以引入 default 语句。下面我们基于 select 语句来实现一个简单的示例代码：</description>
    </item>
    
    <item>
      <title>golang基础(46.通道错误和异常处理)</title>
      <link>https://869413421.github.io/post/golang%E5%9F%BA%E7%A1%80/46.%E9%80%9A%E9%81%93%E9%94%99%E8%AF%AF%E5%92%8C%E5%BC%82%E5%B8%B8%E5%A4%84%E7%90%86/</link>
      <pubDate>Tue, 07 Feb 2023 15:52:40 +0800</pubDate>
      
      <guid>https://869413421.github.io/post/golang%E5%9F%BA%E7%A1%80/46.%E9%80%9A%E9%81%93%E9%94%99%E8%AF%AF%E5%92%8C%E5%BC%82%E5%B8%B8%E5%A4%84%E7%90%86/</guid>
      <description>在并发编程的通信过程中，最需要处理的就是超时问题：比如向通道发送数据时发现通道已满，或者从通道接收数据时发现通道为空。如果不正确处理这些情况，很可能会导致整个协程阻塞并产生死锁。此外，如果我们试图向一个已经关闭的通道发送数据或关闭已经关闭的通道，也会引发 panic。以上都是我们在使用通道进行并发通信时需要尤其注意的。
超时处理机制实现 Go 语言没有提供直接的超时处理机制，但我们可以借助 select 语句来实现类似机制解决超时问题，因为 select 语句的特点是只要其中一个 case 对应的通道操作已经完成，程序就会继续往下执行，而不会考虑其他 case 的情况。基于此特性，我们来为通道操作实现超时处理机制。
package mainimport (&amp;#34;fmt&amp;#34;&amp;#34;time&amp;#34;)func main() {// 1.创建生产管道ch := make(chan int, 10)// 2.创建超时管道timeout := make(chan bool)// 3.开启超时携程go func() {time.Sleep(time.Minute)timeout &amp;lt;- true}()// 4.使用select结束阻塞select {case &amp;lt;-ch:fmt.Println(&amp;#34;接收到数据&amp;#34;)case &amp;lt;-timeout:fmt.Println(&amp;#34;执行超时！&amp;#34;)}} 使用 select 语句可以避免永久等待的问题，因为程序会在从 timeout 通道中接收到数据后继续执行，无论对 ch 的读取是否还处于等待状态，从而实现 1 秒超时的效果。这种写法看起来是一个编程小技巧，但却是在 Go 语言并发编程中避免通道通信超时的最有效方法。
避免对已关闭通道进行操作 为了避免对已关闭通道再度执行关闭操作引发 panic，一般我们约定只能在发送方关闭通道，而在接收方，我们则通过通道接收操作返回的第二个参数是否为 false 判定通道是否已经关闭，如果已经关闭，则不再执行发送操作。</description>
    </item>
    
    <item>
      <title>golang基础(47.利用多核 CPU 实现并行计算)</title>
      <link>https://869413421.github.io/post/golang%E5%9F%BA%E7%A1%80/47.%E5%88%A9%E7%94%A8%E5%A4%9A%E6%A0%B8-cpu-%E5%AE%9E%E7%8E%B0%E5%B9%B6%E8%A1%8C%E8%AE%A1%E7%AE%97/</link>
      <pubDate>Tue, 07 Feb 2023 15:52:40 +0800</pubDate>
      
      <guid>https://869413421.github.io/post/golang%E5%9F%BA%E7%A1%80/47.%E5%88%A9%E7%94%A8%E5%A4%9A%E6%A0%B8-cpu-%E5%AE%9E%E7%8E%B0%E5%B9%B6%E8%A1%8C%E8%AE%A1%E7%AE%97/</guid>
      <description>开始之前，我们先澄清两个概念，「多核」指的是有效利用 CPU 的多核提高程序执行效率，「并行」和「并发」一字之差，但其实是两个完全不同的概念。
「并发」一般是由 CPU 内核通过时间片或者中断来控制的，遇到 IO 阻塞或者时间片用完时会交出线程的使用权，从而实现在一个内核上处理多个任务 而「并行」则是多个处理器或者多核处理器同时执行多个任务，同一时间有多个任务在调度，因此，一个内核是无法实现并行的，因为同一时间只有一个任务在调度 多进程、多线程以及协程显然都是属于「并发」范畴的，可以实现程序的并发执行，至于是否支持「并行」，则要看程序运行系统是否是多核，以及编写程序的语言是否可以利用 CPU 的多核特性。我们来模拟一个可以并行的计算任务：启动多个子协程，子协程数量和 CPU 核心数保持一致，以便充分利用多核并行运算，每个子协程计算分给它的那部分计算任务，最后将不同子协程的计算结果再做一次累加，这样就可以得到所有数据的计算总和。
package mainimport (&amp;#34;fmt&amp;#34;&amp;#34;runtime&amp;#34;&amp;#34;time&amp;#34;)func sum(seq int, ch chan int) {defer close(ch)sum := 0for i := 1; i &amp;lt;= 10000000; i++ {sum += i}fmt.Printf(&amp;#34;子协程%d运算结果:%d&amp;#34;, seq, sum)ch &amp;lt;- sum}func main() {start := time.Now()cpus := runtime.NumCPU()fmt.Println(cpus)runtime.GOMAXPROCS(cpus)chs := make([]chan int, cpus)for i := 0; i &amp;lt; cpus; i++ {chs[i] = make(chan int)go sum(i, chs[i])}sum := 0for _, ch := range chs {res := &amp;lt;-chsum += res}end := time.</description>
    </item>
    
    <item>
      <title>golang基础(48.sync 包：sync.Mutex 和 sync.RWMutex)</title>
      <link>https://869413421.github.io/post/golang%E5%9F%BA%E7%A1%80/48.sync-%E5%8C%85sync.mutex-%E5%92%8C-sync.rwmutex/</link>
      <pubDate>Tue, 07 Feb 2023 15:52:40 +0800</pubDate>
      
      <guid>https://869413421.github.io/post/golang%E5%9F%BA%E7%A1%80/48.sync-%E5%8C%85sync.mutex-%E5%92%8C-sync.rwmutex/</guid>
      <description>sync 包 我们前面反复强调，在 Go 语言并发编程中，倡导「使用通信共享内存，不要使用共享内存通信」，而这个通信的媒介就是我们前面花大量篇幅介绍的通道（Channel），通道是线程安全的，不需要考虑数据冲突问题，面对并发问题，我们始终应该优先考虑使用通道，它是 first class 级别的，但是纵使有主角光环加持，通道也不是万能的，它也需要配角，这也是共享内存存在的价值，其他语言中主流的并发编程都是通过共享内存实现的，共享内存必然涉及并发过程中的共享数据冲突问题，而为了解决数据冲突问题，Go 语言沿袭了传统的并发编程解决方案 —— 锁机制，这些锁都位于 sync 包中。锁的作用都是为了解决并发情况下共享数据的原子操作和最终一致性问题，在系统介绍 sync 包提供的各种锁之前，我们先来聊聊什么情况下需要用到锁。
竞态条件与同步机制 一旦数据被多个线程共享，那么就很可能会产生争用和冲突的情况，这种情况也被称为竞态条件（race condition），这往往会破坏共享数据的一致性。举个例子，同时有多个线程连续向同一个缓冲区写入数据块，如果没有一个机制去协调这些线程的写入操作的话，那么被写入的数据块就很可能会出现错乱。比如，学院君的支付宝账户余额还有 500 元，代表银行自动转账的线程 A 正在向账户转入 3000 元本月工资，同时代表花呗自动扣费的线程 B 正在从账户余额扣除 2000 元还上个月的花呗账单。假设用 money 标识账户余额，那么初始值 money = 500，线程 A 的操作就等价于 money = money + 3000，线程 B 的操作就等价于 money = money - 2000，我们本来期望的结果是 money = 1500，但是现在线程 A 和线程 B 同时对 money 进行读取和写入，所以他们拿到的 money 都是 500，如果线程 A 后执行完毕，那么 money = 3500，如果线程 B 后执行完毕，那么 money = 0（扣除所有余额，花呗欠款1500），这就出现了和预期结果不一致的现象，我们说，这个操作破坏了数据的一致性。在这种情况下，我们就需要采取一些措施来协调它们对共享数据的修改，这通常就会涉及到同步操作。一般来说，同步的用途有两个，一个是避免多个线程在同一时刻操作同一个数据块，另一个是协调多个线程避免它们在同一时刻执行同一个代码块。但是目的是一致的，那就是保证共享数据原子操作和一致性。由于这样的数据块和代码块的背后都隐含着一种或多种资源（比如存储资源、计算资源、I/O 资源、网络资源等等），所以我们可以把它们看做是共享资源。我们所说的同步其实就是在控制多个线程对共享资源的访问：一个线程在想要访问某一个共享资源的时候，需要先申请对该资源的访问权限，并且只有在申请成功之后，访问才能真正开始；而当线程对共享资源的访问结束时，它还必须归还对该资源的访问权限，若要再次访问仍需申请。你可以把这里所说的访问权限想象成一块令牌，线程一旦拿到了令牌，就可以进入指定的区域，从而访问到资源，而一旦线程要离开这个区域了，就需要把令牌还回去，绝不能把令牌带走。或者我们把共享资源看作是有锁的资源，当某个线程获取到共享资源的访问权限后，给资源上锁，这样，其他线程就不能访问它，直到该线程执行完毕，释放锁，这样其他线程才能通过竞争获取对资源的访问权限，依次类推。这样一来，我们就可以保证多个并发运行的线程对这个共享资源的访问是完全串行的，只要一个代码片段需要实现对共享资源的串行化访问，就可以被视为一个临界区（critical section），也就是我刚刚说的，由于要访问到资源而必须进入的那个区域。比如，在前面举的那个例子中，实现了账户余额写入操作的代码就组成了一个临界区。临界区总是需要通过同步机制进行保护的，否则就会产生竞态条件，导致数据不一致。</description>
    </item>
    
    <item>
      <title>golang基础(49.sync 包：条件变量 sync.Cond)</title>
      <link>https://869413421.github.io/post/golang%E5%9F%BA%E7%A1%80/49.sync-%E5%8C%85%E6%9D%A1%E4%BB%B6%E5%8F%98%E9%87%8F-sync.cond/</link>
      <pubDate>Tue, 07 Feb 2023 15:52:40 +0800</pubDate>
      
      <guid>https://869413421.github.io/post/golang%E5%9F%BA%E7%A1%80/49.sync-%E5%8C%85%E6%9D%A1%E4%BB%B6%E5%8F%98%E9%87%8F-sync.cond/</guid>
      <description>sync 包还提供了一个条件变量类型 sync.Cond，它可以和互斥锁或读写锁（以下统称互斥锁）组合使用，用来协调想要访问共享资源的线程。不过，与互斥锁不同，条件变量 sync.Cond 的主要作用并不是保证在同一时刻仅有一个线程访问某一个共享资源，而是在对应的共享资源状态发生变化时，通知其它因此而阻塞的线程。条件变量总是和互斥锁组合使用，互斥锁为共享资源的访问提供互斥支持，而条件变量可以就共享资源的状态变化向相关线程发出通知，重在「协调」。下面，我们来看看如何使用条件变量 sync.Cond。sync.Cond 是一个结构体：
type Cond struct {noCopy noCopy// L is held while observing or changing the conditionL Lockernotify notifyListchecker copyChecker} 提供了三个方法：
// 等待通知func (c *Cond) Wait() {c.checker.check()t := runtime_notifyListAdd(&amp;amp;c.notify)c.L.Unlock()runtime_notifyListWait(&amp;amp;c.notify, t)c.L.Lock() }// 单发通知func (c *Cond) Signal() {c.checker.check()runtime_notifyListNotifyOne(&amp;amp;c.notify) }// 广播通知func (c *Cond) Broadcast() {c.checker.check()runtime_notifyListNotifyAll(&amp;amp;c.notify) } 我们可以通过 sync.NewCond 返回对应的条件变量实例，初始化的时候需要传入互斥锁，该互斥锁实例会赋值给 sync.</description>
    </item>
    
    <item>
      <title>golang基础(5.变量声明，初始化，赋值，作用域)</title>
      <link>https://869413421.github.io/post/golang%E5%9F%BA%E7%A1%80/5.%E5%8F%98%E9%87%8F%E5%A3%B0%E6%98%8E%E5%88%9D%E5%A7%8B%E5%8C%96%E8%B5%8B%E5%80%BC%E4%BD%9C%E7%94%A8%E5%9F%9F/</link>
      <pubDate>Tue, 07 Feb 2023 15:52:40 +0800</pubDate>
      
      <guid>https://869413421.github.io/post/golang%E5%9F%BA%E7%A1%80/5.%E5%8F%98%E9%87%8F%E5%A3%B0%E6%98%8E%E5%88%9D%E5%A7%8B%E5%8C%96%E8%B5%8B%E5%80%BC%E4%BD%9C%E7%94%A8%E5%9F%9F/</guid>
      <description>变量相当于计算机中一块存储区域的命名，通过定义变量能像系统申请到一块存储空间。通过使用变量名，能对这块存储空间进行操作。
定义变量 在go中，可以使用var关键字来定义变量，且要将值类型放置到变量定义后面。
var i int 可以同时定义多个同类型变量
var i1,i2 int 可以分组定义
var (a,a1 intb stringc bool) 需要注意的是定义变量后，系统会将变量初始化为该类型的初始值，入`i`会为`0`，`b`是`空字符串`，`c`是`flase`package mainimport &amp;#34;fmt&amp;#34;// 定义一个var i int// 定义多个同类型var i1, i2 int// 分组定义var (a, a1 intb string)func main() {fmt.Printf(&amp;#34;i is %d &amp;#34;, i)fmt.Printf(&amp;#34;i1 is %d &amp;#34;, i1)fmt.Printf(&amp;#34;i2 is %d &amp;#34;, i2)fmt.Printf(&amp;#34;a is %d &amp;#34;, a)fmt.Printf(&amp;#34;a1 is %d &amp;#34;, a1)fmt.</description>
    </item>
    
    <item>
      <title>golang基础(50.sync 包：原子操作)</title>
      <link>https://869413421.github.io/post/golang%E5%9F%BA%E7%A1%80/50.sync-%E5%8C%85%E5%8E%9F%E5%AD%90%E6%93%8D%E4%BD%9C/</link>
      <pubDate>Tue, 07 Feb 2023 15:52:40 +0800</pubDate>
      
      <guid>https://869413421.github.io/post/golang%E5%9F%BA%E7%A1%80/50.sync-%E5%8C%85%E5%8E%9F%E5%AD%90%E6%93%8D%E4%BD%9C/</guid>
      <description>我们在前两篇教程中讨论了互斥锁、读写锁以及基于它们的条件变量。互斥锁是一个同步工具，它可以保证每一时刻进入临界区的协程只有一个；读写锁对共享资源的写操作和读操作区别看待，并消除了读操作之间的互斥；条件变量主要用于协调想要访问共享资源的那些线程，当共享资源的状态发生变化时，它可以被用来通知被互斥锁阻塞的线程，它既可以基于互斥锁，也可以基于读写锁（当然了，读写锁也是互斥锁，是对后者的一种扩展）。通过对互斥锁的合理使用，我们可以使一个 Go 协程在执行临界区中的代码时，不被其他的协程打扰，实现串行执行，不过，虽然不会被打扰，但是它仍然可能会被中断（interruption）。所谓中断其实是 CPU 和操作系统级别的术语，并发执行的协程并不是真的并行执行，而是通过 CPU 的调度不断从运行状态切换到非运行状态，或者从非运行状态切换到运行状态，在用户看来，好像是「同时」在执行。我们把代码从运行状态切换到非运行状态称之为**中断**。中断的时机很多，比如任何两条语句执行的间隙，甚至在某条语句执行的过程中都是可以的，即使这些语句在临界区内也是如此。所以我们说互斥锁只能保证临界区代码的串行执行，不能保证这些代码执行的原子性，因为原子操作不能被中断。原子操作通常是 CPU 和操作系统提供支持的，由于执行过程中不会中断，所以可以完全消除竞态条件，从而绝对保证并发安全性，此外，由于不会中断，所以原子操作本身要求也很高，既要简单，又要快速。Go 语言的原子操作也是基于 CPU 和操作系统的，由于简单和快速的要求，只针对少数数据类型的值提供了原子操作函数，这些函数都位于标准库代码包 sync/atomic 中。这些原子操作包括加法（Add）、比较并交换（Compare And Swap，简称 CAS）、加载（Load）、存储（Store）和交换（Swap）。
Go 语言中的原子操作 加减法 我们可以通过 atomic 包提供的下列函数实现加减法的原子操作，第一个参数是操作数对应的指针，第二个参数是加/减值：虽然这些函数都是以 Add 前缀开头，但是对于减法可以通过传递负数实现，不过对于后三个函数，由于操作数类型是无符号的，所以无法显式传递负数来实现减法。比如我们测试下 AddInt32 函数：
package mainimport (&amp;#34;fmt&amp;#34;&amp;#34;sync/atomic&amp;#34;&amp;#34;time&amp;#34;)var counter int32 = 0func testAdd(i int32) {atomic.AddInt32(&amp;amp;counter, 1)//counter += 1fmt.Println(counter)}func main() {for i := 0; i &amp;lt; 100; i++ {go testAdd(int32(i))go testAdd(int32(i))go testAdd(int32(i))}time.</description>
    </item>
    
    <item>
      <title>golang基础(51.sync 包：sync.WaitGroup 和 sync.Once)</title>
      <link>https://869413421.github.io/post/golang%E5%9F%BA%E7%A1%80/51.sync-%E5%8C%85sync.waitgroup-%E5%92%8C-sync.once/</link>
      <pubDate>Tue, 07 Feb 2023 15:52:40 +0800</pubDate>
      
      <guid>https://869413421.github.io/post/golang%E5%9F%BA%E7%A1%80/51.sync-%E5%8C%85sync.waitgroup-%E5%92%8C-sync.once/</guid>
      <description>在介绍通道的时候，如果启用了多个子协程，我们是这样实现主协程等待子协程执行完毕并退出的：声明一个和子协程数量一致的通道数组，然后为每个子协程分配一个通道元素，在子协程执行完毕时向对应的通道发送数据；然后在主协程中，我们依次读取这些通道接收子协程发送的数据，只有所有通道都接收到数据才会退出主协程。
chs := make([]chan int, 10)for i := 0; i &amp;lt; 10; i++ {chs[i] = make(chan int)go add(1, i, chs[i])}for _, ch := range chs {&amp;lt;- ch} sync.WaitGroup 类型 sync.WaitGroup 类型是开箱即用的，也是并发安全的。该类型提供了以下三个方法：
Add：WaitGroup 类型有一个计数器，默认值是0，我们可以通过 Add 方法来增加这个计数器的值，通常我们可以通过个方法来标记需要等待的子协程数量； Done：当某个子协程执行完毕后，可以通过 Done 方法标记已完成，该方法会将所属 WaitGroup 类型实例计数器值减一，通常可以通过 defer 语句来调用它； Wait：Wait 方法的作用是阻塞当前协程，直到对应 WaitGroup 类型实例的计数器值归零，如果在该方法被调用的时候，对应计数器的值已经是 0，那么它将不会做任何事情。 至此，你可能已经看出来了，我们完全可以组合使用 sync.WaitGroup 类型提供的方法来替代之前通道中等待子协程执行完毕的实现方法，对应代码如下：
package mainimport (&amp;#34;fmt&amp;#34;&amp;#34;sync&amp;#34;)var wg *sync.WaitGroupfunc addSum(a, b int) {defer wg.</description>
    </item>
    
    <item>
      <title>golang基础(52.通过 context 包提供的函数实现多协程之间的协作)</title>
      <link>https://869413421.github.io/post/golang%E5%9F%BA%E7%A1%80/52.%E9%80%9A%E8%BF%87-context-%E5%8C%85%E6%8F%90%E4%BE%9B%E7%9A%84%E5%87%BD%E6%95%B0%E5%AE%9E%E7%8E%B0%E5%A4%9A%E5%8D%8F%E7%A8%8B%E4%B9%8B%E9%97%B4%E7%9A%84%E5%8D%8F%E4%BD%9C/</link>
      <pubDate>Tue, 07 Feb 2023 15:52:40 +0800</pubDate>
      
      <guid>https://869413421.github.io/post/golang%E5%9F%BA%E7%A1%80/52.%E9%80%9A%E8%BF%87-context-%E5%8C%85%E6%8F%90%E4%BE%9B%E7%9A%84%E5%87%BD%E6%95%B0%E5%AE%9E%E7%8E%B0%E5%A4%9A%E5%8D%8F%E7%A8%8B%E4%B9%8B%E9%97%B4%E7%9A%84%E5%8D%8F%E4%BD%9C/</guid>
      <description>除此之外，我们还可以通过另一种工具实现类似需求，这就是我们今天要介绍的 context 包，这个包为我们提供了以下方法和类型：我们可以先通过 withXXX 方法返回一个从父 Context 拷贝的新的可撤销子 Context 对象和对应撤销函数 CancelFunc，CancelFunc 是一个函数类型，调用它时会撤销对应的子 Context 对象，当满足某种条件时，我们可以通过调用该函数结束所有子协程的运行，主协程在接收到信号后可以继续往后执行。
package mainimport (&amp;#34;context&amp;#34;&amp;#34;fmt&amp;#34;&amp;#34;time&amp;#34;)func go2(ctx context.Context) {select {case &amp;lt;-ctx.Done():println(&amp;#34;携程2已结束&amp;#34;)return}}func go1(ctx context.Context) {go go2(ctx)select {case &amp;lt;-ctx.Done():println(&amp;#34;携程1已结束&amp;#34;)return}}func main() {ctx, cancelFunc := context.WithCancel(context.Background())go go1(ctx)for i := 1; i &amp;lt; 100; i++ {if i &amp;gt; 10 {cancelFunc()}}time.</description>
    </item>
    
    <item>
      <title>golang基础(53.sync 包：临时对象池 sync.Pool)</title>
      <link>https://869413421.github.io/post/golang%E5%9F%BA%E7%A1%80/53.sync-%E5%8C%85%E4%B8%B4%E6%97%B6%E5%AF%B9%E8%B1%A1%E6%B1%A0-sync.pool/</link>
      <pubDate>Tue, 07 Feb 2023 15:52:40 +0800</pubDate>
      
      <guid>https://869413421.github.io/post/golang%E5%9F%BA%E7%A1%80/53.sync-%E5%8C%85%E4%B8%B4%E6%97%B6%E5%AF%B9%E8%B1%A1%E6%B1%A0-sync.pool/</guid>
      <description>在高并发场景下，我们会遇到很多问题，垃圾回收（GC）就是其中之一。Go 语言中的垃圾回收是自动执行的，这有利有弊，好处是避免了程序员手动对垃圾进行回收，简化了代码编写和维护，坏处是垃圾回收的时机无处不在，这在无形之中增加了系统运行时开销。在对系统性能要求较高的高并发场景下，这是我们应该主动去避免的，因此这需要对对象进行重复利用，以避免产生太多垃圾，而这也就引入了我们今天要讨论的主题 —— sync 包提供的 Pool 类型：
type Pool struct {noCopy noCopylocal unsafe.Pointer // local fixed-size per-P pool, actual type is [P]poolLocallocalSize uintptr // size of the local array// New optionally specifies a function to generate// a value when Get would otherwise return nil.// It may not be changed concurrently with calls to Get.New func() interface{}} sync.Pool 是一个临时对象池，可用来临时存储对象，下次使用时从对象池中获取，避免重复创建对象。相应的，该类型提供了 Put 和 Get 方法，分别对临时对象进行存储和获取。我们可以把 sync.</description>
    </item>
    
    <item>
      <title>golang基础(6.常量和枚举)</title>
      <link>https://869413421.github.io/post/golang%E5%9F%BA%E7%A1%80/6.%E5%B8%B8%E9%87%8F%E5%92%8C%E6%9E%9A%E4%B8%BE/</link>
      <pubDate>Tue, 07 Feb 2023 15:52:40 +0800</pubDate>
      
      <guid>https://869413421.github.io/post/golang%E5%9F%BA%E7%A1%80/6.%E5%B8%B8%E9%87%8F%E5%92%8C%E6%9E%9A%E4%B8%BE/</guid>
      <description>常量指的是在编译期间已知的且不可改变的数据类型，常量可以是数值类型、浮点型、复合类型、布尔类型、字符串类型，在go中任何编译器后试图改变常量的操作都会导致编译报错。
定义常量 在go中我们可以使用const来定义常量，以下是常见的击中定义方式
package _constconst Pi float64 = 3.14159265358979323846const zero = 0.0 // 无类型浮点常量const ( // 通过一个 const 关键字定义多个常量，和 var 类似size int64 = 1024eof = -1 // 无类型整型常量)const u, v float32 = 0, 3 // u = 0.0, v = 3.0，常量的多重赋值const a, b, c = 3, 4, &amp;#34;foo&amp;#34; // a = 3, b = 4, c = &amp;#34;foo&amp;#34;, 无类型整型和字符串常量 预定义常量 go中预定义的常量有 true,false,iotaiota比较特殊，它是一个可以被编译器修改的常量。在编译期间每次const关键字出现时iota都会被重置为0，直到下一个const出现前，每出现一次iota都会递增。</description>
    </item>
    
    <item>
      <title>golang基础(7.数据类型概述，以及布尔类型)</title>
      <link>https://869413421.github.io/post/golang%E5%9F%BA%E7%A1%80/7.%E6%95%B0%E6%8D%AE%E7%B1%BB%E5%9E%8B%E6%A6%82%E8%BF%B0%E4%BB%A5%E5%8F%8A%E5%B8%83%E5%B0%94%E7%B1%BB%E5%9E%8B/</link>
      <pubDate>Tue, 07 Feb 2023 15:52:40 +0800</pubDate>
      
      <guid>https://869413421.github.io/post/golang%E5%9F%BA%E7%A1%80/7.%E6%95%B0%E6%8D%AE%E7%B1%BB%E5%9E%8B%E6%A6%82%E8%BF%B0%E4%BB%A5%E5%8F%8A%E5%B8%83%E5%B0%94%E7%B1%BB%E5%9E%8B/</guid>
      <description>基本类型 作为静态语言，go有7种基础数据类型。
布尔类型：bool 整型：int8、byte、int16、int、uint、uintptr （有符号，无符号） 浮点类型：float32、float64 （有符号，无符号） 复数类型：complex64、complex128 字符串：string 字符类型：rune 错误类型：error 在go中的整形以及浮点类型都区分有有符号以及无符号，即1，1.0（无符号）``-1，-1.9（有符号）。浮点类型通过，float 以及double 来区分精度。
复合类型 除去以上7种以为还支持多种复合类型
指针（pointer） 数组（array） 切片（slice） 字典（map） 通道（chan） 结构体（struct） 接口（interface） </description>
    </item>
    
    <item>
      <title>golang基础(8.布尔类型)</title>
      <link>https://869413421.github.io/post/golang%E5%9F%BA%E7%A1%80/8.%E5%B8%83%E5%B0%94%E7%B1%BB%E5%9E%8B/</link>
      <pubDate>Tue, 07 Feb 2023 15:52:40 +0800</pubDate>
      
      <guid>https://869413421.github.io/post/golang%E5%9F%BA%E7%A1%80/8.%E5%B8%83%E5%B0%94%E7%B1%BB%E5%9E%8B/</guid>
      <description>布尔类型定义的关键字为 bool，只支持预定义常量true和false,不支持其他数据类型强制转换。
package mainimport &amp;#34;fmt&amp;#34;func main() {var a bool = truevar b = falsec := (1 != 2)fmt.Println(a, b, c)} </description>
    </item>
    
    <item>
      <title>golang基础(9.整形以及运算符)</title>
      <link>https://869413421.github.io/post/golang%E5%9F%BA%E7%A1%80/9.%E6%95%B4%E5%BD%A2%E4%BB%A5%E5%8F%8A%E8%BF%90%E7%AE%97%E7%AC%A6/</link>
      <pubDate>Tue, 07 Feb 2023 15:52:40 +0800</pubDate>
      
      <guid>https://869413421.github.io/post/golang%E5%9F%BA%E7%A1%80/9.%E6%95%B4%E5%BD%A2%E4%BB%A5%E5%8F%8A%E8%BF%90%E7%AE%97%E7%AC%A6/</guid>
      <description>go支持的整形 类型 长度（单位：字节） 说明 值范围 默认值 int8 1 带符号8位整型 -128~127 0 uint8 1 无符号8位整型，与 byte 类型等价 0~255 0 int16 2 带符号16位整型 -32768~32767 0 uint16 2 无符号16位整型 0~65535 0 int32 4 带符号32位整型，与 rune 类型等价 -2147483648~2147483647 0 uint32 4 无符号32位整型 0~4294967295 0 int64 8 带符号64位整型 -9223372036854775808~9223372036854775807 0 uint64 8 无符号64位整型 0~18446744073709551615 0 int 32位或64位 与具体平台相关 与具体平台相关 0 uint 32位或64位 与具体平台相关 与具体平台相关 0 uintptr 与对应指针相同 无符号整型，足以存储指针值的未解释位 32位平台下为4字节，64位平台下为8字节 0 在PHP中只有一种int类型且不区分符号，最大存储数量基于运行平台决定。在 32 位平台下其最大值为 20 亿左右（等同于 Go 语言中的 int32），64 位平台下的最大值通常是大约 9E18（等同于 Go 语言中的 int64），并且 PHP 中的整型不支持无符号类型，你可以通过 PHP_INT_MAX 常量在 PHP 中获取当前平台的最大整型值。在go中不同类型的int不支持类型自动转换，需要转换类型后才能进行运算</description>
    </item>
    
    <item>
      <title>Go核心(调试Go源代码)</title>
      <link>https://869413421.github.io/post/go%E6%A0%B8%E5%BF%83/%E8%B0%83%E8%AF%95go%E6%BA%90%E4%BB%A3%E7%A0%81/</link>
      <pubDate>Tue, 07 Feb 2023 15:52:40 +0800</pubDate>
      
      <guid>https://869413421.github.io/post/go%E6%A0%B8%E5%BF%83/%E8%B0%83%E8%AF%95go%E6%BA%90%E4%BB%A3%E7%A0%81/</guid>
      <description>编译安装Go 获取源代码 在编译安装之前我们需要获取到相关的代码，golang作为一个开源项目，我们能在各个开源平台上获取到源代码。这里从Github获取到最新的主干代码，截止行文前最新的代码版本为测试版本的1.20。
git clone https://github.com/golang/go.git goroot 安装Go 当前版本Go已经完成了自举（自举即用Go来完成了Go的编译器的编写），所以在编译安装高版本的的Go时，请确保已经安装了编译器所需版本的GO。如当前我需要编译的版本为1.20，所需的编译器最低为1.17.3版本的Go。所以编译安装1.20版本的前提是本机已经安装好1.17.3版本的Go，具体如何安装这里不再赘述。执行前设置好GOROOT_BOOTSTRAP环境变量，即为低版本Go的安装路径。
修改环境变量 # vim /etc/profile export GOROOT_BOOTSTRAP=/usr/local/go1.17.3 # 你的低版本GO的安装路径 # 重新加载环境变量 source /etc/profile 执行安装命令 cd src/ # linux ./make.bash # windows ./make.bat 安装完成 windows下执行
$ ./make.bat Building Go cmd/dist using E:\go\src\github\869413421\go1.18. (go1.18 windows/amd64) Building Go toolchain1 using E:\go\src\github\869413421\go1.18. Building Go bootstrap cmd/go (go_bootstrap) using Go toolchain1. Building Go toolchain2 using go_bootstrap and Go toolchain1. Building Go toolchain3 using go_bootstrap and Go toolchain2. Building packages and commands for windows/amd64.</description>
    </item>
    
    <item>
      <title>laravel中使用自定义的Common类</title>
      <link>https://869413421.github.io/post/laravel/</link>
      <pubDate>Tue, 07 Feb 2023 15:52:40 +0800</pubDate>
      
      <guid>https://869413421.github.io/post/laravel/</guid>
      <description>​	众所周知，laravel是一款高度集成的开发框架，框架内置非常多的操作方法，从而保证了我们的开发效率。但是在日常中为了满足我们的个性化业务，也需要自己去编写工具类，在laravel中我们完成编写后还需要重新去对compoer的自动加载类进行重新加载。
​	首先在主要代码目录app下创建一个test.php1
然后还需要在根目录的composer.json中的autoload的file数组中注册我们刚才的helper类
最后在项目根目录下执行compoer dump-autoload命令，这样我们的类会被compoer自动加载了，在项目中直接描述我们的方法名就可以正常使用了。</description>
    </item>
    
    <item>
      <title>MySql（事务，锁）</title>
      <link>https://869413421.github.io/post/mysql_t/</link>
      <pubDate>Tue, 07 Feb 2023 15:52:40 +0800</pubDate>
      
      <guid>https://869413421.github.io/post/mysql_t/</guid>
      <description>事务是什么 事务是数据库一个不可以分隔的操作序列。在事务中执行的操作，会使结果从一个一致性状态到达另一个一致性状态。要么是产生操作结果，要么就是不产生结果
事务的四个特性（ACID） 原子性：一个事务是是一个执行单位，它要么就是全部执行，要么就是全部不执行。 一致性：事务执行的前后，多个事务对数据的读取是一致的。 隔离性：并发访问数据库是，一个事务中读取的数据是不受其他事务所影响的。 持久性：一个事务在提交之后，他作出的改变是持久的。 脏读 在一个事务中更新了一部分数据，这个时候另外一个事务读取了这一部分数据。而这个时候，第一个事务中回滚了数据，第二个事务中读取到的数据就是错误的了，这就是脏读。 幻读 在一个事务当中，两次查询的数据行数不一致。原因可能是在第一次读取后另外一个事务又插入了几行数据。 不可重复读 在一个事务当中，由于在两次查询中有另外一个事务更新了数据，所以导致两次查询的数据不一致。 事务隔离级别 READ-UNCOMMITTED(读取未提交)： 读取尚未提交的数据，可能造成脏读，幻读，不可重复读
READ-COMMITTED(读取已提交)： 读取已经提交的事务数据，能防止数据脏读，但仍然有可能会幻读，不可重复度。
REPEATABLE-READ(可重复读) 对数据同一个字段读取是保持一致的，当够避免脏读和不可重复读，但幻读依然可能存在。
SERIALIZABLE(可串行化)： 事务最高隔离级别，允许事务串行化，完成一个事务再继续下一个事务。所有事务都是逐个执行，可以避免脏读，幻读，不可重复读。
MySql事务的隔壁界别是REPEATABLE-READ（可重复读），MySql的事务机制是基于锁机制和并发调度实现的，隔离级别越低，使用的锁就越少。InnoDB 存储引擎在 分布式事务 的情况下一般会用到**SERIALIZABLE(可串行化)**隔离级别。
隔离级别与锁的关系 在READ-UNCOMMITTED(读取未提交)隔离级别下，数据不会进行加锁。 在READ-COMMITTED(读取已提交)隔离级别下，会加共享锁，语句执行完之后就会释放锁。 在REPEATABLE-READ(可重复读)，在读操作下会加共享锁，并且在事务结束之后才会进行释放。 SERIALIZABLE(可串行化)，会锁定整个范围的键，并且一直到事务结束。 锁粒度 在关系型数据库中，锁粒度一共有三种。
表级锁（table-level-locking） 表级锁是粒度最大的锁表方式，表示对当前操作的整张表进行加锁。锁资源消耗较少，MyIsAm和InnoDb都支持表级锁。
特点 开销小，加锁快。不会出现死锁，但是锁冲突率高，同时并发能力低。 行级锁（row-level-locking） 行级锁是颗粒度最小的锁表方式，它只会对当前操作的的记录进行锁定。行级锁，分为共享锁和排他锁。
特点 开销大，加锁慢。会出现死锁，锁冲突率低，并发能力高。 页级别锁 页级别锁，是表级别和行级别的一种折中方案。他会锁定当前操作的数据相邻的一组数据。
特点 开销和加锁时间介于行级和表级之间，会出现死锁，并发能力一般 共享锁与排他锁 共享锁：共享锁能同时存在多个，对数据加上共享锁之后，其他事务只能加共享锁对数据进行读取而不能进行修改。 排它锁 锁定的数据只能有一个排它锁，排他锁与其他类型的锁互斥，锁定的数据不能被其他操作进行读取或修改。 乐观锁和悲观锁 悲观锁： 假设数据冲突一定会发生，屏蔽掉一切可能违反数据完整性的操作，事务开启直接将数据锁死，直到事务完成。
乐观锁 ：假设数据冲突一定不会发生，只在最后持久化之前对数据完整性进行检查，如果不一致取消操作。</description>
    </item>
    
    <item>
      <title>MySql（查询优化）</title>
      <link>https://869413421.github.io/post/mysql_search/</link>
      <pubDate>Tue, 07 Feb 2023 15:52:40 +0800</pubDate>
      
      <guid>https://869413421.github.io/post/mysql_search/</guid>
      <description>mysql数据类型 (1) 整数类型
① tinyint
② smallint
③ mediumint
④ int
⑤ bigint
(2) 实数类型
① float
② double
③ decimal 可以存储比bigint还大的整数，可以用于存储精确的小数点
(3) 字符串类型
① varchar 可变长度的字符串类型，对于经常变更的数据char比varchar更好，char不容易产生碎片
② char 定长字符串类型，对于较短的数据varchar存储空间更有效率
③ blob
④ text 查询回使用临时表导致严重的性能开销
(4) 枚举
① 有时可以把常用的字符串替换成枚举类型
② 把不重复的集合存储成一个预定义的集合
③ 尽量避免使用数字作为enum作为常量，容易混乱
(5) 日期类型
① timestamp 存储的是整形，相对空间效率更高
② Datetime
(6) 列属性
① auto_increment自增
② default 默认值
③ not null 非空
④ zerofill 无符号填充
索引 (1) 索引对性能的影响
① 减少数据检索数量</description>
    </item>
    
    <item>
      <title>MySql（系统基础篇）</title>
      <link>https://869413421.github.io/post/mysql/</link>
      <pubDate>Tue, 07 Feb 2023 15:52:40 +0800</pubDate>
      
      <guid>https://869413421.github.io/post/mysql/</guid>
      <description>数据库三大范式是什么 第一范式：每个列不可拆分 第二范式：在第一范式基础上，所有非主键列完全依赖主键列，而不是主键的一部分 第三范式：在第二范式基础上，所有非主键列只依赖主键列，不依赖其他的非主键。 MySql自带的权限表 user权限表：记录允许连接到服务器的用户帐号信息，里面的权限是全局级的。 db权限表：记录各个帐号在各个数据库上的操作权限。 table_priv权限表：记录数据表级的操作权限。 columns_priv权限表：记录数据列级的操作权限。 host权限表：配合db权限表对给定主机上数据库级操作权限作更细致的控制。这个权限表不受GRANT和REVOKE语句的影响。 MySql的binlog binlog是MySql存储的二进制日志，用于记录用户操作Sql语句的信息
binlog具备三种模式
STATMENT模式 在STATMENT模式中用户每一条 修改数据的SQL都会记录到日志当中 优点： 不需要记录每一条SQL语句和每行的数据变化，减少磁盘的读写IO,减少数据库开销。
缺点： 在某些情况下会导致master-slave中的数据不一致(如sleep()函数， last_insert_id()，以及user-defined functions(udf)等会出现问题)
ROW模式 不记录每一条SQL的上下文的信息，只记录那一行被修改了，修改成什么样。 优点：不会出现某些特定情况下的存储过程、或function、或trigger的调用和触发 无法被正确复制的问题。
缺点：会产生大量的日志，尤其是alter table的时候会让日志暴涨。
混合模式 一般情况下使用STATMENT模式记录日志，在无法使用STATMENT模式时，切换为ROW模式。MySql会根据执行的SQL来选择日志保存的方式。 binlog的设置 在MySQL配置文件my.cnf文件中的mysqld节中添加下面的配置文件：
[mysqld]
#设置日志格式
binlog_format = mixed 设置日志路径，注意路经需要mysql用户有权限写 log-bin = /data/mysql/logs/mysql-bin.log 设置binlog清理时间 expire_logs_days = 7 binlog每个日志文件大小 max_binlog_size = 100m binlog缓存大小 binlog_cache_size = 4m 最大binlog缓存大小 max_binlog_cache_size = 512m 重启MySQL生效，如果不方便重启服务，也可以直接修改对应的变量即可。
引擎 常用的引擎
InnoDB InnoDB提供了ACID的事务支持，并且支持行级别锁，外键约束。 MyIASM MyIASM不支持事务，行级别锁。但是支持表级别锁 MEMORY MyIASM所有数据存储在内存中，读取速度快，但是安全性低 MyIAM索引和InnoDB索引的区别 InnoDB索引是聚簇索引，MyISAM索引是非聚簇索引。 InnoDB索引的叶子节点上存储着索引和行数据，所以InnoDB的主键索引数据非常高效 MyIASM索引的叶子节点上存储着行数据距的指向地址，需要再根据地址去进行查找 InnoDB的非主键索引的叶子节点存储着主键和其他非主键索引的列数据 InnoDB的四大特性 插入缓冲 插入缓冲在非唯一索引非聚合索引才生效，当第一次插入时，MySQL会先检查buffer中是否包含索引页，如果有直接插入，如果没有先放置到buffer中，等到一定频率再合并操作。</description>
    </item>
    
    <item>
      <title>MySql（索引）</title>
      <link>https://869413421.github.io/post/mysql_index/</link>
      <pubDate>Tue, 07 Feb 2023 15:52:40 +0800</pubDate>
      
      <guid>https://869413421.github.io/post/mysql_index/</guid>
      <description>索引是什么 索引是一个特殊的文件，他是实际存在在文件系统中的，记录着数据表里所有数据的引用指针
索引是一个数据结构，是数据库操作系统的一种排序数据结构，能帮助我们快速查询，更新我们数据表的数据
优点 创建索引的原因是为了帮助用户快速地检索数据 缺点 创建索引能加快检索速度，但是也意味着数据库增删改时需要对索引进行维护，会增加增删改的性能消耗，降低执行效率。 索引是实际存在系统中的，会占用系统的存储空间。 索引使用场景 where 因为主键索引中存储或者包含了行数据的引用地址，一般情况下，主键索引是最快的。如果一个where 语句中包含多个索引，MySql会选择最优的命中。
orderBy 在我们对某个字段进行orderBy时，如果这个字段没有建立索引，MySql会使用外部排序，即是将查询到的结果集分批从硬盘当中读取内存中进行排序,这个操作不仅要进行IO操作还要占用内存进行排序所以它是非常影响性能的。
如果存在索引的情况下，MySql会直接根据索引的排序和映射逐条取出数据。如果是分页的话直接取索引某个范围进行读取。不再需要读入内存中排序后再进行截取某一部分数据。
join 在我们设计表结构的时候，我们要join的字段应该是一个外键并且应该加上索引，这样能提高join时的查询效率，如果外键不存在索引的情况下，join的表可能会出现全表扫描。严重损耗检索效率
索引覆盖 如果我们一个select语句中，需要查询的字段都建立过索引，那么MySql会直接从索引页中获取数据，而不再去查询原始数据，这个就是索引覆盖。索引我们在写查询语句的时候尽量select需要的字段，提高索引覆盖的几率。
索引的几种类型 主键索引：数据表中的唯一标识，不允许为null
唯一索引：数据表的的列不允许重复，多个列可以聚合，允许为null
普通索引：基础的索引，多个列可以聚合，允许为null
全文索引： 一种全文搜索索引
索引的两种算法 b+tree BTree是最常用的mysql数据库索引算法，也是mysql默认的算法。因为它不仅可以被用在=,&amp;gt;,&amp;gt;=,&amp;lt;,&amp;lt;=和between这些比较操作符上，而且还可以用于like操作符，只要它的查询条件是一个不以通配符开头的常量， 例如：
hash算法 Hash索引只能用于对等比较，例如=,&amp;lt;=&amp;gt;（相当于=）操作符。由于是一次定位数据，不像BTree索引需要从根节点到枝节点，最后才能访问到页节点这样多次IO访问，所以检索效率远高于BTree索引。
索引的设计原则 适合索引的字段应该是出现在where语句中，或者join连接的列中。
数据过少的表不适合创建索引
尽量是用短索引，有时需要索引很长的字符列，它会使索引变大并且变慢。索引字符串的前半部分能有效地节约索引空间。
不要过度索引，索引会占用磁盘空间，并且会降低写性能。索引的创建只要保证查询性能即可。
索引的创建原则 最左前匹配原则，是聚合索引中非常重要的原则，MySql会一直向右匹配直到遇到范围查询(&amp;gt;、&amp;lt;、between、like)就停止匹配。例如组合索引abc,查询语句为a=1,b&amp;gt;2,c=3。这样c是使用不了索引的。
字段较为频繁查询的应该使用索引。
频繁更新的字段不适合创建索引。
不能有效区分的列不适合创建索引。(如性别，男女未知，最多也就三种，区分度实在太低)
尽量扩展索引，而不是去新建索引。如系统上有a索引，要增加一个ab索引，应该直接拓展索引，将a索引修改为ab索引。
有外键的列一定要建立索引。
对text,image,bit或者数据过长的字段不要建立索引
创建索引需要注意什么 不要设置可空字段，因为可空字段很难被查询优化，同事会使索引排序运算更加复杂，可以使用一个特殊的值或者0或者空字符串代替。
取离散值最大的字段（数据表值唯一值越多的离散值越大）
索引字段越小越好，字段过长影响索引效率，占用更多内存空间。
最左前缀原则，最左前匹配原则 顾名思义，就是最左优先，在创建多列索引时，要根据业务需求，where子句中使用最频繁的一列放在最左边。
最左前缀匹配原则，非常重要的原则，mysql会一直向右匹配直到遇到范围查询(&amp;gt;、&amp;lt;、between、like)就停止匹配，比如a = 1 and b = 2 and c &amp;gt; 3 and d = 4 如果建立(a,b,c,d)顺序的索引，d是用不到索引的，如果建立(a,b,d,c)的索引则都可以用到，a,b,d的顺序可以任意调整。
=和in可以乱序，比如a = 1 and b = 2 and c = 3 建立(a,b,c)索引可以任意顺序，mysql的查询优化器会帮你优化成索引可以识别的形式</description>
    </item>
    
    <item>
      <title>MySql（高可用，高拓展）</title>
      <link>https://869413421.github.io/post/mysql_build/</link>
      <pubDate>Tue, 07 Feb 2023 15:52:40 +0800</pubDate>
      
      <guid>https://869413421.github.io/post/mysql_build/</guid>
      <description>* 分区表原理 工作原理
对用户而言，分区表是一个独立的逻辑表，mysql底层将其分成了多个物理子表，每一个分区表都是一个独立的文件
适用场景
表非常大，无法全部存放在内存中，或者表的最后有热点数据，其他的都是历史数据 分区表更容易维护，可以对独立的分区进行操作 分区表可以分布在不同的服务器上 可以使用分区表避免某些特殊的瓶颈 可以恢复和备份独立的分区 限制
一个表只能有1024个分区 5.1版本中，分区表达式必须是整数 分区表字段中如果包含主键和唯一所以，那么主键和唯一列必须包含进去 分区表中不能使用外键约束 如果需要对现有表进行修改 所有分区表虚使用相同的存储引擎 某些引擎不支持分区 分库分表 工作原理 通过一些hash算法和工具实现将一张表的数据，垂直拆分和水平拆分 使用场景 单表记录数到达百万或者千万级别时候 解决表锁的问题 分表方式 水平分割 表很大，分割后降低在查询时候所需要读取的数据和索引的页数 使用场景 表中的数据具有独立性，比如说表中记录各个地区的数据或者不同时期的数据 需要把不同的数据存放在不同的介质 缺点 给应用增加复杂度，查询某些数据的时候需要定位到数据在某张表 垂直分割 将数据表的列进行分割，常用的列和不常用的列拆分成两个表 使用场景 一个表中一些列不常用，列外一些列常用 可以使数据行变小，一个数据也能存储更多的数据，查询时候减少IO次数 缺点 查询冗余，查询需要进行join操作 mysql主从复制 工作原理 在主库上把数据更改记录到二进制文件，从库将 主库的日志复制到自己的中继日志当中。从库读取日志，将数据重写到从库数据当中。 主从复制解决的问题 数据分布：随意停止或开始复制 负载均衡：降低单个服务器压力 高可用和故障切换：某个节点失败后其他节点顶替其工作，避免程序崩溃 异步复制：也是默认的主从同步方式。这种方式的优点是效率高。缺点是不能保证数据一定会到达slave。可能会受到网络等原因出现延迟，导致主从数据不一致。当前对master中的表进行数据操作，master将事务Binlog事件写入到Binlog文件中，此时主库只会通知一下Dump线程发送这些新的Binlog到slave（slave的 I/O 线程读取并将事件写入relay-log中）然后主库就会继续处理提交操作，而此时不会保证这些Binlog传到任何一个从库节点上。 全同步复制：优点是能够保证数据的强一致性，缺点是效率太低。当master上有提交事务之后，Dump线程发送这些新的Binlog到slave上，并且必须等待所有的slave回复成功（所有从库将事件写入中继日志，并将数据写入数据库）才能继续下一步操作。 半同步复制：优点是在耗费少量性能的基础上能在一定程度上保证数据的一致性。当master上有提交事务之后，Dump线程发送这些新的Binlog到slave上，并且必须等待其中一个slave回复成功（slave将事件写入relay-log）才能继续下一步操作。 </description>
    </item>
    
    <item>
      <title>swoole（基础）</title>
      <link>https://869413421.github.io/post/swoole/</link>
      <pubDate>Tue, 07 Feb 2023 15:52:40 +0800</pubDate>
      
      <guid>https://869413421.github.io/post/swoole/</guid>
      <description>swoole是什么？ swoole是一款为PHP使用C和C++编写的高性能，异步事件携程通信引擎。
为什么是通信引擎？ 因为原生的PHP是寄宿在服务器上经由PHP-FPM进行通讯处理的，PHP不负责请求响应部分生命周期的处理，只负责程序的运算。所有原生的PHP是不支持通讯处理的，而swoole能够不依赖服务器，PHP安装swoole后能够自己启动服务直接对用户的请求响应等通讯数据进行处理，所以swoole是一款通信引擎。
异步 可以参照前端的ajax进行理解，异步是基于事件的，当我们在执行异步代码时。他不会阻塞当前的进程，而是将 即将执行的事件放置到事件循环当中 EventLoop当中，不理解 EventLoop的可以参考这篇文章 EventLoop详解
携程 携程可以理解为是一个超轻量级线程，但与线程是由CPU以抢占的方式进行调度的，而携程是由程序员自行进行调度的。
线程与携程的消耗对比 线程是不进行内存隔离的，但是每个线程都会进行加锁，而加锁的开销相对携程来说更大。 携程是运行在单进程单线成当中的，是以 串行的方式执行，每个携程都拥有自己的堆栈，所以不存在抢占和内存加锁。 如果需要深入理解请阅读 进程，线程，携程的区别
当在携程中遇到耗时的IO或者网络请求时，当前的携程会自动让出控制权给主进程。关于携程更详细的理解请阅读 携程详解
Channel 通道，用于协程间通讯，支持多生产者协程和多消费者协程。底层自动实现了协程的切换和调度。</description>
    </item>
    
    <item>
      <title>数据结构（基础）</title>
      <link>https://869413421.github.io/post/data/</link>
      <pubDate>Tue, 07 Feb 2023 15:52:40 +0800</pubDate>
      
      <guid>https://869413421.github.io/post/data/</guid>
      <description>什么是数据结构 数据结构就是按照一定的逻辑把元素按照一定关系进行存储的数据
数据结构分类 可以分为逻辑机构和物理结构两大类
逻辑结构
1.集合机构，指的是一堆没有任何关系的数据存储在一个集合中，它们是平级的
2.线性结构，所有的的数据都存在1对1的关系、
3.树形结构，树状机构中存在着1对多的关系
4.图形结构，图形结构中存储着多对多的复杂关系
物理结构
1.顺序结构，所有的数据都是存储在一个连续内存当中的，它的逻辑关系和物理关系是一致的。
2.链式存储结构，在链式存储结构中，存储单元可以是连续的也可以是不连续的，元素之间不能反映元素的逻辑关系，只存储元素的引用地址。根据指针来确定彼此的逻辑关系
线性表 线性表是最基本，最简单的数据结构，一个线性表是一组拥有相同特征的元素的有限序列 线性表特征 表中第一个元素没有前驱，它是线性表的头结点。 表中最后一个元素没有后继，它是线性表的尾结点。 除了第一个和最后一个元素外，其他元素都拥有前驱和后续结点。 线性表的分类 根据不同的存储方式分类，线性表可以分为顺序表和链表
顺序表 顺序表是一种以数组方式进行表示的线性表，顺序表中元素间的逻辑与物理存储的逻辑一致，表中的元素是存储在一个连续内存当中的。 链表 之前我们已经使用顺序存储结构实现了线性表，我们会发现虽然顺序表的查询很快，时间复杂度为O(1),但是增删的效率是比较低的，因为每一次增删操作都伴随着大量的数据元素移动。这个问题有没有解决方案呢？有，我们可以使用另外一种存储结构实现线性表，链式存储结构。 链表是一种物理存储单元上非连续、非顺序的存储结构，其物理结构不能只管的表示数据元素的逻辑顺序，数据元素的逻辑顺序是通过链表中的指针链接次序实现的。链表由一系列的结点（链表中的每一个元素称为结点）组成，结点可以在运行时动态生成。 单向链表 单向链表是链表的一种，它由多个结点组成，每个结点都由一个数据域和一个指针域组成，数据域用来存储数据，指针域用来指向其后继结点。链表的头结点的数据域不存储数据，指针域指向第一个真正存储数据的结点。单向链表查找或者插入数据都要从头部开始查找。 </description>
    </item>
    
    <item>
      <title>网络基础(一次完整的网络请求生命周期)</title>
      <link>https://869413421.github.io/post/%E7%BD%91%E7%BB%9C%E5%9F%BA%E7%A1%80/%E4%B8%80%E6%AC%A1%E5%AE%8C%E6%95%B4%E7%9A%84%E7%BD%91%E7%BB%9C%E8%AF%B7%E6%B1%82%E7%94%9F%E5%91%BD%E5%91%A8%E6%9C%9F/</link>
      <pubDate>Tue, 07 Feb 2023 15:52:40 +0800</pubDate>
      
      <guid>https://869413421.github.io/post/%E7%BD%91%E7%BB%9C%E5%9F%BA%E7%A1%80/%E4%B8%80%E6%AC%A1%E5%AE%8C%E6%95%B4%E7%9A%84%E7%BD%91%E7%BB%9C%E8%AF%B7%E6%B1%82%E7%94%9F%E5%91%BD%E5%91%A8%E6%9C%9F/</guid>
      <description>客户端发起请求 以访问百度为例，用户在浏览器输入www.baidu.com。用户输入的www.baidu.com仅仅是一个域名，这时候计算机并不清楚需要去哪里访问到响应的资源。这时候就需要通过DNS协议来对域名进行解析，从DNS服务商中取得与域名相对应的IP地址。从而通过HTTP协议或者HTTPS协议，打包请求头和请求体向IP地址发起请求。DNS，HTTP，HTTPS所在的层为应用层，经过应用层的封装后，浏览器将包交由下一层来进行处理，这个过程是通过socket编程来实现的。下一层即是传输层，传输层主要有两种协议。一种是无连接的UDP协议，一种是面向连接的TCP协议。UDP协议不需要建立连接即可进行通讯，不需要额外的性能开销，传输速度更快，但是不可靠，可能会发生丢包的情况。TCP协议需要通过三次握手来建立连接才可以进行通信，相对来说有额外的性能开销，传输速度低于UPD，但是TCP能保证数据到底目的地，更加安全以及可靠。HTTP以及HTTPS都是基于TPC协议的可靠连接。TCP协议中有两个主要端口，一个是基于浏览器的端口（用于监听服务器的响应），一个是基于服务器的端口，HTTP对应的端口为80，HTTPS对应的端口为443，主要用于监听对服务器的请求。操作系统会根据端口将包转发给相应的处理进程。传输层封装完毕后会讲包交给操作系统的网络层，网络层的基本协议是IP协议，网络层会将传输层传递过来的包加上IP头，其中包含发送方的IP地址，以及接收方的IP地址。操作系统得到IP头中的IP地址后会寻找目标地址，如果服务端的IP地址是内网地址（局域网），便会直接传递请求数据。如果是外网的就需要通过网关去寻找目标机器。操作系统在启动的时候会通过DHCP协议来配置计算机的IP地址，以及默认的网关地址。计算会通过ARP协议通过IP地址获取网关的MAC地址，并且将网关以及MAC地址写到MAC头中。这样经过封装后，会将IP包交由下一层链路层，再经由网卡发送出去。（客户端机器与网关之间还有物理层的线路连接）。 :::warning MAC地址是唯一的，从网卡正式使用开始就具有全球唯一性。MAC地址相当于身份证，IP地址是动态分配的，有可能会重复。 ::: 网关会根据路由表，判断目标IP怎么走，经过多个网关的跳转最终找到目标服务器的网关，最终通过APR协议和目标IP取得目标机器的MAC地址，网络包最终可以根据MAC地址到达目的地。
服务端接收请求 服务端和客户端的流程正好相反，服务端是自下而上的。目标服务器接收到请求包后，会取下请求中的MAC头交由网络层中发现IP也对上了，就会取下IP头交由传输层，传输层会原路返回包，告诉客户端请求接收到了。如果过了一段时间（超时时间），客户端还是没有收到来自服务器的回复，会重新发送这个包，直到收到回复为止。同样，这个重发也不是重新发起上面那个客户端请求，而是传输层将同一个请求反复重试，对用户来说，只有一次请求。回到目标服务器，当网络包到达传输层后，TCP头中有一个服务器监听端口号，通过这个端口号，可以找到 服务端正在监听的端口，即 Nginx 中配置的 443 端口，端口对上之后，取下 TCP 头，将网络包交给应用层，开始对 HTTP/HTTPS 请求进行处理。如果是前端资源的话，直接通过 Nginx 进行响应，如果是 PHP 动态请求的话，再由 Nginx 将请求转发给后台运行的 PHP-FPM 进程进行处理。当然如果 Nginx 做了负载均衡，以及后端服务是分布式系统或者提供了微服务的化（涉及到RPC远程调用），还有更加复杂的处理逻辑，这些我们放到后面去讲。当后台服务处理完成后，就会返回一个 HTTPS 的响应包，告知用户请求成功，并返回响应内容，同样这个网络响应包和请求包一样，自上而下经过层层打包，顺着来路经过层层「关卡」（网关），回到发起请求的客户端，然后再经过自下而上的处理，最终在客户端浏览器显示。</description>
    </item>
    
    <item>
      <title>网络基础(计算机网络协议基础)</title>
      <link>https://869413421.github.io/post/%E7%BD%91%E7%BB%9C%E5%9F%BA%E7%A1%80/%E8%AE%A1%E7%AE%97%E6%9C%BA%E7%BD%91%E7%BB%9C%E5%8D%8F%E8%AE%AE%E5%9F%BA%E7%A1%80/</link>
      <pubDate>Tue, 07 Feb 2023 15:52:40 +0800</pubDate>
      
      <guid>https://869413421.github.io/post/%E7%BD%91%E7%BB%9C%E5%9F%BA%E7%A1%80/%E8%AE%A1%E7%AE%97%E6%9C%BA%E7%BD%91%E7%BB%9C%E5%8D%8F%E8%AE%AE%E5%9F%BA%E7%A1%80/</guid>
      <description>计算机网络从发明至今，经历了单机到联机，局域网到广域网，私有网络到互联网的发展阶段。为了让多台计算机可以相互通信，网络协议应运而生。网络协议指的是一种协议，本质上是一种约定，只要遵循这种约定可以使不同厂商，不同CPU等硬件不一致的计算机进行通讯。好比制定了一种语言，两个会说同样语言的人就可以互相交流。网络协议主要分为五层</description>
    </item>
    
    <item>
      <title>个人简历</title>
      <link>https://869413421.github.io/about/</link>
      <pubDate>Thu, 02 Feb 2023 16:59:22 +0800</pubDate>
      
      <guid>https://869413421.github.io/about/</guid>
      <description>个人信息 姓名：黄彦铭
性别：男
年龄：25
手机：13528685024
邮箱：13528685024@163.com
GitHub：github.com/869413421
学历：广州岭南职业技术学院(专科)
简介 一年.NET开发经验，两年 PHP 后端研发经验，有两年微信平台开发经验，擅长微信公众号，小程序，微信支付，商城系统，swoole，LNMP，PHP常驻内存框架，前后端分离项目，第三方接口对接开发。具备一定数据库性能调优，大流量高并发以及前端开发能力 。拥有良好的代码规范，对自己代码负责并且拥有良好的代码注释习惯， 保持代码的简洁易读。善于阅读技术官方文档，善于分享，有积极向上的学习心态。
职业技能 编程语言：PHP,C#,Go(了解)
PHP框架和拓展：laravel，Swoole，laravelS，Hyperf
测试：PHPUnit，PostMan(API功能测试)
.NET框架：.Net Core ，.Net Framework
前端：HTML，Js，JQ，Vue
前端框架：微信小程序，element-ui
关系型数据库：MySql，SQL Server
缓存&amp;amp;NoSql：Redis，memcache，MongoDb ，Elastic Search
消息中间件：RabbitMQ
运维：Linux，Docker ，Jenkins ，Nginx
管理工具：Git，Svn，Composer
工作经历 2018.04- 至今 广州市简美网络科技有限公司 岗位：
PHP开发工程师
工作描述：
负责公司微信运营项目业务开发以及产品版本迭代，参与产品需求研讨，技术选型，数据结构定型，技术难点攻克，撰写接口或技术文档，前端开发对接以及指导项目新成员实现功能模块。
2017.06 - 2018.04 上海联蔚科技有限公司 岗位：
.NET开发工程师 工作描述：
负责广汽本田官方网站及其下数据分发，流量监控系统开发以及维护
项目经验 微信运营集成系统 技术架构: ubuntu+laravels+mysql+es+redis+rabbitmq+jenkins+element
项目背景：
一套为长隆，中石化，真功夫，中信银行等大型企业提供微信公众号运营的多站点定制系统。系统集成营销推送，智能应答，微信卡券，票务，分销，粉丝社群，积分商城，微信门店，等一系列微信生态子系统。
主要职责：
负责客户各种定制模块的需求研讨，技术设计，开发。参与旧模块功能优化迭代开发。
项目成果：
使用 RabbitMQ+ElasticSearch 对微信智能应答模块进行重构，提高高峰期系统响应效率和并发能力，减少微信事件丢失，提高响应内容精准度。
使用 RabbitMQ重构异步任务代码，提高系统运行和响应效率。
使用阿里云OOS重构社群上传图片文件等功能，降低服务器资源使用成本。
使用定时任务对需要数据分析模块进行统计，实现系统数据汇总可视化。
使用swoole加ZipArchive编写数据导出服务，使系统支持海量数据快速导出。
使用注解+redis+lua脚本实现分布式锁，限流器，缓存器，应用至系统各种业务场景，提高开发效率，系统并发能力以及业务健壮性。
营销活动定制sass系统 技术架构: ubuntu+laravels+mysql+es+redis+jenkins+element</description>
    </item>
    
    <item>
      <title>laravel&#43;Redis简单实现队列通过压力测试的高并发处理</title>
      <link>https://869413421.github.io/post/laravel_redis/</link>
      <pubDate>Thu, 02 Feb 2023 14:57:24 +0800</pubDate>
      
      <guid>https://869413421.github.io/post/laravel_redis/</guid>
      <description>秒杀活动 在一般的网络商城中我们会经常接触到一些高并发的业务状况，例如我们常见的秒杀抢购等活动，
在这些业务中我们经常需要处理一些关于请求信息过滤以及商品库存的问题。
在请求中比较常见的状况是同一用户发出多次请求或者包含恶意的攻击，以及一些订单的复购等情况。
而在库存方面则需要考虑超卖这种状况。
下面我们来模拟一个简单可用的并发处理。
直接上代码
代码的流程 1.模拟用户请求，将用户写入redis队列中
2.从用户中取出一个请求信息进行处理（可以在这个步骤做更多的处理，请求过滤，订单复购等）
3.用户下单（支付等），减少库存。下面使用了两种方式进行了处理，一种使用了Redis中单线程原子操作的特性使程序一直线性操作从而保持了数据的一致。
另外一种是用了事务进行操作，可以根据业务进行调整，这里就不一一描述了。
实际的业务状况更为复杂，但更多的是出于对基础思路的拓展。
AB测试 这里我使用了apache bench对代码进行测试
不了解的可以参阅这篇文章，有非常详细的讲解
https://www.jianshu.com/p/43d04d8baaf7
调用 代码中的
AddUserToRedis() 方法将一堆请求用户放进redis队列中 先看库存
这里我们完成了1200个请求，其中标记失败的有1199个。这个是因为apache bench会以第一个请求响应的内容作为基准，
如果后续请求响应内容不一致会标记为失败，如果看到length中标记的数量不要方，基本可以忽略，我们的请求实际是完成了的。</description>
    </item>
    
    <item>
      <title>PHP（基础面试题）</title>
      <link>https://869413421.github.io/post/php_mst/</link>
      <pubDate>Thu, 02 Feb 2023 14:57:24 +0800</pubDate>
      
      <guid>https://869413421.github.io/post/php_mst/</guid>
      <description>引用变量 (1) 引用变量的概念
引用变量指引用变量是指定义不同名称的对象，他们的指向同一个内存空间，不会开辟新的内存。是基于CopyAndWrite实现的。
(2) PHP变量的机制
PHP的变量是基于CopyAndWrite的机制进行实现的，比如定义了变量a,变量b=a,这个时候变量的的内存空间是一致的，b只是指向a的内存空间。只有修改了a的变量内容时，才会开辟一块新的空间重新存储变量a，而变量b的内存空间不变。
unset只会取消变量的引用，而不会去销毁变量空间。只有等GC进行清理的时候才会销毁占用的空间
Object类型本来就是基于引用实现的，两个对象修改值会彼此影响。需要复制对象时候使用clone
真题：
常量以及数据类型 (1) .PHP字符串的定义方式和各自区别
单引号:单引号不能解析变量，单引号不能解析转义字符，只能解析单引号和反斜杠本身，单引号效率更高
双引号：双引号可以解析变量可以解析转义字符，可以使用{}解析变量
Heredoc：类似双引号，用于处理大文本
Newdoc：类似单引号，用于处理大文本
(2) .PHP的数据类型
三大数据类型
标量：
浮点数Float(不能用于比较运算中)
整形
字符串
布尔类型
false的七种情况
0，0.0，’ ’，””，’0’，false，array()，null
复合
数组，对象
特殊
资源
NULL
(3)超全局变量
$GLOBALS
$_SERVER
$_REQUEST
$_POST
$_GET
$_FILES
$_ENV
$_COOKIE
$_SESSION
(3) Null
null的三种情况
直接赋值为NULL,未定义的变量，unset变量
(4) 常量
定义常量difine是函数，const是语言结构。
const可以定义类的常量，而difine不能定义，常量定义后不能修改不能删除
一定义常量
FILE
LINE
DIR
FUNCTION
CLASS
TRAIT
METHOD
NAMESPACE
真题
用PHP写出当前客户端的IP和服务端的ID？
__FILE__是什么？
当前的文件路径和文件名称
运算符 (1) foo()和@foo()之间的区别？
@是错误控制符，放置在一个PHP表达式之前，表达式产生的错误信息都会被忽略掉
(2) 运算符优先级</description>
    </item>
    
    <item>
      <title>PHP（高并发面试题）</title>
      <link>https://869413421.github.io/post/php_mst2/</link>
      <pubDate>Thu, 02 Feb 2023 14:57:24 +0800</pubDate>
      
      <guid>https://869413421.github.io/post/php_mst2/</guid>
      <description>web防盗链 判断referer 使用nginx模块的ngx_http_referer_module来阻挡非法域名的请求 判断签名 使用nginx第三方模块httpAccessKeyModule配置 减少页面HTTP请求 性能黄金法则
10%-20%花在响应用户接收请求的HTML文档上
80%是花在请求HTML所需要的所有组件
如何改善 减少请求组件数量 图片地图 将多个图片合并，根据点击图片位置解析超链接 Css精灵 合并脚本和样式 图片使用base64编码减少引用 浏览器缓存以及压缩技术 200 form cache 直接从本地读取
304 not modified 协商缓存，如果本地缓存失效，请求头发送一定校验数据到服务端，如果服务端数据没有改变，直接从本地缓存响应
200 ok 以上两种失败，没有使用缓存，服务器直接返回完整响应。
脚本压缩
js压缩 css压缩 图片压缩 可以修改nginx配置
CDN加速 建立独立图片服务器 动态语言静态化 原因
动态脚本需要计算和数据查询，访问量大，服务器压力就大
服务端 集群部署，负载均衡，减少单机的访问压力 缓存，浏览器缓存，CDN缓存，分布式缓存。设定缓存雪崩，缓存击穿，缓存穿透，双写一致等容灾方案 异步处理任务，次要操作通过多线程，异步队列，延时或者定时任务进行处理 优化数据库，分区，分库，分表，优化索引，可以使用全文搜索引擎来代替复杂查询 缓存预热 较少IO次数 减少IO传输大小 限流，通过前端页面限流，nginx设置阈值限流，服务端进行限流 各种池技术，连接池，进程池 优化代码的流程逻辑 锁选择，尽量避免使用悲观锁 并发处理，可以开启多线程，多携程等方式对业务进行处理 </description>
    </item>
    
    <item>
      <title>PHP中的traits快速入门</title>
      <link>https://869413421.github.io/post/php_traits/</link>
      <pubDate>Thu, 02 Feb 2023 14:57:24 +0800</pubDate>
      
      <guid>https://869413421.github.io/post/php_traits/</guid>
      <description>traits 在学习PHP的过程中，我们经常会翻阅PHP的官方手册。一般理解能力强悍的人多阅读几遍便可轻松理解其中要领，但往往更多的初学者对官方文档中寥寥数语的描述难以理解。作为一个曾有同样困扰的人，我的经验是遇到这种情况的时候，首先使用搜索引擎翻阅他人分享的学习成果，当知其一二有了概念以后随手写下一些文档，方便巩固知识，日后在工作中有需要时再去深入细节。
traits是什么？ 首先我们先对这个知识有一个基本的概念，你可以先将traits理解成类似include用于代码复用的技术，include针对的是一个类或者其他文件，而traits则是一个针对方法结构的技术，我们使用use关键字就可以将结构体引用到当前的class当中。
需求 图中一共存在五个类，分别是基类A以及其子类BCD和一个完全独立的E类，我们有两个方法getSum,getSub。我们需要在B，C，E中同时包含这两个方法，但D类中不包含。
这时候，我们第一个想法大都会是
1.在B，C，E中复制同样的代码实现这两个方法。
2.定义一个接口让B,C,E去实现。
在没有traits之前可能我们大部分人正是如此去实现需求，不管哪种方法最终的方式都是复制代码重用。
然而这些方式的弊端是
1.繁复的复制工作造成的代码冗余。
2.不具备灵活性当需要添加新的方法时每个地方都要修改，难以维护。
traits的出现正是为了解决上述问题
如何使用traits 使用traits的方式很简单，和我们定义类的方式相像，除了关键字以为其余一致。
当定义好一个结构体后我们只需要在类里面使用use关键字进行调用，根据我们上面的需求我们在B,C,E中分别use myCode这个tratis
在代码中我们分在每个类中调用了我们定义的方法结构，从而我们不需要在每个类中对方法进行描述，因为程序已经将tratis中的方法自动添加到了每一个类中，这样我们就见面了各种手动繁复的操作，而如果程序后期需要对这几个类拓展的时候只需要对定义的tratis进行修改就可以达到预设的目的，极大地提交了可维护性。
运行这段代码的返回结果为：
最终我们的程序结构如下
这样我们就算是对tratis进行了一个简单入门，但应该已经满足我们日常开发的需求；
如果你需要深入了解更多细节可以参阅一下文章
1.https://blog.csdn.net/qq_16142851/article/details/80437560
2.https://segmentfault.com/a/1190000008009455</description>
    </item>
    
    <item>
      <title>RabbitMQ(基础)</title>
      <link>https://869413421.github.io/post/rabbitmq/</link>
      <pubDate>Thu, 02 Feb 2023 14:57:24 +0800</pubDate>
      
      <guid>https://869413421.github.io/post/rabbitmq/</guid>
      <description>1.1RabbitMQ的作用 异步任务 系统解耦 削峰 1.2为什么选择RabbitMQ 基于AMPQ协议(高级消息队列协议) 文档齐全，社区活跃 并发性能较高 相比其他中间件更容易维护 2.1RabbitMQ的七种运行模式
普通模式：简单的队列 竞争模式 消息确认，生产者要收到消费者返回的信息才确认推出的消息被消费，否则重新进入队列重新分配 公平派遣，生产者不指派1个以上的消息给同一个消费者 发布订阅（广播系统）：生产者将消息发送到交换机，消费者生成队列绑定到交换机。使用fanout交换机 交换机推送：direct，根据路由key全匹配 如何保证rabbitMQ高可用性？ 使用主备模式，使用haproxy对消息进行分发，如果主节点挂掉了它会自动将请求转发到备用节点上。 如何保证消息不被重复消费？ 保证消息的唯一性，在生产的时候做唯一表示。判断这个表示是否已经消费过。 如何保证消息不被重复消费？ 生产者丢失，开始rabbitMQ的事务模式和confirm模式。一旦信道进入confirm模式，他会生成一个唯一ID，一旦消息被投递到队列，队列会返回一个包含唯一ID的ACK确认消息已经到达队列,如果失败会返回一个Nack给生产者并且重试。 消息队列丢数据,开启队列持久化，当数据到达队列持久化成功以后返回一个ack给到生产者。生产者如果没有收到会重新发送到队列。 消费数据丢失，关闭自动确认消息。根据业务判断，是否已经消费过，手动进行确认。 如何保证RabbitMQ消息的顺序性？
答：单线程消费保证消息的顺序性；对消息进行编号，消费者处理消息是根据编号处理消息；
如何保证RabbitMQ消息的顺序性？ 答：单线程消费保证消息的顺序性；对消息进行编号，消费者处理消息是根据编号处理消息；</description>
    </item>
    
    <item>
      <title>面向对象的六大原则（单一职责原则）</title>
      <link>https://869413421.github.io/post/single/</link>
      <pubDate>Thu, 02 Feb 2023 14:57:24 +0800</pubDate>
      
      <guid>https://869413421.github.io/post/single/</guid>
      <description>当我们要审视判断事物的好坏时，无论如何我们都需要有一个标准。而作为一个程序员我们也需要有一个标准去判断代码结构设计的优劣。而在我们设计程序时这个标准正是面向对象的六大原则。
单一职责原则（S） 开闭原则（O） 里氏替换原则（L） 依赖倒置原则（D） 接口隔离原则（I) 合成复用原则 迪特米法则 单一职责原则 单一职责原则理解起来非常简单，一个人应该干好自己的本职工作就是遵循了单一职责原则，一个类只做属于这个类的事情也是遵循了单一职责原则。
违反单一职责原则会存在什么问题? 代码无法复用 调度混乱（不知道这个类到底是用来做什么的） 难以拓展维护 我们看一个违反单一原则的类，看看这样的设计是否也存在你的项目中
&amp;lt;phpclass OrderService{//获取数据库连接public function getConnention(){}//获取订单public function getOrder(){}//创建JSONpublic function createJson(){}//返回订单JSONpublic function responeJson(){}}?&amp;gt; 我们可以看到 OrderService这个类它完成了几种职责
获取数据库连接 获取订单号 构建订单JSON 返回JSON 当一个类需要 获取数据库连接时或者我需要构造一个JSON时，我去创建一个 OrderService显然是不合理的
这时候我们需要怎么去改进这样的设计呢？
Class DB{//获取数据库连接public function getConnention(){}}Class OrderService{private $dbpublic function __construct(DB $db){$this-&amp;gt;db = $db;}//获取订单public function getOrder(){}}Class Json{//创建订单JSONpublic function createOrderJson(){}//返回订单JSONpublic function responeJson(){}} 重构完成以后 DB类负责和数据库进行交互 OrderService类负责订单相关的逻辑 Json类负责Json的构建和响应</description>
    </item>
    
    <item>
      <title></title>
      <link>https://869413421.github.io/post/docker-%E7%8E%AF%E5%A2%83%E4%B8%8B%E5%90%84%E7%A7%8D%E5%AE%89%E8%A3%85%E5%91%BD%E4%BB%A4/</link>
      <pubDate>Mon, 01 Jan 0001 00:00:00 +0000</pubDate>
      
      <guid>https://869413421.github.io/post/docker-%E7%8E%AF%E5%A2%83%E4%B8%8B%E5%90%84%E7%A7%8D%E5%AE%89%E8%A3%85%E5%91%BD%E4%BB%A4/</guid>
      <description>docker 环境下各种安装命令 elasticsearch 拉取镜像
docker pull docker.elastic.co/elasticsearch/elasticsearch:6.3.2 运行容器
docker run -d --name es -p 9200:9200 -p 9300:9300 -e &amp;#34;discovery.type=single-node&amp;#34; docker.elastic.co/elasticsearch/elasticsearch:6.3.2 -e &amp;ldquo;discovery.type=single-node&amp;rdquo; ，设置为单节点。 -p 9200:9200 http协议，为elasticsearch默认端口，用于外部通讯。 -p 9300:9300 tcp协议，用于集群之间通信。 </description>
    </item>
    
    <item>
      <title></title>
      <link>https://869413421.github.io/post/transformer/chatbot_ptuning/</link>
      <pubDate>Mon, 01 Jan 0001 00:00:00 +0000</pubDate>
      
      <guid>https://869413421.github.io/post/transformer/chatbot_ptuning/</guid>
      <description>--- title: &amp;#34;Transformer 学习之路 - P-tuning 技术深入解析&amp;#34; date: 2024-04-19T17:35:18+08:00 draft: false description: &amp;#34;深入解析 P-tuning 技术，探讨其核心思想、工作原理、优点及应用场景，并结合代码示例进行讲解。&amp;#34; categories: [&amp;#34;Python&amp;#34;, &amp;#34;Transformer&amp;#34;] --- # Transformer 学习之路 - P-tuning 技术深入解析 P-tuning 是 Prompt Tuning 的一种增强版本，最早由清华大学提出，设计初衷是通过优化提示词来提升预训练模型在下游任务上的表现。P-tuning 不仅仅是简单的 Prompt Tuning，而是进一步利用**可学习的嵌入向量**来模拟提示词，以适应特定任务需求。P-tuning 特别适合语言模型微调，不仅适用于小规模的下游任务，也适用于需要更高灵活性的大模型。 ## P-tuning 的核心思想 与普通的 Prompt Tuning 通过固定的提示词嵌入来引导模型不同，P-tuning 使用一组可训练的嵌入向量作为提示词，这些嵌入向量会在训练过程中自动调整，生成特定任务的嵌入表示。相比普通的 Prompt Tuning，P-tuning 的灵活性和适应性更高。 ## P-tuning 的工作原理 1. **初始化嵌入向量**： - 与普通 Prompt Tuning 类似，P-tuning 初始化时也会创建一组提示词嵌入向量（即提示 token），但这些向量是随机初始化的，没有特定的含义。 2. **提示词和输入拼接**： - 将这些嵌入向量与实际输入文本拼接，形成输入序列。例如，对于句子 &amp;#34;I love this product!&amp;#34;，模型接收的输入会是： ``` [提示1] [提示2] ... [提示N] I love this product!</description>
    </item>
    
    <item>
      <title></title>
      <link>https://869413421.github.io/post/transformer/mrc_simple_version/</link>
      <pubDate>Mon, 01 Jan 0001 00:00:00 +0000</pubDate>
      
      <guid>https://869413421.github.io/post/transformer/mrc_simple_version/</guid>
      <description>--- title: &amp;#34;Transformer 学习之路 - 机器阅读理解任务实现&amp;#34; date: 2024-04-19T17:35:18+08:00 draft: false description: &amp;#34;深入解析如何使用 Transformer 实现机器阅读理解任务，涵盖数据预处理、模型训练与预测等关键步骤。&amp;#34; categories: [&amp;#34;Python&amp;#34;, &amp;#34;Transformer&amp;#34;] --- # Transformer 学习之路 - 机器阅读理解任务实现 在自然语言处理（NLP）领域，机器阅读理解（Machine Reading Comprehension, MRC）是一项核心任务，旨在让模型从给定的文本中提取出与问题相关的答案。Transformer 模型的出现极大地推动了这一领域的发展。本文将带你一步步实现一个基于 Transformer 的机器阅读理解任务。 ## 1. 环境准备 首先，我们需要安装必要的库。这里我们使用 `datasets` 库来加载数据集，`transformers` 库来加载预训练模型和分词器。 ```python !pip install datasets 2. 导入相关包 接下来，我们导入所需的包，包括数据集加载、模型加载、分词器、训练参数配置等。
from datasets import load_dataset from transformers import AutoModelForQuestionAnswering, AutoTokenizer, TrainingArguments, Trainer, DefaultDataCollator 3. 加载数据集 我们使用 cmrc2018 数据集，这是一个中文机器阅读理解数据集。通过 load_dataset 函数加载数据集，并查看数据集的结构。
datasets = load_dataset(&amp;#34;cmrc2018&amp;#34;, cache_dir=&amp;#34;data&amp;#34;) datasets 查看训练集中的第一条数据：
datasets[&amp;#34;train&amp;#34;][0] 4.</description>
    </item>
    
    <item>
      <title></title>
      <link>https://869413421.github.io/post/transformer/multiple_choice/</link>
      <pubDate>Mon, 01 Jan 0001 00:00:00 +0000</pubDate>
      
      <guid>https://869413421.github.io/post/transformer/multiple_choice/</guid>
      <description>--- title: &amp;#34;Transformer 学习之路 - 多项选择任务实现&amp;#34; date: 2024-04-19T17:35:18+08:00 draft: false description: &amp;#34;深入解析如何使用 Transformer 技术实现多项选择任务，涵盖数据预处理、模型训练与评估等关键步骤。&amp;#34; categories: [&amp;#34;Python&amp;#34;, &amp;#34;Transformer&amp;#34;] --- # Transformer 学习之路 - 多项选择任务实现 在自然语言处理（NLP）领域，多项选择任务是一种常见的任务类型，旨在从给定的多个选项中选择最合适的答案。本文将深入探讨如何使用 Transformer 技术实现多项选择任务，并详细分析其中的技术原理和实现步骤。 ## 1. 导入相关包 首先，我们需要导入必要的 Python 包。这些包包括 `evaluate`、`datasets` 和 `transformers`，它们分别用于评估模型性能、加载数据集以及构建和训练 Transformer 模型。 ```python !pip install evaluate from google.colab import drive import sys drive.mount(&amp;#39;/content/drive&amp;#39;) import evaluate from datasets import load_dataset from transformers import AutoModelForMultipleChoice, AutoTokenizer, TrainingArguments, Trainer 2. 加载数据集 接下来，我们加载 C3 数据集。C3 是一个中文多项选择数据集，包含上下文、问题和多个选项。我们首先加载数据集，并检查其结构和内容。
c3 = load_dataset(&amp;#34;clue/clue&amp;#34;, &amp;#34;c3&amp;#34;, cache_dir=&amp;#34;.</description>
    </item>
    
    <item>
      <title></title>
      <link>https://869413421.github.io/post/transformer/ner/</link>
      <pubDate>Mon, 01 Jan 0001 00:00:00 +0000</pubDate>
      
      <guid>https://869413421.github.io/post/transformer/ner/</guid>
      <description>--- title: &amp;#34;Transformer 学习之路 - 基于 Transformers 的命名实体识别&amp;#34; date: 2024-04-19T17:35:18+08:00 draft: false description: &amp;#34;深入解析如何使用 Transformers 进行命名实体识别，涵盖技术原理、代码实现及应用场景&amp;#34; categories: [&amp;#34;Python&amp;#34;, &amp;#34;Transformer&amp;#34;] --- # Transformer 学习之路 - 基于 Transformers 的命名实体识别 在自然语言处理（NLP）领域，命名实体识别（NER）是一项基础且重要的任务，旨在从文本中识别出特定类别的实体，如人名、地名、组织名等。本文将深入探讨如何使用 Transformers 库实现命名实体识别，并详细解析其技术原理和实现步骤。 ## 1. 背景与问题 命名实体识别是信息抽取的核心任务之一，广泛应用于知识图谱构建、问答系统、机器翻译等领域。传统方法依赖于规则和特征工程，而基于深度学习的 Transformer 模型通过自注意力机制，能够捕捉文本中的长距离依赖关系，显著提升了 NER 的性能。 ### 1.1 Transformer 的优势 - **自注意力机制**：Transformer 通过自注意力机制，能够同时关注输入序列中的每个位置，捕捉全局依赖关系。 - **并行计算**：与 RNN 不同，Transformer 无需按顺序处理输入，能够充分利用 GPU 的并行计算能力。 - **预训练模型**：通过大规模预训练，Transformer 模型能够学习到丰富的语言知识，适用于多种下游任务。 ## 2. 实现步骤 ### 2.1 环境准备 首先，安装必要的库： ```python ! pip install datasets evaluate seqeval 2.2 导入相关包 import evaluate from datasets import load_dataset from transformers import AutoTokenizer, AutoModelForTokenClassification, TrainingArguments, Trainer, DataCollatorForTokenClassification 2.</description>
    </item>
    
    <item>
      <title></title>
      <link>https://869413421.github.io/post/transformer/summarizatioin_glm/</link>
      <pubDate>Mon, 01 Jan 0001 00:00:00 +0000</pubDate>
      
      <guid>https://869413421.github.io/post/transformer/summarizatioin_glm/</guid>
      <description>--- title: &amp;#34;Transformer 学习之路 - 基于前缀模型的文本摘要技术&amp;#34; date: 2024-04-19T17:35:18+08:00 draft: false description: &amp;#34;深入解析基于前缀模型的文本摘要技术，结合代码示例，详细分析其技术原理与应用场景。&amp;#34; categories: [&amp;#34;Python&amp;#34;, &amp;#34;Transformer&amp;#34;] --- # Transformer 学习之路 - 基于前缀模型的文本摘要技术 在自然语言处理（NLP）领域，文本摘要是一项重要的任务，它旨在从长篇文章中提取出关键信息，生成简洁的摘要。随着 Transformer 模型的兴起，基于前缀模型（Prefix Model）的文本摘要技术逐渐成为研究热点。本文将深入探讨前缀模型的原理、应用场景及其实现细节，并结合代码示例帮助读者理解如何应用这一技术。 ## 前缀模型的基本概念 前缀模型是一种特殊的文本生成模型，通过在输入序列前添加“前缀”来引导模型生成符合特定需求的输出。其核心思想是通过给输入文本添加适当的“前缀”来调整模型的生成方向。例如，如果想要生成一个故事摘要，我们可以添加“摘要:”作为前缀，这样模型会理解它应该输出的是摘要而不是继续讲述故事。 ### 前缀模型的典型应用场景 前缀模型在多种生成式任务中都有广泛应用，包括但不限于： - **文本摘要**：在输入文本前加上“总结：”或“摘要：”这样的前缀，引导模型输出总结性的句子。 - **机器翻译**：在输入前添加特定语言标签，例如在英文文本前添加“[Translate to French]”，模型将生成法语的翻译。 - **对话生成**：在输入前加入角色标签或话题前缀，模型可以生成具有特定风格的对话。 - **代码生成**：添加“生成代码：”等指示语，使模型知道要生成符合指令的代码。 ## 前缀模型的结构和实现 前缀模型通常基于序列到序列（Seq2Seq）架构，前缀信息可以直接添加到输入序列中。以文本摘要为例，其实现步骤如下： 1. **定义前缀**：在输入文章前添加“摘要：”，即构造输入`“摘要：[原文内容]”`。 2. **编码阶段**： - 编码器会处理整个带前缀的输入序列，把“摘要：”这一部分信息编码为隐藏状态。 - 编码器将这个包含“摘要”提示的隐藏状态传递给解码器。 3. **解码阶段**： - 解码器根据编码器的输出，结合“摘要：”前缀的暗示，从而生成一段总结性内容。 ### 实现示例 假设我们使用 Hugging Face 提供的 `transformers` 库的一个预训练模型来实现前缀模型，以下是一个简单的代码示例： ```python from transformers import AutoTokenizer, AutoModelForSeq2SeqLM # 加载模型和分词器，例如BART或T5 tokenizer = AutoTokenizer.</description>
    </item>
    
  </channel>
</rss>
