字节跳动后端青训营笔记:Go语言进阶

1.语言进阶&依赖管理

1.1 语言进阶

从并发编程的视角了解Go高性能的本质。

1.1.1 并发&并行

在单核CPU下,线程实际还是串行执行的。操作系统中有一个组件叫做任务调度器,它将CPU的时间片(window下最小约为15毫秒)分给不同的程序使用,只是由于CPU在线程间的切换非常快,人类一般是感觉不到的,所以会觉得他们是同时运行的。一句话说就是微观串行,宏观并行

一般将这种线程轮流使用CPU的做法称为并发,concurrent,多核 cpu下,每个 核(core) 都可以调度运行线程,这时候线程可以是并行的。

这里引用Golang语言创造者的Rob Pike的一句描述:

  • 并发(concurrent)是同一时间应对(dealing with)多件事情的能力
  • 并行(parallel)是同一时间动手做(doing)多件事情的能力

提到高并发,Go也是一大利器。

1.1.2 线程&协程
image-20230116210841153

线程

一个进程之内可以分为一到多个线程,一个线程就是一个指令流,指令流的一条条指令按照一定的顺序加载给CPU执行,比如在Java中,线程作为最小的调度单位,进程作为资源分配的最小单位。

协程

协程不是系统线程,很多时候被称为轻量级线程、微线程、纤程等,简单来说可以认为协程是线程里面不同的函数,这些函数之间可以相互快速切换,协程和用户线态线程非常接近,用户态线程之间的切换不需要陷入内核,但这不是绝对的,部分系统中的切换也是需要内核态线程的辅助的。协程是编程语言提供的特性(之间的切换方式与过程可以由程序员确定),属于用户态操作。

小结

线程:内核态、轻量级、栈MB级别

协程:用户态、线程可以跑多个协程、栈KB级别


在Go语言中开启协程只需要在函数调用之前加上go关键字即可。比如下面这段代码,通过协程的方式,打印一段输出。

package main

import (
	"fmt"
	"time"
)
func main() {
	HelloGoRoutine()
}
func hello(i int) {
	println("hello goroutine:" + fmt.Sprint(i))
}

func HelloGoRoutine() {
	for i := 0; i < 5; i++ {
        //开启协程
		go func(j int) {
			hello(j)
		}(i)
	}
	time.Sleep(time.Second)
}

Go的CSP并发模型

与其他编程语言不同,Go语言除了支持传统语言的 多线程共享内存并发模型之外,还有自己特有的 **CSP(communicating sequential processes)**并发模型,一般情况下,这也是Go语言推荐使用的。

与传统的 多线程通过内存来通信 相比,CSP讲究的是 以通信的方式来共享内存

image-20230116215148018

普通的线程并发模型,他们的线程通信一般是通过共享内存的方式来进行的,非常典型的方式就是,在访问共享数据(数组、Map等)的时候是通过锁来访问,因此很多时候会衍生出一种叫做 线程安全的数据结构的东西。

而Go的CSP并发模型则是通过goroutinechannel来实现的。

  • goroutine 是Go语言中并发的执行单位。可以理解为用户空间的线程。
  • channel是Go语言中不同goroutine之间的通信机制,即各个goroutine之间通信的”管道“,有点类似于Linux中的管道。

Channel

channel类似与一个队列,满足先进先出的规则,严格保证收发数据的顺序,每一个通道只能通 过固定类型的数据如果通道进行大型结构体、字符串的传输,可以将对应的指针传进去,尽量的节省空间。

image-20230116221510849

在Go中,可以通过make函数创建通道(channel,后文都简称通道)。

make(chan 元素类型,[缓冲大小])

无缓冲:make(chan int)

在发送和接收者之间同步的传递消息,这种方式可以保证消息的顺序和每个消息只被接收一次。

有缓冲:make(chan int,2)

缓冲通道具有固定大小的缓冲区,发生者和接收者之间不再是同步的,可以提高程序的性能,但可能会出现消息的丢失或重复。

基本操作

  • 操作符<-

取出数据使用操作符 <-操作符右边是输入变量,操作符左边是通道代表数据流入通道内.

// 声明一个通道
var a chan int
a <- 5

下面通过一段简单的生产-消费模式的代码示例,熟悉channel的基本使用。

package main

func main() {
	CalSquare()
}

