上下文包最佳实践
在Go中使用上下文包有几个最佳实践:
- 使用context.WithCancel,context.WithTimeout或context.WithDeadline创建带有超时或取消信号的上下文。
- 始终将上下文作为第一个参数传递给可能需要很长时间才能完成的函数,例如网络请求或数据库查询。
- 使用context.Value存储和检索与上下文关联的值,例如用户ID或请求ID。
- 使用context.WithValue基于现有上下文创建新上下文,并将其他值与其关联。
- 检查上下文的Done通道,查看它是否已被取消。
- 在整个应用程序中使用上下文包来传播请求范围的值和取消信号,而不是使用全局变量或手动信号。
- 避免使用context.Background(),因为它没有超时或取消信号,而是使用context.TODO() 表示上下文稍后将被调用方替换。
- 不要将上下文存储在结构中,而是将它们作为参数传递给函数。
- 始终检查上下文感知函数的错误返回值,以查看上下文是否被取消或超时。
Go中的上下文包用于跨API边界传递请求范围的值、取消信号和截止时间。它可用于存储元数据、取消信号、超时和其他请求范围的值。上下文包提供了一种取消长时间运行的操作以及跨API边界存储元数据的方法。它通常与http包一起使用,以管理HTTP请求的请求范围值和取消信号。
它允许您在多个函数调用和goroutine之间传播请求范围的值,从而更轻松地管理应用程序中的信息流。
context.Context是使用context.With*函数创建,例如context.WithValue,context.WithCancel 或 context.WithTimeout。这些函数返回新的上下文,携带指定值或信号的上下文值。
context.Context可以作为参数传递给需要访问请求范围值或侦听取消信号的函数和方法。然后,这些函数可以使用context.Value和context.Done访问上下文中存储的值和信号的done方法。
在需要取消长时间运行的操作或跨多个goroutine传播请求范围的值的情况下,上下文包特别有用。它通常用于服务器端编程和其他并发方案。
应始终将上下文作为第一个参数传递给执行可能被取消的工作的任何函数。
例如,HTTP服务器可以使用上下文在客户端断开连接时取消请求的工作,数据库包可以使用上下文来实现可取消的查询,等等。
context包定义了 Context 类型,该类型是一个Go接口,具有四个方法,分别名为Deadline()、Done()、Err() 和Value():
type Context interface {
Deadline() (deadline time.Time, ok bool)
Done()
Err()
Value(key)
}
上下文接口定义的方法
方法 | 描述 |
---|---|
Value(key) | 返回对应的值 |
Done() | 此方法返回可用于接收取消通知的频道 |
Deadline() | 此方法返回time.Time, 表示请求的截止日期,如果没有指定截止日期,则布尔值为false。 |
Err() | 此方法返回一个错误,指示完成通道接收信号的原因。上下文包定义了两个可用于比较错误的变量:Canceled表示请求已取消,DeadlineExeeded表示截止日期已过。 |
用于创建上下文值的上下文包函数
方法 | 描述 |
---|---|
Background() | 此方法返回默认上下文,从中派生其他上下文。 |
WithCancel(ctx) | 此方法返回一个上下文和一个取消函数。 |
WithDeadline(ctx, time) | 此方法返回一个带有截止日期的上下文,该截止日期用time.Time表示值。 |
WithTimeout(ctx, duration) | 此方法返回一个带有截止日期的上下文,该截止日期用time.Time表示值。 |
WithValue(ctx, key, val) | 此方法返回一个包含指定键值对的上下文。 |
context.WithCancel
下面是一个示例,说明context.WithCancel可以在Go中使用:
package main
import (
"context"
"fmt"
"time"
)
func doWork(ctx context.Context) {
for {
select {
case <-ctx.Done():
fmt.Println("Work done!")
return
default:
fmt.Println("Working...")
}
}
}
func main() {
ctx, cancel := context.WithCancel(context.Background())
defer cancel() // 推迟执行cancel
go doWork(ctx)
// Wait for a while before canceling the context
select {
case <-ctx.Done():
case <-time.After(time.Second * 3):
cancel()
}
}
在此示例中,我们使用context创建一个新的上下文 ctx.WithCancel(context.background())中。context.Background() 函数返回一个空的context。context.WithCancel 返回一个新的上下文和一个取消函数。我们推迟取消函数,以便在主函数退出时调用它。在 doWork 函数中,它将检查上下文是否已完成,如果是,则返回该函数。
在main函数中,我们正在运行一个goroutine,并通过传递上下文来完成其中的工作。等待3秒后,main函数会通过调用cancel函数来取消上下文,这将使上下文的Done通道关闭。因此,doWork函数将从Done通道接收,打印 "Work done!" 并返回。
context.WithTimeout
在Go中,您可以使用context.WithTimeout函数创建一个新上下文,该上下文在指定的超时时间过后被取消。该函数采用两个参数:现有上下文和超时持续时间。
下面是如何使用上下文的示例。WithTimeout创建在5秒后取消的上下文:
package main
import (
"context"
"fmt"
"time"
)
func main() {
ctx := context.Background()
ctx, cancel := context.WithTimeout(ctx, 5*time.Second)
defer cancel()
// Do some work
select {
case <-ctx.Done():
fmt.Println("Work completed")
case <-time.After(10 * time.Second):
fmt.Println("Work took longer than 10 seconds")
}
}
在此示例中,上下文是通过调用context.WithTimeout与后台上下文5秒的超时创建。该函数返回一个新上下文和一个用于取消上下文的函数。cancel函数在defer语句中调用,以确保在函数返回时取消上下文。select语句用于等待上下文完成或超时结束。
您还可以使用ctx检查上下文是否已完成。Done() 通道,您可以在select语句或循环中使用此通道来检查上下文是否完成,如果完成则意味着上下文已过期。
context.WithDeadline
在Go中,context.WithDeadline 函数创建具有关联截止时间的新上下文。截止日期是一个特定的时间点,在此时间点之后,上下文将被视为"死亡",任何相关工作都将被取消。该函数接受两个参数:现有上下文和截止时间。它返回一个新的上下文,该上下文将在指定的截止时间取消。
下面是一个示例:
package main
import (
"context"
"fmt"
"time"
)
func main() {
ctx := context.Background()
deadline := time.Now().Add(time.Second * 5)
ctx, cancel := context.WithDeadline(ctx, deadline)
defer cancel()
select {
case <-time.After(time.Second * 10):
fmt.Println("overslept")
case <-ctx.Done():
fmt.Println(ctx.Err())
}
}
在此示例中,将创建一个背景上下文,然后设置未来5秒的截止时间。WithDeadline 函数用于基于背景上下文创建具有指定截止时间的新上下文。select语句用于等待上下文被取消或等待10秒过去。如果在10秒之前取消上下文,它将打印错误消息上下文截止时间超过,否则将打印"overslept"
使用Context的SQL查询超时
要在Golang中使用超时的SQL查询,您可以使用上下文包来设置查询执行的截止时间。首先,使用上下文创建具有超时的context.WithTimeout函数。然后,将上下文作为第一个参数传递给查询执行函数(例如db.QueryContext() 或db.ExecContext())。
下面是如何为SELECT查询设置1秒超时的示例:
package main
import (
"context"
"database/sql"
"fmt"
"time"
)
func main() {
// Open a connection to the database
db, _ := sql.Open("driverName", "dataSourceName")
// Create a context with a timeout of 1 second
ctx, cancel := context.WithTimeout(context.Background(), time.Second)
defer cancel()
// Execute the query with the context
rows, err := db.QueryContext(ctx, "SELECT * FROM table")
if err != nil {
fmt.Println(err)
}
defer rows.Close()
// Handle the query results
// ...
}
使用上下文超时读取文件
在Go中,可以使用上下文包来设置读取文件的超时时间。下面是一个示例:
package main
import (
"context"
"fmt"
"io/ioutil"
"time"
)
func main() {
ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second)
defer cancel()
data, err := ioutil.ReadFile("example.txt", ctx)
if err != nil {
fmt.Println("Error:", err)
return
}
fmt.Println(string(data))
}
在此示例中,我们首先使用context创建一个超时为2秒的context.WithTimeout。然后,我们将此上下文传递给ioutil。ReadFile读取文件"example.txt"的内容。如果读取文件的时间超过2秒,则上下文的Done通道将关闭,并且ioutil.ReadFile返回错误。
将上下文用于HTTP
在Go中,您可以使用上下文包来设置HTTP请求的超时。下面是一个示例:
package main
import (
"context"
"fmt"
"io/ioutil"
"net/http"
"time"
)
func main() {
ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second)
defer cancel()
req, err := http.NewRequest("GET", "https://example.com", nil)
if err != nil {
fmt.Println("Error:", err)
return
}
req = req.WithContext(ctx)
client := &http.Client{}
resp, err := client.Do(req)
if err != nil {
fmt.Println("Error:", err)
return
}
defer resp.Body.Close()
body, err := ioutil.ReadAll(resp.Body)
if err != nil {
fmt.Println("Error:", err)
return
}
fmt.Println(string(body))
}
在此示例中,我们首先使用context创建一个超时为2秒的context.WithTimeout。然后,我们使用req.WithContext(ctx) 将此上下文附加到HTTP请求中。当我们使用http.Client.Do方法发出请求时,如果在收到响应之前关闭了上下文的Done通道,它将自动取消请求。
您还可以使用客户端库(如golang.org/x/net/context/ctxhttp)发出带有上下文的http请求,该库具有Get和Post方法,该方法将上下文作为第一个参数并返回响应和错误。
package main
import (
"context"
"fmt"
"io/ioutil"
"net/http"
"time"
"golang.org/x/net/context/ctxhttp"
)
func main() {
ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second)
defer cancel()
resp, err := ctxhttp.Get(ctx, nil, "https://example.com")
if err != nil {
fmt.Println("Error:", err)
return
}
defer resp.Body.Close()
body, err := ioutil.ReadAll(resp.Body)
if err != nil {
fmt.Println("Error:", err)
return
}
fmt.Println(string(body))
}
此示例使用ctxhttp.Get方法向"https://example.com"发出GET请求,超时为2秒。
请务必注意,在这两种情况下,如果上下文关闭了Done通道,则请求将被取消,但不会关闭连接。应用程序负责关闭连接。
使用Context作为键值存储
在Go中,您可以使用上下文包来存储可以与请求或一段代码一起传递的键值数据对。这允许您将其他信息与请求或代码段相关联,而不必将其作为显式参数传递。下面是一个示例:
package main
import (
"context"
"fmt"
)
func main() {
ctx := context.WithValue(context.Background(), "user_id", "12345")
// use the context in a function
processRequest(ctx)
}
func processRequest(ctx context.Context) {
userID := ctx.Value("user_id").(string)
fmt.Println("User ID:", userID)
}
在此示例中,我们首先使用context创建一个上下文。WithValue方法。我们传递上下文。Background() 作为父上下文,以及方法的键值对 "user_id" 和 "12345"。然后,我们将此上下文传递给processRequest函数。在函数中,我们使用Value方法从上下文中检索"user_id"值,并将其打印出来。
请务必注意,上下文值仅用于传输进程和API边界的请求范围数据,而不用于将可选参数传递给函数。如果需要将可选参数传递给函数,最好使用结构或函数选项模式。
此外,上下文值不是线程安全的,如果您处于并发环境中,请使用sync.Map或同等产品。
uni ext api,是一种需要下载uni_modules插件才能使用的、uni.开头的js API。
它是uni对象的方法,但不预置在uni-app的runtime中,需要单独下载对应的uni_modules。
示例:
uni.getBatteryInfo,这个API就是一个ext api,需要下载插件才能使用。详见
- 背景1,uni-app runtime越来越大
uni对象的api,如uni.navigateTo,之前都是内置在uni-app的runtime中的,跟随uni-app/HBuilder的升级而变化。
随着各家小程序和uni-app的演进,uni-app的api越来越多,而很多api又不常用,这会增加uni-app的runtime体积,尤其是在web和app平台。
比如很多小程序都内置有getBatteryInfo获取电量的API,在uni-app中理应拉齐各端实现,但在uni-app的web和app上内置它并不合适。
所以推出了uni ext api,这些API以uni_modules的方式出现,从插件市场单独下载。但导入到工程后,仍使用uni.getBatteryInfo方式来调用。
为此,在这些uni_modules的package.json中提供了一种特殊的注册方式,允许插件向uni对象自动挂载api。(只有DCloud官方插件,以uni-开头的插件,才能使用此机制)
- 背景2,uts将重构uni-app的内部实现
在过去,uni-app的web和小程序端,是github的独立项目,由DCloud的js团队维护。而uni-app的app端,是另2个独立项目,由DCloud的原生团队维护。 每次新加一个api,需要各个团队在不同的工程里开发,尤其是app端,需要先在原生项目里开发,然后在前端项目里封装。还需要再另一个语法提示工程中编写代码提示。
有了uts后,uni-app的实现机制将被重构,大幅的简化。
每个api,都使用uts这门统一的语言来开发,不再使用不同的技术语言。作为一个独立的uni_modules,独立的工程,有独立的版本。
仍然以uni.getBatteryInfo电量为例,开发这个api,不再需要在庞大复杂的uni-app的若干个项目里编码,也不需要再关心功能间关联和耦合。
只需要在uni-getBatteryInfo这个uni_modules下开发,目录结构如下。
这个目录清晰的列出了这个插件要做的所有事情:在不同的目录下编写uts或js代码、在d.ts里写语法提示。
├── uni_modules
│ ├── uni-getbatteryinfo
│ │ ├── changelog.md
│ │ ├── index.d.ts // 类型声明,需要同时扩展uni声明当前注册的API
│ │ ├── package.json
│ │ ├── readme.md
│ │ └── utssdk // 在不同目录实现平台能力
│ │ ├── app-android
│ │ │ └── index.uts
│ │ ├── app-ios
│ │ │ └── index.uts
│ │ ├── mp-weixin
│ │ │ └── index.js
│ │ └── web
│ │ └── index.js
复制代码
这种模式,还给开发者带来若干好处,比如开放性和灵活性。
- 以往,uni-app内置api如果有bug,普通开发者很难看懂源码,很难参与完善。
现在,在uts的支持下,普通前端也可以review这些api的实现,发现其中的问题和提出改进方案。
- 以往,这些uni api的bug被修复时,需要等待HBuilder发版,由于每次发版都需要发很多功能,可能bug1虽然已经修好,但bug2复测出问题,导致版本不能及时发布。
现在,ext api的uni_modules脱离HBuilder独立升级,快速解决开发者的问题。并且开发者可以自己修本地工程中ext api的bug。让问题得以更快速的解决。
- 背景3,内置api复写
很多uni的内置api,比如uni.showWaiting,实现的比较简单,在web端,常见的waiting都有更丰富的样式、使用更多的图片资源。
uni-app的runtime不适合内置很多waiting样式和资源,而使用三方插件,又需要按三方插件的写法调用,之前工程里写好的uni.showWaiting的代码不得不重改。
有了ext api,可以实现一个uni-showwaiting的uni_modules,在web端提供更丰富的效果,并且开源,可自由裁剪和增补。
导入这个uni_modules后,之前的uni.showWaiting的api的功能就被复写。
综上,背景1、2、3的问题,都将使用uni ext api来解决。uni-app很多新增的、不常用的api将采用ext api方式。
在uts的发展路线上,uni-app自身也将使用uts实现;使用uts将可以开发完整的uni-app。
目前所有的ext api,在未来的uts版的uni-app,其内置的uni对象的api中,均会得到复用。也就是说ext api将大幅推进下一代uni-app(纯uts版)的上线速度。
欢迎广大开发者参与到uni ext api的开源共建中来。
#注意事项
- 由于开发api是以uni.开头的,所以无法像普通插件那样由开发者自己开发、自己发布。
参与共建的开发者需要在DCloud官方插件的开源项目中提pr,由官方审核后再发布新版uni_modules。
也只有uni-开头的插件,才能在package.json中编写注册声明,挂载方法到uni对象上。
- uni ext api的版本将不再跟随HBuilder和uni-app cli的版本,它将是独立的版本。
只有uni的内置api才跟随HBuilder升级。请记得内置api和ext api的区别。
- ext api的实现不一定都是uts,但如果使用了uts,则将受uts自身的约束。如
- uts在iOS上真机运行必须打包自定义基座。后续官方会继续优化
- uts在Android上涉及arr、jar、so库等三方sdk的时候,也需打包自定义基座才能真机运行。
- wgt升级只对js、css生效,uts代码无法热更新。但这不意味着使用uts的项目就失去热更新能力,只是uts部分不能热更。js、vue、css、图片仍然可以打包成wgt热更。
- ext api在入口文件export的API,必须在package.json中编写注册声明
#如何开发uni ext api
#创建一个符合规则的uni_modules插件(通常是utssdk类型),如uni-getbatteryinfo
注意: 插件ID格式为:uni-API名称全小写
├── uni_modules
│ ├── uni-getbatteryinfo
│ │ ├── changelog.md
│ │ ├── index.d.ts // 类型声明,需要同时扩展uni声明当前注册的API
│ │ ├── package.json
│ │ ├── readme.md
│ │ └── utssdk // 在不同目录实现平台能力
│ │ ├── app-android
│ │ │ └── index.uts
│ │ ├── app-ios
│ │ │ └── index.uts
│ │ ├── mp-weixin
│ │ │ └── index.js
│ │ └── web
│ │ └── index.js
复制代码
#配置package.json
{
"uni_modules": {
"uni-ext-api": {
"uni": ""// string | string[] | Record<string,string>
}
}
}
复制代码
- 默认导出
{
"uni_modules": {
"uni-ext-api": {
"uni": "getBatteryInfo"
// 等同于
// import getBatteryInfo from "@/uni_modules/uni-getbatteryinfo";
// uni.getBatteryInfo = getBatteryInfo
}
}
}
复制代码
- 导出多个
{
"uni_modules": {
"uni-ext-api": {
"uni": ["getBatteryInfo", "isCharging"]
// 等同于
// import { getBatteryInfo, isCharging } from "@/uni_modules/uni-getbatteryinfo";
// uni.getBatteryInfo = getBatteryInfo
// uni.isCharging = isCharging
}
}
}
复制代码
- 导出别名
{
"uni_modules": {
"uni-ext-api": {
"uni": {
"onUserCaptureScreen": "onCaptureScreen",
"offUserCaptureScreen": "offCaptureScreen"
}
// 等同于
// import { onCaptureScreen, offCaptureScreen } from "@/uni_modules/uni-getbatteryinfo";
// uni.onUserCaptureScreen = onCaptureScreen
// uni.offUserCaptureScreen = offCaptureScreen
}
}
}
复制代码
- 指定支持的运行环境
{
"uni_modules": {
"uni-ext-api": {
"uni": {
"request": {
"name": "request", // 可选别名配置
"app": {// 表示在app平台,仅在iOS swift环境下生效
"js": false,
"kotlin": false,
"swift": true
}
}
}
}
}
}
复制代码
注意:
所有uni ext api均需要提供utssdk/interface.uts文件
所有对外暴露的方法,类型均需要在interface.uts中定义
在具体平台实现中,通过引用interface.uts中的定义的方法,类型来约束实现
声明对象字面量时,必须指定具体类型,如:const res:GetBatteryInfoSuccess = { level:10,.. }
命名规范:
- API入参类型命名为API名称首字母大写 + 'Options',
- 异步API success,fail回调结果类型命名为API名称首字母大写 + 'Success' 和API名称首字母大写 + 'Fail'
- 事件API回调结果类型命名为API名称首字母大写 + 'CallbackResult'
HBuilderX3.96及以下使用uni ext api后,云打包同时勾选android和iOS会导致打出来的包不能正确包含uni ext api插件,解决方案:升级至3.97+或android和iOS单独打包
#如何使用uni ext api
在插件市场查找uni ext api插件,导入HBuilderX中的项目即可直接使用。
如:uni-getbatteryinfo,导入后,即可直接使用uni.getBatteryInfo
RUN apt-get install -y zip libzip-dev && docker-php-ext-install zip
首先,安装libzip库,然后就是docker-php-ext-install来安装zip扩展即可。