func CalSquare() {
	//无缓冲channel
	src := make(chan int)
	//有缓冲channel
	dest := make(chan int, 3)
	//协程A生成0-9数字
	go func() {
		defer close(src)
		for i := 0; i < 10; i++ {
			src <- i
		}
	}()
	//协程B计算输入数字的平方
	go func() {
		defer close(dest)
		for i := range src {
			dest <- i * i
		}
	}()
	//主线程打印输出最终结果
	for i := range dest {
		//TODO
		println(i)
	}
}

程序中,协程A作为生产者角色,生产出数字0-9以供下面消费者角色的B协程,该协程从src管道取到数字之后,对数字进行平方计算,再将结果最终流入dest通道内,最后使用主线程打印最终处理之后的结果。


并发安全Lock—传统并发模式

考虑下面这个场景:

使用5个协程并发执行,对某变量执行2000次的+1操作。

package main

import (
	"sync"
	"time"
)

var (
	x    int64
	lock sync.Mutex
)

func main() {
	Add()
}

// 加锁的自增方法
func addWithLock() {
	for i := 0; i < 2000; i++ {
		lock.Lock()
		x += 1
		lock.Unlock()
	}
}

// 不加锁的自增方法
func addWithoutLock() {
	for i := 0; i < 2000; i++ {
		x += 1
	}
}

func Add() {
	x = 0
	for i := 0; i < 5; i++ {
		go addWithoutLock()
	}
	time.Sleep(time.Second)
	println("没加锁:", x)
	x = 0
	for i := 0; i < 5; i++ {
		go addWithLock()
	}
	time.Sleep(time.Second)
	println("加锁的:", x)
}

上面的程序中,同样都是启用5个协程并发对x自增2000次,一个使用了lock一个没有,在并发情况下,没有加锁的操作可能会引起数据的错误,比如未加锁情况下最终的是8337,而加锁的情况下确实正确结果10000,可见加锁在一定程度上可以防止数据错误,保证了原子性。

  • 这里导入了一个名为sync的库。其中的Mutex类型直接支持互斥锁关系,它的Lock方法能够获取锁和释放锁Unlock。每个goroutine试图访问x变量时,他都会调用mutexLock方法来获取一个互斥锁。如果其他goroutine已经获得这个锁的情况下,该操作将会被阻塞,直到其他goroutine调用了Unlock方法释放锁变回可用状态。
  • LockUnlock直接的代码段中的内容goroutine可以随便读取或修改,这个这个代码叫做临界区(注意前面提到的两种并发模式的示意图)。锁的持有者在其他goroutine获取该锁之前需要调用Unlock,任务执行结束之后释放锁是必要的,无论哪一条路径通过函数都需要释放,即使是在出现异常的情况下。
  • 在上面的代码中,由于变量x的自增函数中只有短短的一行,没有分支调用,在代码最后调用Unlock就显得更加的直截了当。但在复杂的临界区应用中,尤其是必须要尽早处理错误并返回的情况下,就很难靠认为的去判断对LockUnlock的调用是在所有的路径中都是严格配对的了。所以面对这种情况,Go也是有自己独有的应对方法,使用go中的defer,可以使用defer来调用Unlock,临界区会隐式的延伸到函数作用域的最后,这样我们就无需每次都要记得使用Unlock去释放锁,Go会帮我们完成这件事。

WaitGroup

在Go语言中除了使用通道(channel)和互斥锁(lock)进行并发同步之外,还可以使用等待组WaitGroup来完成多个任务的同步,与前面提到的锁不同,等待组可以保证在并发环境中完成指定数量的任务。

image-20230117095515900

WaitGroup类型中,每个sync.WaitGroup内部维护了一个计数器,初始默认值为0,详情见上图所示。计数器计数逻辑如下:

  • 开启协程+1
  • 执行结束-1
  • 主协程阻塞直到计数器为0

修改之前 使用协程打印输出的代码:

package main

import (
	"fmt"
	"sync"
	"time"
)

func main() {
	//HelloGoRoutine()
	ManyGWaitGroup()
}
func hello(i int) {
	println("hello goroutine:" + fmt.Sprint(i))
}

func ManyGWaitGroup() {
	var wg sync.WaitGroup
    //增加5个等待组数量
	wg.Add(5)
	for i := 0; i < 5; i++ {
		go func(j int) {
            //当一个goroutine完成之后,减少一个等待组
			defer wg.Done()
			hello(j)
		}(i)
	}
    //直到所有的操作都完成
	wg.Wait()
}
  • func(*WaitGroup) Add(delta int)

Add方法向内部计数加上deltadelta可以是负数;如果内部计数器变为0,Wait方法阻塞等待的所有线程都会释放,如果计数器小于0,方法panic。注意Add加上正数的调用应在Wait之前,否则Wait可能只会等待很少的线程。一般来说本方法应在创建新的线程或者其他应等待的事件之前调用。

  • func(*WaitGroup) Done

Done方法减少WaitGroup计数器的值,应在线程的最后执行,Done的执行应标志着一个goroutine的结束

  • func(*WaitGroup) Wait

Wait方法阻塞直到WaitGroup计数器减为0。如果WaitGroup不为0,那么程序就会一直阻塞在Wait函数这里


1.2 依赖管理

了解Go语言依赖管理的演进路线。

1.2.1 GOPATH

目前为止,Go的依赖管理主要经历了三个阶段:

GOPATH->Go Vender->Go Module

整个路线主要围绕着实现下面两个目标来迭代发展的:

  • 不同环境(项目)依赖版本不同
  • 控制依赖库的版本
image-20230117104308978

所有工程代码要求放在GOPATH/src目录下
工程本身也将作为一个依赖包,可以被其它 GOPATH/src 目录下的工程引用
在 $GOPATH/src 下进行 .go 文件或源代码的存储,我们可以称其为 GOPATH 的模式

弊端

image-20230117105056546
  • 没有版本控制的概念
  • 所有的项目需要存放在$GOPATH/src目录下,否则就不能编译。

1.2.2 Go Vender
  • 解决 GOPATH模式 所有项目都在$GOPATH/src目录的问题,可以随处可以创建项目,不用扎堆src 目录下。
  • 通过每个项目引入一份依赖的副本,解决了多个项目需要同一个package依赖开的冲突问题。
image-20230117105437601
  1. 项目目录西卡增加了vender文件,所有的依赖包副本形式存放在ProjectRoot/vender下。
  2. 依赖的寻址方式:vender->GOPATH

不足

image-20230117110650847
  1. 无法控制依赖版本
  2. 更新项目又可能出现依赖冲突,导致编译出错

1.2.3 GoModule
  • 通过go.mod文件管理依赖包版本
  • 通过go get/go mod指令工具管理依赖包
  • 实现定义版本规则和管理项目依赖关系,类比Java中的Maven

核心三要素

  • 配置文件,描述依赖-go.mod
  • 中央仓库管理依赖库-Proxy
  • 本地工具-go get/mod

go.mod

启用了 Go modules 的项目,初始化项目时,会生成一个 go.mod 文件。描述了当前项目(也就是当前模块)的元信息。

image-20230117111736841

首先模块路径用来标识一个模块,从模块路径可以看出来从哪里找到该模块,如果是clone自github,则前缀就是Github仓库,依赖包的源代码由github托管。

如果项目的子包想被单独的引用,就需要通过单独的init go。mod文件进行管理。

image-20230117112409253

image-20230117112419299

版本管理

image-20230117112957421
  • gopathgovender都是源码副本方式依赖,没有版本规则的概念,而gomod为了方便管理定义了版本规则,分为语义化版本基于commit的伪版本
  • 在语义化版本规则中,不同的MAJOR版本表示不兼容的API,所以即使是同一个库,MAJOR版本不同也不会被认为为是不同的模块;
  • MINOR版本通常是新增函数或者功能,向后兼容;而PATCH版本一般是修复BUG;
  • 基于commit的版本中,基础版本前缀和语义化版本一样,时间戳(yyyymmddhhmmss)也就是该提交Commmit的时间,最后的校验码部分包含了12位的哈希前缀,每次提交commitgo都会默认生成一个伪版本号。

indirect

  • indirect是依赖单元中的特殊标识符之一,以后缀的形式存在,表示go.mod对应的当前模块,没有直接导入该依赖的包,也就是非直接依赖,标识间接依赖,如:

image-20230117114422690

  • 在执行命令go mod tidy时,Go module 会自动整理go.mod 文件,如果有必要会在部分依赖包的后面增加// indirect注释。一般而言,被添加注释的包肯定是间接依赖的包,而没有添加// indirect注释的包则是直接依赖的包,即明确的出现在某个import语句中。

依赖图

依赖分发&回源

就是我从哪里来,怎么来的问题。

github是比较常见的代码托管平台,而Go Modules系统中定义的依赖,最终可以对应到多版本代码管理系统中某一个项目的特定提交版本,这样的话,对于go mod中定义的依赖,则直接可以从对应仓库中下载指定的软件依赖,从而完成依赖分发。

但是直接使用版本管理仓库下载依赖,会存在一些问题,比如无法保证构建确定性,软件作者可以直接在代码平台对软件的版本进行增删改查,导致下次构建使用的是另外一个版本依赖,或者找不到依赖版本,无法保证依赖的可用性,依赖软件作者可以直接在平台删除软件,导致依赖不可用,大幅度增加第三方代码托管平台的压力。 基于此,可以使用下面的Proxy的方式解决这些问题。

Proxy

go proxy是一个服务站点,他会缓存源站中的软件名称,缓存的软件版本不会改变,并且在源软件删除之后依然可以用,从而实现了供immutabilityavailable的依赖分发;使用proxy之后,构建时会直接从proxy站点拉取依赖。

变量GOPROXY

GOPROXY="https://proxy1.cn,https://proxy2.cn ,direct"服务站点URL列表,"direct”表示源站。

go module通过goproxy环境变量控制如何使用go proxy;goproxy是一个goproxy站点的URL列表,可以使用direct表示源站。对于示例配置,整体的依赖寻找路径,会优先从proxy下载依赖,如果proxy1不存在,会从proxy2继续寻找,如果proxy2z中不存在则会返回到源站直接下载依赖,缓存到proxy站点中。

go get

get 命令用来解决go模块及其依赖项目的下载、创建和安装问题。实际该命令线执行从在线仓库(BitBucketGitHubGoogle ``Code、国内的gitee等)下载模块(包),再执行Go Install命令。
get 命令是依赖git。

get 会先下载相关依赖项目模块,下载时每个包或包的部分模块,下载的版本默认遵从以下顺序:

最新release版 > 最新pre-release版 > 其他可用的较高版本

官方文档

go mod


2.测试&项目实战

2.1 测试

从单元测试实践出发,提升质量意识。

  • 单元测试
  • Mock测试
  • 基准测试

什么?你写代码不用测试?不要钱了是吧!

  • 营销配置错误,导致非预期用户享受权益,资金损失10w+
  • 用户体现,幂等失效,短时间可以多次体现,资金损失20w+
  • 代码逻辑错误,广告位被占,无法出广告,收入损失500w+
  • 代码指针使用错误,导致APP不可用,损失kw+

所以,测试就成了避免事故的最后一道屏障

如上图所示,从上到小,覆盖率逐层变大,成本却逐层降低。

  • 回归测试一般是QA同学手动通过终端回归的一些固定的主流程场景。
  • 集成测试是对系统功能维度做的测试验证;
  • 单元测试测试开发阶段,开发者对单独的函数、模块做功能的测试,层级从上至下,测试成本逐渐减低,而覆盖率逐步上升,所以单元测试的覆盖率一定程度上决定着代码的质量
2.1.1 单元测试

单元测试主要包括输入、测试单元。输出以及校对,单元测试的概念比较广,包括了接口、函数、模块等;用最后的校对来保证代码的功能与我们的预期相符;一方面可以保证质量,在整体覆盖率足够的情况下,一定程度上既保证了新功能本身的正确性,也不会破坏原有代码的正确性。另一方面可以提升效率,在代码有bug的情况下,通过编写单元测试,可以在一个较短的周期内定位和修复问题。

单元测试规则

下面是单元测试的一些基本规范,这样从文件上就很好的区分源代码和测试代码,以Test开头,且理解的第一个字母大写。

  • 所有测试文件以_test.go结尾;
  • func TestXxx(*testing.T);
  • 初始化逻辑放到TestMain中;
package test

import "testing"

func HelloTom() string {
    return "Jerry"
}

func TestHelloTom(t *testing.T) {
    output := HelloTom()
    expectOutput := "Tom"
    if output != expectOutput {
        t.Errorf("Expected %s do not match actual %s", expectOutput, output)
    }
}

在上面的测试代码中,我们调用HelloTom()分方法,我们的正确预期是希望测试输出Tom,但实际函数输出一个Jerry,明显这是不符合预期的,所以测试不予通过。

单测-assert

前面测试直接使用的是比较运算符,除此之外,还有很多现有的aeert包可以帮助我们实现测试中的比较操作。

package test

import (
	"github.com/stretchr/testify/assert"
	"testing"
)

func HelloTom() string {
	return "Tom"
}

func TestHelloTom(t *testing.T) {
	output := HelloTom()
	expectOutput := "Tom"
	assert.Equal(t, expectOutput, output)
	/*if output != expectOutput {
		t.Errorf("Expected %s do not match actual %s", expectOutput, output)
	}*/
}

单测-覆盖率

package test

import (
	"github.com/stretchr/testify/assert"
	"testing"
)

func JudgePassLine(score int16) bool {
	if score >= 60 {
		return true
	}
	return false
}

func TestJudgePassLineTrue(t *testing.T) {
	isPass := JudgePassLine(80)
	assert.Equal(t, true, isPass)
}

这是一个判断成绩是否合格的程序,返回bool,输入分数为80,执行测试之后发现只有%66.7左右的覆盖率。因为用例为80的时候,只是跑了程序的前面两行,也就是分数大于60的逻辑,而剩下的返回false部分的逻辑并没有得到测试,所有覆盖率自然不会是100%

所以,我们新增一个测试如下:这样就可以做到测试覆盖率百分百。

func TestJudgePassLineTrue(t *testing.T) {
	isPass := JudgePassLine(80)
	assert.Equal(t, true, isPass)
}

func TestJudgePassLineFail(t *testing.T) {
	isPass := JudgePassLine(40)
	assert.Equal(t, false, isPass)
}

单测-Tips

  • 一般覆盖率: 50%-60%,较高可达80%
  • 测试分支相互独立,全面覆盖
  • 测试单元粒度足够小,函数单一职责

单测-依赖

实际工程中,复杂的项目一般都会有依赖,而我们单元测试需要保证稳定性和幂等性,稳定性是指相互隔离,能在任何环境、任何时间运行测试。

幂等性指的是每一次测试运行都因该产生与之前一样的结果,而要实现这一目的就要用到Mock机制。

示例-文件处理

如图,这个例子中,我们将文件中的第一行字符串中的11替换为00,执行单元测试并通过单元测试,而我们的单元测试需要依赖本的文件,如果文件被修改或者删除,测试就会出现fail。为了保证测试case的稳定性,就需要对读取文件的函数进行mock屏蔽对于文件的依赖。


2.1.2 Mock测试

我们可以使用Monkey,这个开源的mock测试库,对method或者实例的方法进行mock,Mockey Patch的作用域在Runtime,在运行时通过Gounsafe包,能够将内存中的函数地址替换为运行时函数地址。

image-20230118154214486

快速Mock函数:

  • 为一个函数打桩
  • 为一个方法打桩

参考阅读


2.1.3 基准测试

[见]【高质量编程与性能调优】篇


2.2 项目实战

通过项目需求、需求拆解、逻辑涉及、代码实现来感受真实的项目开发基本流程。

2.2.1 需求背景

社区话题页面

  • 展示话题(标题、文字描述)和回帖列表
  • 暂不考虑前端页面的实现,仅仅实现一个本地web服务
  • 话题和回帖数据用文件存储,不涉及数据库连接

需求用例

用户浏览页面消费,涉及页面的展示,包括话题内容和回帖列表,从图中可以抽象出来两个实体,以及实体之间的属性与联系,从而定义出对应的结构体。

E-R图


2.2.2 分层结构

  • 数据层:数据Model,外部数据的增删改查
  • 逻辑层:业务Entity,处理核心业务逻辑输出
  • 视图层:视图View,处理和外部的交互逻辑

整体分为三层,repository数据层,service逻辑层,controoler视图层。数据层关联底层数据模型,也就是这里的model,封装外部数据的增删改查,我们的数据存储在本地文件,通过文件操作拉取话题,帖子数据;数据层面向逻辑层,对service层透明,屏蔽下游数据差异,也就是不管下游是文件,还是数据库,还是微服务等,对service层的接口模型是不变的。

Service逻辑层处理核心业务逻辑,计算打包业务实体entiy,对应我们的需求,就是话题页面,包括话题和回帖列表,并上送给视图层;Controller视图层负责处理和外部的交互逻辑,以view视图的形式返回给客户端,对于我们需求,我们封装json格式化的请求结果,api形式访问即可。

组件工具

  • gin

    这里主要涉及 路由分发

  • go mod

    • go mod init
    • go get

    初始化go mod配置文件以及获取gin依赖下载。

    go get gopkg.in/gin-gonic/gin.v1@v1.3.0
    

    如果执行get命令之后长时间没反应出现超时情况,可以在命令行执行下面的命令之后再次尝试即可下载。

    go env -w GOPROXY=https://goproxy.cn
    

2.2.3 开发步骤

Reposity

开发工作准备好之后,首先处理的是数据层逻辑,可以使用struct定义好结构体,文件中的数据格式如下,实现QueryTopicById以及QueryPostsByParentId两个查询。

数据索引

由于需要根据ID查询到帖子和话题数据,在没有使用数据库的情况下, 我们如何实现呢?最直接的方式就是针对数据文件进行全盘扫描,但显然这是比较耗时的操作,并不是最优的选择,所以这里就用到了 索引的概念。

索引就类似于书本的目录,通过这种方式,我们可以快速的定位到我们所需内容的位置。具体的,这里使用map结构来实现内存索引,在数据服务对外暴露之前,利用文件元数据初始化全局内存索引,这样就可以实现O(1)时间复杂度的查找操作了。

  • 初始化话题数据索引

初始化数据话题数据
基本逻辑:打开文件,基于file初始化scanner,通过迭代器方式遍历数据行,转为结构体存储至内存map

func initTopicIndexMap(filePath string) error {
	open, err := os.Open(filePath + "topic")
	if err != nil {
		return err
	}
	scanner := bufio.NewScanner(open)
	topicTmpMap := make(map[int64]*Topic)
	for scanner.Scan() {
		text := scanner.Text()
		var topic Topic
		if err := json.Unmarshal([]byte(text), &topic); err != nil {
			return err
		}
		topicTmpMap[topic.Id] = &topic
	}
	topicIndexMap = topicTmpMap
	return nil
}

func initPostIndexMap(filePath string) error {
	open, err := os.Open(filePath + "post")
	if err != nil {
		return err
	}
	scanner := bufio.NewScanner(open)
	postTmpMap := make(map[int64][]*Post)
	for scanner.Scan() {
		text := scanner.Text()
		var post Post
		if err := json.Unmarshal([]byte(text), &post); err != nil {
			return err
		}
		posts, ok := postTmpMap[post.ParentId]
		if !ok {
			postTmpMap[post.ParentId] = []*Post{&post}
			continue
		}
		posts = append(posts, &post)
		postTmpMap[post.ParentId] = posts
	}
	postIndexMap = postTmpMap
	return nil
}

  • 查询话题数据

有了内存索引,直接根据查询key获得对应的value就可以了,这里用到了一个sync.once,主要用于适用高并发场景下只执行一次,这里基于once的实现模式就是平常说的单例模式,减少存储的浪费。

func NewTopicDaoInstance() *TopicDao {
	topicOnce.Do(
		func() {
			topicDao = &TopicDao{}
		})
	return topicDao
}

func (*TopicDao) QueryTopicById(id int64) *Topic {
	return topicIndexMap[id]
}

Service

具体的编排流程,通过err控制流程退出,正常会返回页面信息。

func (f *QueryPageInfoFlow) Do() (*PageInfo, error) {
	if err := f.checkParam(); err != nil {
		return nil, err
	}
	if err := f.prepareInfo(); err != nil {
		return nil, err
	}
	if err := f.packPageInfo(); err != nil {
		return nil, err
	}
	return f.pageInfo, nil
}

接下来只需要编写对应的每一个实现方法即可。写完controller之后,创建一个服务启动入口server.go

func main() {
	if err := Init("./data/"); err != nil {
		os.Exit(-1)
	}
	r := gin.Default()
	r.GET("/community/page/get/:id", func(c *gin.Context) {
		topicId := c.Param("id")
		data := controller.QueryPageInfo(topicId)
		c.JSON(200, data)
	})
	err := r.Run()
	if err != nil {
		return
	}
}

func Init(filePath string) error {
	if err := repository.Init(filePath); err != nil {
		return err
	}
	return nil
}

执行下面的命令启动并访问接口:

  • go run server.go
  • http://127.0.0.1:8080/community/page/get/2

项目扩展:

  • 帖子发布支持
  • 本地Id生成需要保证不重复、唯一性
  • Append文件,更新索引、注意Map的并发安全问题

2.2.4 项目小结
  • os.Open()

    Open打开一个文件用于读取。如果操作成功,返回的文件对象的方法可用于读取数据;

    open, err := os.Open(filePath + "topic")
    if err != nil {
     return err
    }
    
  • func NewReader(rd io.Reader) *Reader

    bufio包实现了有缓冲的I/O。它包装一个io.Readerio.Writer接口对象,创建另一个也实现了该接口,且同时还提供了缓冲和一些文本I/O的帮助函数的对象。

    bufio.NewReader()使用 NewReader 读取文件时,首先,我们需要打开文件,接着, 使用打开的文件返回的文件句柄当作 函数参数 传入 NewReader

    最后,我们使用 NewReader 返回的 reader 对象调用 Read 来读取文件。文件读取结束的标志是返回的 n 等于 0,因此,如果我们需要读取整个文件内容,那么我们需要使用 for 循环 不停的读取文件,直到 n 等于 0。

    注意,在上面的项目中实际使用到的是``func NewReader(rd io.Reader) *Scanner构造函数。读取数据之后返回一个 Scanner,再调用其 Scan()方法对文件数据进行逐行扫描读取,直到遇到文件末尾,方法返回 false`。


致谢&参考

  • 协程的概念
  • 《Java并发编程之美》
  • 字节内部课PPT
  • 使用monkey进行mock
  • Go在线手册

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:/a/629971.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

第十四届蓝桥杯大赛软件赛国赛C/C++ 大学 B 组 拼数字

//bfs只能过40%。 #include<bits/stdc.h> using namespace std; #define int long long int a,b,c,dp[2028]; struct s {int x,y,z;string m; }; map<vector<int>,int>k; signed main() {ios::sync_with_stdio(false);cin.tie(0),cout.tie(0);cin>>a…

[数据结构]红黑树的原理及其实现

文章目录 红黑树的特性红黑树的时间复杂度推导&#xff1a;结论红黑树与AVL树比较 红黑树的插入红黑树的节点定义调整策略思考情况2&#xff1a;思考情况3&#xff1a; 代码实现myBTRee.htest.cpp 红黑树的特性 红黑树最常用的平衡二叉搜索树。跟AVL树不同的是&#xff0c;红黑…

基于springboot+vue+Mysql的交流互动系统

开发语言&#xff1a;Java框架&#xff1a;springbootJDK版本&#xff1a;JDK1.8服务器&#xff1a;tomcat7数据库&#xff1a;mysql 5.7&#xff08;一定要5.7版本&#xff09;数据库工具&#xff1a;Navicat11开发软件&#xff1a;eclipse/myeclipse/ideaMaven包&#xff1a;…

【RAG 论文】IRCoT:基于 CoT 的交叉检索解决多步骤问题

论文&#xff1a;Interleaving Retrieval with Chain-of-Thought Reasoning for Knowledge-Intensive Multi-Step Questions ⭐⭐⭐⭐ ACL 2023, arXiv:2212.10509 Code: github.com/stonybrooknlp/ircot 论文速读 大多数 RAG 都是一次检索来辅助 LLM 生成&#xff0c;但是面对…

品鉴中的艺术审美:如何将红酒品鉴提升为一种艺术体验

品鉴云仓酒庄雷盛红酒不仅仅是对酒本身的体验&#xff0c;更是一种艺术和审美的过程。将品鉴提升为一种艺术体验&#xff0c;需要我们用更细腻的感官、情感和认知去感受葡萄酒的魅力。 首先&#xff0c;我们要意识到品鉴葡萄酒是一种多感官的体验。除了基本的视觉、嗅觉和味觉…

利用管道通信(pipe)测量进程间的上下文切换(context switch)开销

利用管道通信(pipe)测量进程间的上下文切换(context switch)开销 《https://pages.cs.wisc.edu/~remzi/OSTEP/cpu-mechanisms.pdf》 Measuring the cost of a context switch is a little trickier. The lmbench benchmark does so by running two processes on a single CPU…

Vditor集成于VUE笔记

文章目录 前言一、安装Vditor二、渲染markdown三、options3.1 自建CDN3.2 outline大纲不显示、不跳转问题3.3 upload 图片/视频上传3.4 toolbar提示位置点击事件more中文字 3.5 sv分屏渲染模式隐藏编辑框3.6 after中的insertValue或者setValue 前言 Vditor是一款易于使用的 Ma…

3D Web轻量化引擎HOOPS Communicator如何处理DWG文件中的图纸?

在当今工程设计和建筑领域&#xff0c;数字化技术已经成为不可或缺的一部分。HOOPS Communicator作为一种强大的三维数据可视化工具&#xff0c;被广泛应用于处理各种CAD文件&#xff0c;其中包括AutoCAD的DWG格式。在这篇文章中&#xff0c;我们将探讨HOOPS Communicator是如何…

DRF 跨域问题

【一】说明 CORS&#xff08;跨来源资源共享&#xff0c;Cross-Origin Resource Sharing&#xff09;是一种浏览器技术的规范&#xff0c;旨在解决浏览器同源策略&#xff08;Same-Origin Policy&#xff09;的限制&#xff0c;使得Web服务可以从不同的网域&#xff08;源&…

org.springframework.jdbc.BadSqlGrammarException

Cause: java.sql.SQLSyntaxErrorException: Table ‘web.emp’ doesn’t exist 产生原因&#xff1a;web表找不到&#xff0c;所以可能数据库配置错误 spring.datasource.urljdbc:mysql://localhost:3306/web02 更改完成后运行成功

Web浏览器的兼容性测试需要考虑哪些测试点?

测试web网站兼容性时&#xff0c;可以使用各种测试用例来确保网站在不同浏览器中的良好兼容性。以下是一些常见的兼容性测试用例示例&#xff1a; 1. 页面加载测试&#xff1a; - 确保网站在不同浏览器中正常加载&#xff0c;没有加载错误。 - 检查页面加载时间&#xff0c;…

场景文本检测识别学习 day09(SSM、S4、Mamba、S6)

SSM&#xff08;State Space Model&#xff09; SSM是一个针对连续函数的模型&#xff0c;即输入是连续函数&#xff0c;输出也是连续函数。上图为状态方程和输出方程&#xff0c;其中h(t)是当前时刻的状态&#xff0c;x(t)是当前时刻的输入&#xff0c;h’(t)是下一个时刻的状…

应用TRIZ创新原理解决技术矛盾的步骤是什么样的?

如何在有限的时间和资源下&#xff0c;高效地解决技术矛盾&#xff0c;推动技术的进步和创新呢&#xff1f;答案就隐藏在TRIZ创新原理之中。 首先&#xff0c;我们需要了解什么是TRIZ创新原理。TRIZ&#xff0c;即发明问题解决理论&#xff0c;是一套系统的创新方法论&#xff…

程序验证之Dafny--证明霍尔逻辑的半自动化利器

一、What is Dafny?【来自官网介绍 Dafny 】 1)介绍 Dafny 是一种支持验证的编程语言&#xff0c;配备了一个静态程序验证器。 通过将复杂的自动推理与熟悉的编程习语和工具相结合&#xff0c;使开发者能够编写可证明正确的代码&#xff08;相对于 {P}&#xff33;{Q} 这种…

uniapp微信小程序通过萤石云接入海康摄像机

需求&#xff1a;在uniapp微信小程序上查看海康威视的摄像机监控视频和和操作摄像机拍摄方向 在萤石云接入海康摄像机设备&#xff0c;由于不同品牌设备在不同时间段接入方式可能不一致&#xff0c;具体接入方式查看官方文档或咨询官方客服。 海康摄像机官方客服热线&#xf…

web学习笔记(五十五)

目录 1. 配置代码片段的步骤 2. 条件判断 2.1 v-if、v-else、v-else-if 2.2 v-show 2.3 v-show和v-if的区别 1. 配置代码片段的步骤 在Visual Studio Code中我们可以将常用的代码配置成代码片段&#xff0c;这样就可以在页面上快速输入大段代码了。 &#xff08;1&#…

JavaScript-基本数据类型和变量

基本数据类型 JavaScript支持数字、字符串和布尔值3种基本数据类型 字符串型 字符串型是JavaScript用来表示文本的数据类型&#xff0c;字符串通常由单引号或双引号括起来&#xff0c;如果字符串存在特殊字符&#xff0c;可以用转义字符代替 数字型 数字型也是JavaScript中的基…

pytest教程-46-钩子函数-pytest_sessionstart

领取资料&#xff0c;咨询答疑&#xff0c;请➕wei: June__Go 上一小节我们学习了pytest_report_testitemFinished钩子函数的使用方法&#xff0c;本小节我们讲解一下pytest_sessionstart钩子函数的使用方法。 pytest_sessionstart 是 Pytest 提供的一个钩子函数&#xff0c…

U盘中毒文件变乱码?揭秘原因与高效恢复方法!

在日常使用U盘的过程中&#xff0c;有时我们会遭遇到一个非常棘手的问题——文件突然出现乱码。当你满怀期待地插入U盘&#xff0c;准备打开某个重要文件时&#xff0c;却发现文件名或内容变成了一堆无法识别的字符&#xff0c;这种心情无异于晴天霹雳。乱码文件不仅影响了我们…

数据结构------二叉树经典习题1

博主主页: 码农派大星. 关注博主带你了解更多数据结构知识 1判断相同的树 OJ链接 这道题相对简单,运用我们常规的递归写法就能轻松写出 所以我们解题思路应该这样想: 1.如果p为空&#xff0c;q为空&#xff0c;那么就是两颗空树肯定相等 2.如果一个树为空另一棵树不为空那么…