阅读视图

发现新文章,点击刷新页面。
☑️ ☆

Golang i18n 之格式化千分位

背景

在做 i18n 和 l10n 的时候,不同国家的千分位表示是不同的,例如法国的千分位是空格。

实现

使用 text 包自带的方法可以解决。

package mainimport ("golang.org/x/text/language""golang.org/x/text/message")func main() {number := 12345678.9012345678pEn := message.NewPrinter(language.English)pEn.Printf("en: %.2f\n", number)pAr := message.NewPrinter(language.ModernStandardArabic)pAr.Printf("ar: %.2f\n", number)pFr := message.NewPrinter(language.French)pFr.Printf("fr: %.2f\n", number)}

Ref

☑️ ☆

Gin 路由命中问题

背景

在 prometheus 监控接口时,需要通过接口 uri 统计。在有 Path Variables(路径参数)的情况下,如果简单使用 c.Request.Method+c.Request.URL.Path 来统计,会导致一些使用了路径参数的接口没有被归为同一个。

解决方案

c.Request.Method+" "+c.FullPath()

Ref

Get matched route in context · Issue #748 · gin-gonic/gin · GitHub
go - Gin send metrics to promethesus where the URL has a parameter in the path - Stack Overflow

☑️ ☆

使用 pre-commit hook 提高本地开发效率

背景

有些编辑器检查不了的错误或者没有强提醒的错误(例如循环引包),在开发时不容易发现,代码提交后才发现编译不了。利用 git 的 hook 功能,在 commit 前就检查代码是否能编译通过,能够保证提交的代码一定能编译通过,也节省了来回 debug 的时间。

实现

在提交前实现一种 dry-run 的机制,确保代码能编译通过。如果有 Makefile,可以添加一个dry-build 的 target,例如:

.PHONY: dry-builddry-build:go build -o deploy/xxx cmd/xxx/main.go && rm deploy/xxx

然后编写一个 shell 脚本,命名为 pre-commit

#!/bin/bashmake dry-buildif [[ $? -ne 0 ]]; thenecho ">>>>>>>> 编译失败, 需要修复后再进行push"exit 1fiecho ">>>>>>>> 编译成功"exit 0

然后将该脚本移动到.git/hooks目录下。在编译失败时,会阻止 commit。

参考

Git - Git Hooks

☑️ ☆

ORM 框架中为什么需要默认启用事务

背景

发现很多 ORM 框架都有 auto-commit 的配置,在执行任意写操作时,实际上都会开一个事务,然后自动提交。针对 GORM 框架深究了一下。

框架的行为

根据文档 Transactions | GORM - The fantastic ORM library for Golang, aims to be developer friendly.,GORM 框架会对写操作(create/update/delete)默认开启事务来保证数据一致性。这个行为可以通过配置全局禁用或者在单个操作中手动禁用。

// Globally disabledb, err := gorm.Open(sqlite.Open("gorm.db"), &gorm.Config{  SkipDefaultTransaction: true,})// Continuous session modetx := db.Session(&Session{SkipDefaultTransaction: true})tx.First(&user, 1)tx.Find(&users)tx.Model(&user).Update("Age", 18)

why?

找到一个比较靠谱的说法:why GORM perform single create, update, delete operations in transactions by default ? · Issue #5969 · go-gorm/gorm · GitHub

ORM 框架通常提供了一些逻辑层面的 API,例如批量插入、更新等,用户层面可能只是一个 update 操作,但实际上执行了多条 sql,用户不一定意识到这个问题,因此默认开启事务是更优解。

☑️ ⭐

记一个无关痛痒的功能改进和对 Unmarshal 的一些了解

有如下代码:

type Config struct {   App Application `yaml:"application"`}type Application struct {   AppName    string `yaml:"app_name"`   ServerHost  string `yaml:"server_host"`   ServerPort   string `yaml:"server_port"`   RunMode     string `yaml:"run_mode"`}// 从指定目录取指定环境和类型的配置文件cfg := configlib.NewLocalFileWithOptions(   configlib.WithPath("configs"),   configlib.WithConfigName(base_flags.FlagCluster),   configlib.WithConfigType(configlib.ConfigTypeYaml),)config = &Config{}// 将配置加载进结构体if err := cfg.LoadToObject(config); err != nil {   panic(err)}log.Infof(context.Background(), "配置加载成功: %+v", config)

以及以下的配置:

application: app_name: common server_host: 127.0.0.1 server_port: "1234" run_mode: debug

在调试中,发现 config 没有值。一开始怀疑文件路径不对或者没加载到配置文件,尝试了一下,如果没读取到文件,会报文件不存在的错。事情没有这么简单,于是继续看这个 cfg.LoadToObject 函数的实现。

func (c *config) LoadToObjectNew(v interface{}) error {   err := c.Load()   if err != nil {      return err   }   err = c.adapter.GetData(v)   if err != nil {      return err   }   return nil}

其中 c.Load 包装了一下用 viper 读配置文件的方法,看了一下没什么大问题,没有报错,用 debugger 看也能看到 c 实际上是有值的,所以问题聚焦在 GetData 这个方法上。

func (a *fileProvider) GetData(obj interface{}) error {   by, err := json.Marshal(a.vp.AllSettings())   if err != nil {      return err   }   if err = json.Unmarshal(by, obj); err != nil {      return err   }   return nil}

好,到这里问题其实已经解决了 80%:这个方法从 a.vp (*viper.Viper) 中拿到一个map[string]interface{},然后把这个先是 json.Marshal ,再 json.Unmarshal 回最终需要的obj 上,因为一路上都用的 json,之前的配置结构体没有 json 的 tag,自然是没有值的。尝试了一下,给它加上 json 的 tag,马上就好了。

本来到这里其实就可以下班了,活了,能用。但是老觉得有些别扭,我明明是一个 yaml 的配置文件,凭啥我要多写一个 json 的 tag?而且,理论上 viper 支持以下类型的配置文件,有其他需求的时候怎么办?封装这个的作者也没有留下文档,记录这个一定需要 json tag 的坑,下次有人再使用肯定又会踩到,所以感觉还是需要优化一下。

"yaml", "yml", "json", "toml", "hcl", "tfvars", "ini", "properties", "props", "prop", "dotenv", "env"

从上面的代码可以看到,GetData 做的就是将读取到的配置加载进一个结构体。之前出的问题是,因为使用了 json 包,如果没有 json 的 tag,就读不到了。

我的思路是,应该会有一个通用的 Unmarshal ,可以根据来源的类型 Unmarshal 回对应的 tag 中。于是查了一下 viper 的文档,果然有这么一个方法 viper.Unmarshalviper package - github.com/spf13/viper - Go Packages

回到刚才有问题的代码,我尝试把这个 json marshal 完再 unmarshal 的方法替换成 viper.Unmarshal ,如下。

func (c *config) LoadToObjectNew(v interface{}) error {  return a.vp.Unmarshal(obj)}

再试了一下,还是不行。

这时候看到基础库里,有一个单元测试,于是进行了一些实验,发现如下现象。

  • 单测中删掉结构体上的所有 json tag,除了结构体成员名称与实际名称不同的都能读到值;
  • 在 yaml 的例子中,如果没有 json tag 所有值都读不到。
  • 在 yaml 的例子中,如果有部分字段有 json tag,有 tag 的字段能读到值。

感觉关键还是这个 tag。接着往下看,viper.Unmarshal 这个方法主要是包装了一下 mapstructure.Decode ,再往下,这个函数主要是用反射来做映射的,一切都跟预想中的差不多。回忆了一下之前看映射的时候的知识,映射是可以拿到 struct tag 的,猜想 unmarshal 应该会跟这个有关,查了一下,发现了这个 What are the use(s) for struct tags in Go? - Stack Overflow

于是继续去查 mapstructure 包的文档,直接搜 tag 。看到如下:

You can change the behavior of mapstructure by using struct tags. The default struct tag that mapstructure looks for is “mapstructure” but you can customize it using DecoderConfig.

再搜 DecoderConfig ,感觉离答案越来越近了。果然让我找到有一个 TagName 的选项,见 mapstructure package - github.com/mitchellh/mapstructure - Go Packages。如果在 viper.Unmarshal 的时候,让 mapstructure 的 decoder 根据输入文件的类型去找 tag,不就能解决了吗?于是根据文档,把这个 GetData 改成了如下模样。

func (a *fileProvider) GetData(obj interface{}) error {   unmarshalOptions := viper.DecoderConfigOption(func(decoderConfig *mapstructure.DecoderConfig) {      decoderConfig.TagName = a.opts.configType   })   return a.vp.Unmarshal(obj, unmarshalOptions)}

中间这里还遇到了一个坑,当时输入 decoderConfig. 的时候编辑器怎么都不提示有 TagName 这个字段,一看文档的版本是 v1.5.0,去 go.mod 里面看版本是 v1.4.1 。更新了一下 mapstructure 的版本就解决了。

再试,果然活了。

一些在踩坑过程中了解到的东西

一开始怀疑 jsoniterencoding/json 实现上有所不同,查阅文档发现它的 marshal 和 unmarshal 宣称跟原来的是 dropin replacement,姑且相信它。

查阅 json 的文档,json package - encoding/json - Go Packagesjson package - encoding/json - Go Packages,有以下几个点需要注意。

  • The encoding of each struct field can be customized by the format string stored under the “json” key in the struct field’s tag. The format string gives the name of the field, possibly followed by a comma-separated list of options. The name may be empty in order to specify options without overriding the default field name.
  • As a special case, if the field tag is “-”, the field is always omitted.
  • (对于最底层嵌套的结构体,以下规则成立)
    1. Of those fields, if any are JSON-tagged, only tagged fields are considered, even if there are multiple untagged fields that would otherwise conflict.
    1. If there is exactly one field (tagged or not according to the first rule), that is selected.
    1. Otherwise there are multiple fields, and all are ignored; no error occurs.
  • To unmarshal JSON into a struct, Unmarshal matches incoming object keys to the keys used by Marshal (either the struct field name or its tag), preferring an exact match but also accepting a case-insensitive match. By default, object keys which don’t have a corresponding struct field are ignored (see Decoder.DisallowUnknownFields for an alternative).

总结一下,在没有 tag 的时候,会按以下方式取值:

  • 精确匹配
  • 大小写不敏感匹配

如果没匹配上,不会报错(坑)。这个解释了单测实验中的现象。

以上这几段话还是有点不理解,以及 MarshalUnmarshal 有很详尽的规则,有时间可以细看一下。

yaml.Marshalyaml.Unmarshal 的行为又有所不同,它的匹配策略是默认按照字段小写匹配。详见yaml package - gopkg.in/yaml.v2 - Go Packagesyaml package - gopkg.in/yaml.v2 - Go Packages

☑️ ☆

猫鱼周刊 vol. 000 我有独特的创作技巧

其实想写 newsletter 很久了,每周都会阅读、收藏很多文章,但“收集->整理->输出“这个流程最多只走到了整理——把所有文章往 Cubox 里一扔,最多打个标签就草草结束了。从这期开始,我尝试每一周都消化一下上一周往收藏夹里扔的内容,分享一些有趣的文章、网站,发表一些自己的评论,也会分享一些这周遇到的优秀开源项目。我的周刊只会分享一些我在冲浪中遇到的有趣的内容,不会像一些其他周刊一样照搬 GitHub Trending 或者追时下一些热门的内容。

我觉得写作最大的难度是素材的收集,而 Cubox 就能很好地帮我完成这个工作。我创建了一个叫【最近一周】的智能文件夹,规则就是【7天内】收藏的链接。在平时冲浪时,遇到好的文章,只需要点一下浏览器右上角的收藏快捷方式即可。

文章

Golang int类型的最大最小值(原文

以前学 C++ 的时候,老师说过可以把一些值初始化成 INT_MAX 之类的值。在 Golang 中也有类似的用法,他在 math 包里面。值得注意的是并没有 MinUintX , 因为 unsigned 的最小值就是0。具体枚举可以看 math 包的文档

话说比起包文档,这篇文章这种带有 example 的方式更加简单易懂,更适合作为 cheatsheet 使用。

Golang 结构体对齐(原文

最近在使用 golangci-lint 来改善自己代码的质量,在屎山勘探的时候发现它给我标了一个这样的错:

warning: struct of size 40 could be 32 (maligned)

当时有点懵逼,这个报错让人完全摸不着头脑,为什么一个结构体可以变小?看了这篇文章马上想起了“结构体对齐”这个知识点。CPU 是按 字长 (word size)去访问内存的,在 64位上,字长为8字节。因此如果字段和其类型排布不合理,就会浪费大量字节在填充(padding)上。

不过这种极致优化能节省的内存感觉也不多,甚至比起代码的可读性来说可能是值得牺牲的。感觉现代可能已经很少去注重这些极致的优化。之前看过一个视频讲以前的游戏如何在内存极小的机器上运行起来,只能说现在计算机性能的提升让很多原来的奇技淫巧(例如算法和内存上的优化)变得不再重要。

Golang 中的 context(原文

之前一直以为这个 context 一个从入口一路带到最后一层调用的一个键值对。而事实上它跟并发控制有关,有超时、取消等功能。目前来说还是有点一知半解,但简单来说,context 只适合携带请求相关的参数,而不能作为传值的方式。另一篇文章 提到,“Inform, not control.”。

这个话题较为复杂,下期文章精读可能会介绍一下这方面的内容,敬请期待。

进程间通信(原文

和朋友讨论到各种语言的协程通信方式,发现其实 kotlin 和 golang的协程都有相似的地方,其实这相似性就来源于进程间通信。基础知识永不过期呀!

项目

direnv - 自动加载环境变量

一个在进入和退出目录时自动加载目录下 .envrc 中的环境变量的工具。配合其他 CLI 工具(例如 Make)可能会有奇效。

just - 提供一种保存和运行项目特有命令的便捷方式

简化版的 Make,支持任意语言编写配方。

Coolify - 自建 Heroku

可以自建的 all in one 解决方案,能够快捷创建应用、数据库等。有点像自建版的 leancloud

MessAuto - macOS 自动提取验证码并复制粘贴回车

MessAuto 是一款 macOS 平台 自动提取 短信验证码并 粘贴回车 的软件,百分百由Rust开发,适用于任何APP。

但是现在该 APP 没有签名,用起来略有顾虑,粘贴回车的方式感觉也略微不优雅,先观望观望。

这是猫鱼周刊的第1期,本系列每周日更新,主要内容为每周收集内容的分享,同时发布在
竹白:猫兄的和谐号列车
博客:阿猫的博客-猫鱼周刊

☑️ ⭐

日志最佳实践

文章链接:My Logging Best Practices

在动作后打日志

// don't do thatlog.info("Making request to REST API")restClient.makeRequest()// do thatrestClient.makeRequest()log.info("Made request to REST API")

第一个例子是一个反例,这种日志不知道下面的方法执行了没有。第二个例子在方法出错时会抛出异常,同时日志也不会打印。

分离参数和日志信息

// don't do thatrestClient.makeRequest()log.info("Made request to {} on REST API.", url)// do thatrestClient.makeRequest()log.info("Made request to REST API. [url={}]", url)

日志中有两种信息,一种是人工写的帮助判断状态的信息,另一种是实际操作中的参数。第一个例子中,参数过长会让日志信息可读性降低,同时添加参数需要重写句子。第二种方式能使得日志结构清晰更好解析,可读性高,同时也更好扩展参数列表。

区别 WARNING 和 ERROR

简单来说,两者的区别为:

  • WARNING:做了某些操作,但是有问题
  • ERROR:没有进行操作

需要注意的是,WARNING 和 ERROR 在无需处理的时候,不应该记录。

业务使用 INFO,技术使用 DEBUG

DEBUG | Saved user to newsletter list. [user="Thomas", email="thomas@tuhrig.de"]DEBUG | Send welcome mail. [user="Thomas", email="thomas@tuhrig.de"]INFO  | User registered for newsletter. [user="Thomas", email="thomas@tuhrig.de"]DEBUG | Started cron job to send newsletter of the day. [subscribers=24332]INFO  | Newsletter send to user. [user="Thomas"]INFO  | User unsubscribed from newsletter. [user="Thomas", email="thomas@tuhrig.de"]

简单来说,INFO 记录做了什么,但不关心具体怎么做的;技术有关的内容使用 DEBUG 记录。

这是阿猫精读技术文章的第1篇,本系列会不定期更新,主要内容为技术文章精读(翻译),同时发布在
竹白:猫兄的和谐号列车
博客:阿猫的博客-技术文章精读

☑️ ☆

Android 项目使用 Github Actions 实现自动打包发布

背景

朋友的安卓项目[1]想要实现更新代码(发布版本)后自动打包成apk并发布。同时,他有这么一个需求:由于他的app需要支持安装和升级(安装时,app需要签名;同时,如果需要升级,必须用同一个key签名,才能保留上一版本的数据),因此需要增加签名这一步骤。

本文假定你已经掌握Android app的开发流程,熟悉Gradle,熟悉安卓应用的打包发布方法,本文重点介绍利用GitHub Actions 进行Android 项目CI/CD 的过程。

基础知识和思路

如果不使用CI/CD ,你大概需要使用 Android Studio 的 Generate Signed Bundle/APK 功能[2],关于应用打包发布的具体过程,这篇官方的文章讲得已经非常详细,就不再叙述了。后续的操作假定你已经熟悉使用这个功能进行打包,同时已经生成了一个供签名的key。

同时,文章也假定你已经熟悉Gradle[3],这是一个构建工具,能够自动化构建流程。在打包发布(Release)版本的的apk时,只需要使用命令gradle assembleRelease 即可构建供发布的apk。

最后,我们需要给apk签名。你可以使用apksigner等,在此不多赘述。

因此,如果要使用一个自动化的流程替代我们手工的发布,需要完成以下几个步骤:

  1. 将新版本的代码构建成apk
  2. 将上一步生成的apk用我们的key签名
  3. 以某种方式将这个apk发布出去,供用户下载

经过一些资料的查,其实Github 的 Marketplace就提供了很多已经封装好的workflow,例如

  1. https://github.com/marketplace/actions/gradle-build-action 解决构建apk的问题
  2. https://github.com/marketplace/actions/sign-android-release 解决apk签名的问题
  3. https://github.com/marketplace/actions/create-release 解决发布的问题

所以我们要做的事情就是将这几个积木串起来就大功告成了。另外,我希望这个过程能够跟git 工作流程结合起来,我们并不需要每次push代码都构建发布一个版本,使用tag来进行版本的管理是比较理想的:每当一个tag被push到Github,就触发一次构建,发布一个版本。

走,实操走起

Talk is cheap, show me the code. 这里先把workflow的yaml贴出来,再慢慢解释。

name: Releaseon:  push:    tags:      - "v*"jobs:  build:    runs-on: ubuntu-latest    permissions:      contents: write    steps:      - uses: actions/checkout@v3      - uses: actions/setup-java@v3        with:          distribution: temurin          java-version: 11      - uses: gradle/gradle-build-action@v2        with:          gradle-version: current          arguments: assembleRelease      - uses: r0adkll/sign-android-release@v1        id: sign_app        with:          releaseDirectory: app/build/outputs/apk/release          signingKeyBase64: ${{ secrets.SIGNING_KEY }}          alias: ${{ secrets.ALIAS }}          keyStorePassword: ${{ secrets.KEY_STORE_PASSWORD }}          keyPassword: ${{ secrets.KEY_PASSWORD }}      - run: mv ${{steps.sign_app.outputs.signedReleaseFile}} LittleGooseOffice_$GITHUB_REF_NAME.apk      - uses: ncipollo/release-action@v1        with:          artifacts: "*.apk"          token: ${{ github.token }}          generateReleaseNotes: true

一个GitHub Action的yaml包括几个必须的元素:name、on和jobs。name就是这个action的名称,on就是这个action 的触发方式,jobs就是这个action要干的事情。

从这个on开始解释,前面说到,我们希望“每当一个tag被push到Github,就触发一次构建,发布一个版本”。我们的tag应当使用semantic versioning[4],例如v1.0.1-beta。因此,on这部分我们这样写:

on:  push:    tags:      - "v*"

表示在有tag被push,且tag是以v开头时,运行这个action。

接下来到jobs,这里定义这个action要干的事情。我们要做的事情比较简单,就只用一个叫build的job就好啦。如果定义多个job,可以享受到一些例如复用job、job之间的依赖之类的功能[[https://docs.github.com/en/actions/using-jobs/using-jobs-in-a-workflow](https://docs.github.com/en/actions/using-jobs/using-jobs-in-a-workflow)],在这里就不多赘述啦。再往里看,首先是一个`runs-on`标签,表示这个job要在什么环境下运行。一般情况下我们使用`ubuntu-latest`就可以啦,有特殊需求的可以使用其他的系统[https://docs.github.com/en/actions/using-jobs/choosing-the-runner-for-a-job]。然后是一个permission标签,这里不多叙述,感兴趣可以查阅文档[5],后面的steps才是重点。

可以看到,steps下面是多个单独的项,每个项里面又有useswith 等标签。steps之间是串行运行的,意味着前一个step运行成功后才会进入下一个step,如果某一个step失败了,整个过程就会失败。uses表示使用某个模板,with则是传入模版的一些参数,模板的使用可以在marketplace的文档中找到。接下来我们逐个step往下看。

检出代码

- uses: actions/checkout@v3

这一步是基本上所有CI/CD流程必须的,把对应的代码从git仓库中检出,放到工作目录中。

Gradle打包

- uses: actions/setup-java@v3  with:    distribution: temurin    java-version: 11- uses: gradle/gradle-build-action@v2  with:    gradle-version: current    arguments: assembleRelease

这里实际上有两步,第一步先配置好java环境,这一步就不多说了;第二步就是前面提到的将新版本的代码构建成apk。这里传入了两个参数(with),gradle-version表示要使用的gradle版本,这里选择current,就是当前最新的稳定版本。arguments表示gradle命令的参数,我们这里要Release,所以选assembleRelease。根据经验,这个未签名的apk会生成在app/build/outputs/apk/release,这里暂时用不到,但是我们先记下来,下一步有用。

给apk签名

- uses: r0adkll/sign-android-release@v1  id: sign_app  with:    releaseDirectory: app/build/outputs/apk/release    signingKeyBase64: ${{ secrets.SIGNING_KEY }}    alias: ${{ secrets.ALIAS }}    keyStorePassword: ${{ secrets.KEY_STORE_PASSWORD }}    keyPassword: ${{ secrets.KEY_PASSWORD }}

这一步就是要给apk签名了。这里有一个陌生的id标签,主要用来标识这一步,后续的step可以使用这一步产生的一些变量。

这一步的with中也有一些新东西,就是以 ${{ xxx }}标识的变量。这里都是secrets[6],后续我们还会遇到一些环境变量和GitHub定义的变量。secrets的设置在仓库的Settings - Security - Secrets - Actions找到,例如${{ secrets.SIGNING_KEY }}我们需要对应创建一个SIGING_KEY的secret。

这一步的alias等就不赘述了,就是前面提到的签名的时候使用的几个参数。重点讲一下signingKeyBase64,前面提到,签名需要用到这个keystore,但是这属于不能公开的内容(否则谁都可以用你的key来给应用签名),因此这个key不能存在公开的仓库中。这个workflow的作者使用base64对key进行编码(base64[7]是一种可以将任意二进制数据转化成文字的编码方法),通过secrets传进workflow中,避免key的泄露。这个base64的生成方法如下:

> openssl base64 < some_signing_key.jks | tr -d '\n' | tee some_signing_key.jks.base64.txt

发布

- run: mv ${{steps.sign_app.outputs.signedReleaseFile}} LittleGooseOffice_$GITHUB_REF_NAME.apk- uses: ncipollo/release-action@v1  with:    artifacts: "*.apk"    token: ${{ github.token }}    generateReleaseNotes: true

这里还是走了两步,第一步是将上一步生成的文件改一个名,第二部才是创建一个Release

第一步的run是一个新鲜东西,表示直接在工作区运行一个命令,这里使用一个mv命令来给生成的apk改个名,顺便移到最外面去。还记得上面提到的step的id吗,这里就用到了。${{steps.sign_app.outputs.signedReleaseFile}}表示sign_app这一步的outputs.signedReleaseFile这个变量,是生成的签名apk的目录。这里还有一个新玩意,就是$GITHUB_REF_NAME。这个是Github定义的变量,其值为“触发workflow的分支或tag名称”[8]。我们希望生成的apk文件命名为LittleGooseOffice_v1.0.2.apk,所以做这么一个改名的操作。

第二步也比较简单,创建一个Release,这里artifact表示要包含到Release Assets中的文件,*.apk就能搞定;token这里填${{ github.token }},这也是一个内置的变量,会使用一个临时的token来创建Release;最后的generateReleaseNotes能够自动生成一些变化列表之类的内容。

到这里,这个action就已经大功告成了。接下来我们体验一下如何用gitflow实现自动构建发布。

扬帆起航!

在每次完成一个版本的开发后,我们执行以下操作:

> git tag v1.0.0> git push --tags

可以看到在Actions页签出现了一个workflow run,如图。(这里由于workflow已经完成,所以是绿色,如果是刚提交的,应该是黄色。)

等待workflow完成后,前往Release,应该就能看到如下。

这时候,用户点击Assets中的apk就可以下载到我们最新的包啦!


  1. https://github.com/MReP1/LittleGooseOffice ↩︎

  2. https://developer.android.com/studio/publish/app-signing ↩︎

  3. https://gradle.org/ ↩︎

  4. https://semver.org/lang/zh-CN/ ↩︎

  5. https://docs.github.com/en/actions/using-jobs/assigning-permissions-to-jobs ↩︎

  6. https://docs.github.com/en/actions/security-guides/encrypted-secrets ↩︎

  7. https://en.wikipedia.org/wiki/Base64 ↩︎

  8. https://docs.github.com/en/actions/learn-github-actions/environment-variables#default-environment-variables ↩︎

☑️ ☆

Postman 使用Pre-request 脚本及引入外部依赖

背景

在使用 Postman 模拟请求一些接口时,如果遇到需要签名、加密等情况,需要对请求的字段动态做一些处理,每次去计算签名就比较麻烦。好在 Postman 提供了 Pre-request 脚本[1]这一功能,支持使用 JavaScript 写一些脚本来计算请求参数,做自动化接口测试等。

基础知识

其实 Postman 的脚本能实现的功能就是可以动态地生成请求的参数,这里需要用的其实就是get/set请求的参数。

获取请求体

pm.request.body

提供了一个 toJSON() 方法可以把请求体变成JSON结构体。

设置环境变量

pm.environment.set("signature", sign)

将生成的签名设置到环境变量中,后续用于请求。

使用 Postman 提供的变量

Postman 提供了一系列动态变量[2],例如时间戳、随机整数等。

var timestamp = pm.variables.replaceIn('{{$timestamp}}')

使用外部依赖

这是本篇文章的重点之一。有的接口官方提供了代码样例,如

var genSignature=function(secretKey,paramsJson){    var sorter=function(paramsJson){        var sortedJson={};        var sortedKeys=Object.keys(paramsJson).sort();        for(var i=0;i<sortedKeys.length;i++){            sortedJson[sortedKeys[i]] = paramsJson[sortedKeys[i]]        }        return sortedJson;    }    var sortedParam=sorter(paramsJson);    var needSignatureStr="";    for(var key in sortedParam){        var value=sortedParam[key];        needSignatureStr=needSignatureStr+key+value;    }    needSignatureStr+=secretKey;    var md5er = crypto.createHash('md5');//MD5加密工具    md5er.update(needSignatureStr,"UTF-8");    return md5er.digest('hex');};

可以看到,这里使用了一个crypto 的包。我对JS并不熟悉,虽然 Postman 中自带有一个 crypto-js,但我不想花时间研究这两个包的差异,然后再把这个代码替换成等效的用法,因此需要有一种机制去像正常引包一样,把这个包require 进来。

经过一番查找,我找到了这个repo[3]。其使用方法较为简单,clone 项目到本地然后按照repo里的提示运行即可。


  1. https://learning.postman.com/docs/writing-scripts/pre-request-scripts/ ↩︎

  2. https://learning.postman.com/docs/writing-scripts/script-references/variables-list/ ↩︎

  3. https://github.com/matt-ball/postman-external-require ↩︎

☑️ ☆

关于 Golang 多平台打包发布这件事..

build

一个演示如何使用 GitHub Actions 将一个 Golang 项目打包成多平台的二进制文件并发布到 GitHub Releases 和 DockerHub 的例子。

由来

作为软件开发者,在软件发布上浪费大量重复劳动是极其没有必要的,这应该是一个高度自动化的过程。在发布软件的过程中,有以下几个痛点:

  • 构建多个系统和架构的二进制文件
  • 跨平台编译时,可能需要搭建适当的编译环境
  • 发布过程繁琐

当然了,这些痛点或多或少已经被解决

  • Golang 本身就支持跨平台的编译
  • 使用 Docker 或虚拟机等
  • 编写发布脚本等

然而,这样还不够"自动化",如果使用 GitHub Actions 来自动化发布过程,就能更优雅地解决这些问题,使软件开发者更加专注于软件的开发上。

前提

本文假设你已经熟悉 Golang、git 和 Docker, 并对 GitHub Actions 有一定了解。

让我们开始吧

这篇文章共有两个目标,分别是

  • 将一个 Golang 项目打包成多平台的二进制文件并发布到 GitHub Releases
  • 将一个 Golang 项目打包成多平台的二进制文件并发布到 DockerHub

编写一个简单的 Golang 程序

我们只是为了测试在不同系统和架构上二进制文件的执行,所以一个非常简单的 Golang 程序即可。我们这里就用一个最简单的 Hello World 吧。

package mainimport "fmt"func main() {fmt.Println("Hello, World!")}

在终端中运行一下,结果如下。

> go run main.goHello, World!

看起来不错,接下来让我们把这个 Golang 程序打包成一个二进制可执行文件。

> go build -o hello main.go

这个命令什么输出都没有,说明运行成功了,没有错误。对于命令行来说,没有消息就是好消息。

在当前目录下,可以看到一个名为 hello 的可执行文件(在 Windows 中,该文件可能名为 hello.exe)。我们运行一下。

> ./helloHello, World!

不错,跟我们之前使用 go run main.go 的结果是一样的。

快速回顾:

使用 go build 命令可以将一个 Golang 程序打包成二进制可执行文件。

编译跨平台二进制可执行文件

还记得前面提到,在 Windows 平台上,go build 命令会生成一个以 .exe 结尾的可执行文件,这就不得不提 Golang 的跨平台编译能力。Golang 能轻松地生成不同平台上的二进制可执行文件,不需要开发者了解任何跨平台编译方面的细节。

假设我们正在使用 macOS 进行开发,如果我们需要编译一个可以在 Windows 平台上运行的二进制可执行文件,我们只需要运行

> CGO_ENABLED=0 GOOS=windows GOARCH=amd64 go build -o hello_windows_amd64.exe main.go

目录下就会生成一个 hello_windows_amd64.exe 文件,复制到 Windows 下运行即可。

编译多平台二进制可执行文件并打包发布到 GitHub Releases

在开始之前,你可以看一下本项目的 Releases

可以看到,对于每个目标平台,都有一个对应的 tar.gzzip 压缩包,及其对应的 md5 校验码。压缩包中含有二进制可执行文件以及 LISENCEREADME.md 文件。

这一步的实现非常简单,已经有现成的 Actions 替我们搞定。见 wangyoucao577/go-release-action

它的用法非常简单,如下。

name: buildon:  release:    types: [created] # 表示在创建新的 Release 时触发jobs:  build-go-binary:    runs-on: ubuntu-latest    strategy:      matrix:        goos: [linux, windows, darwin] # 需要打包的系统        goarch: [amd64, arm64] # 需要打包的架构        exclude: # 排除某些平台和架构          - goarch: arm64            goos: windows    steps:      - uses: actions/checkout@v3      - uses: wangyoucao577/go-release-action@v1.30        with:          github_token: ${{ secrets.GITHUB_TOKEN }} # 一个默认的变量,用来实现往 Release 中添加文件          goos: ${{ matrix.goos }}          goarch: ${{ matrix.goarch }}          goversion: 1.18 # 可以指定编译使用的 Golang 版本          binary_name: "hello" # 可以指定二进制文件的名称          extra_files: LICENSE README.md # 需要包含的额外文件

当你完成一个版本代码的编写,准备要发布时,只需要在 git 中对该提交打上版本号,如 v0.0.2,然后提交至 GitHub。在 Releases 页面,点击 Draft a new release,选择刚才的标签,点击最底下的 Publish release 按钮即可。

然后我们可以进入 Actions 页面,应该可以看见已经有 workflow 正在运行。完成后,再回到 Releases 页面,就可以看到打包出的文件。

快速回顾:

使用 GitHub Actions 可以自动化编译、打包并将二进制可执行文件发布到 Release。

编译多平台二进制可执行文件并打包发布到 DockerHub

前面提到过,运行一个 Golang 程序最简单的方法就是将其编译成二进制可执行文件后,直接运行。这一步甚至不需要考虑实际运行的机器上是否有 Go 环境。

因此,如果要把 Golang 程序打包进一个 Docker 镜像,只需要一个最小的 Linux 系统,把这个二进制文件打包进去即可。

知道这个以后,我们很顺其自然就可以编写出以下 Dockerfile。

FROM alpine:3.15.5COPY hello /usr/local/bin/helloRUN chmod +x /usr/local/bin/hello

简单验证一下我们的想法。(注意:以下代码仅能在 Linux 下运行,在 macOS 和 Windows 上会因为编译出的二进制文件与 Docker 的运行环境不匹配而不能运行。)

> docker build -t hello ....(输出大量的日志)> docker run -it --rm hello> helloHello, world!

成功了!(或者失败了,因为你没看我前面的注意)

不管到这一步你是成功还是失败,你应该已经想到,只需要编译出不同平台上的二进制可执行文件,再构建对应平台的 Docker 镜像就好了。

Makefile 魔法

前面讲过,我们使用带参数的 go build 命令就可以生成在不同平台下的二进制可执行文件。但是我们可能有多个平台和架构,这时候 Makefile 就派上用场了。

我们编写一个简单的 Makefile ,里面指定了编译不同平台和架构使用的命令。考虑到我们的 Docker 镜像只需要 linux/amd64linux/arm64 两个平台,所以我们只需要编写下面的两行命令。
当然,如果你需要一个可以本地使用,能直接编译所有平台的二进制可执行文件的 makefile ,可以参考仓库中的 Makefile 文件。

all: build-linux-amd64 build-linux-arm64build-linux-amd64:mkdir -p buildCGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -o build/hello_linux_amd64 main.gobuild-linux-arm64:mkdir -p buildCGO_ENABLED=0 GOOS=linux GOARCH=arm64 go build -o build/hello_linux_arm64 main.go

需要注意的是,第一条规则 all 为默认的规则,执行 make 时就执行它指定的目标。mkdir -p build 保证了输出目录一定存在。

如果你使用 macOS 或者 Linux 系统,可以试着运行 make 命令,它会在当前目录下创建一个 build 文件夹,并将生成的二进制可执行文件都放在里面。

Dockerfile 中的环境变量

前面提到过,如果你在前面的小试验中失败了,是因为 Docker 镜像中打包进了错误的二进制可执行文件。因此我们需要控制打包进镜像的二进制可执行文件的版本。

在上一步的 Makefile 中,可能你已经发现,我使用了后缀来区分不同平台和架构的二进制可执行文件,例如 hello_linux_amd64
在这一步中,我们需要让 Dockerfile 通过打包镜像的平台和架构,自动选择合适的二进制可执行文件。

我们修改一下之前的 Dockerfile, 使他变成以下的样子。

FROM alpine:3.15.5ARG TARGETOSARG TARGETARCHCOPY build/hello_${TARGETOS}_${TARGETARCH} /usr/local/bin/helloRUN chmod +x /usr/local/bin/hello

TARGETOSTARGETARCH 是自带的自动变量,但你需要使用 ARG 命令来说明你需要这两个变量。
COPY 命令中,可以看到我们把对应平台和架构的二进制可执行文件拷贝到了 /usr/local/bin 目录下并给了它可执行权限,因此用户直接输入 hello 就能运行我们的程序。

如果你在上一次小试验没有成功,现在可以重新试一下了。

编写 GitHub Actions

至此,所有的障碍我们都已经解决了,剩下就是编写一个可以自动完成上面流程的 GitHub Actions。

我们不需要新建一个 Action, 只需要在之前的 Action 上新增一个作业(job)即可。

build-docker-image:    runs-on: ubuntu-latest    steps:      - uses: actions/checkout@v3      - uses: docker/metadata-action@v4        id: meta        with:          images: leslieleung/hello      - uses: actions/setup-go@v3        with:          go-version: 1.18      - uses: docker/setup-qemu-action@v2      - uses: docker/setup-buildx-action@v2      - uses: docker/login-action@v2        with:          username: ${{ secrets.DOCKERHUB_USERNAME }} # 记得在 secrets 中添加响应的 secret          password: ${{ secrets.DOCKERHUB_PASSWORD }}      - run: make      - uses: docker/build-push-action@v3        with:          context: .          platforms: linux/arm64,linux/amd64 # 需要的平台          push: true          tags: ${{ steps.meta.outputs.tags }}

像前面一样发布一个新的版本,然后在 Dockerhub 上就能看到刚才构建的镜像了。可以看到有 linux/amd64linux/arm64 两个平台。

大功告成!

番外

经过 v2ex 论坛上的朋友提醒(见原帖 关于 Golang 多平台打包发布这件事… ),另外还有两种方法供参考。

  • GoReleaser:能够提供跨平台编译及打包 Docker 镜像、发布等,非常强大的工具。有免费和付费的 Pro 版本。
  • gox:能够并行编译。

由于 gox 具有并行编译的特性,这里增加一下关于 gox 的介绍。

使用 gox 加速发布过程

通过查看 gox 的文档可以发现, gox 的命令非常简单。我们往前面的 Makefile 中添加以下几行。

gox-linux:gox -osarch="linux/amd64 linux/arm64" -output="build/hello_{{.OS}}_{{.Arch}}"gox-all:gox -osarch="darwin/amd64 darwin/arm64 linux/amd64 linux/arm64 windows/amd64" -output="build/hello_{{.OS}}_{{.Arch}}"

此时,运行 make gox-linuxmake gox-all 就能完成对应平台的编译了。

同时修改一下 build.yml

build-docker-image:    runs-on: ubuntu-latest    steps:      - uses: actions/checkout@v3      - uses: docker/metadata-action@v4        id: meta        with:          images: leslieleung/hello      - uses: actions/setup-go@v3        with:          go-version: 1.18      - uses: docker/setup-qemu-action@v2      - uses: docker/setup-buildx-action@v2      - uses: docker/login-action@v2        with:          username: ${{ secrets.DOCKERHUB_USERNAME }}          password: ${{ secrets.DOCKERHUB_PASSWORD }}      - run: go install github.com/mitchellh/gox@latest # 安装 gox      - run: make gox-linux      - uses: docker/build-push-action@v3        with:          context: .          platforms: linux/arm64,linux/amd64          push: true          tags: ${{ steps.meta.outputs.tags }}

常见问题

权限不足,发布到 Release 失败

原因见 链接

解决方法:在 build.yml 中添加以下内容。

name: buildon:  release:    types: [created]    permissions: # 添加  contents: write # 添加jobs:  build-go-binary:    runs-on: ubuntu-latest...

参考

GitHub Action - Build and push Docker images

GNU make

Dockerfile reference

wangyoucao577/go-release-action

☑️ ☆

【收藏夹】Git

教程

廖雪峰的 Git 教程 :Git 入门经典教程,必读。

菜鸟教程 - Git教程 :可以当 cheatsheet,速查用。

GitHub - 30 天精通 Git 版本控管 :一个比较贴近现实使用的 Git 教程,写作语言为 zh-tw。

猴子都能懂的 GIT入门:暂时还没看过。

Learn Git Branching:一个在线交互式学习git的分支。

规范

Git 提交的正确姿势:Commit message 编写指南

阮一峰的网络日志 - Commit message 和 Change log 编写指南 :commit message 规范,作者为阮一峰。

gpg签名相关

Git commit 中使用 gpg 签名提交

介绍了在macOS上使用gpg签名,以及如何设置某个repo需要使用gpg签名。

☑️ ☆

从超星事件看国内高校信息安全

超星事件时间线梳理

  • 2022.06.20下午,部分网络安全社区传出超星学习通数据库遭入侵,在非法渠道兜售的信息;
  • 2022.06.20晚,多人在社工平台查询到超星相关信息,并通过查询其本人的信息证实了流传的超星数据库为真;
  • 2022.06.21早上,一份内容为受波及学校及组织的名单在网上流传,涉及到幼儿园至中学、各高校;
  • 2022.06.21下午,超星官微发表声明称“还未发现明确的用户信息泄露证据”、“网上传言密码泄露是不实的”;

潜伏在高校中的“超星”们

近几年,“信息化”、“无纸化”的推广,以及由于疫情的影响,学校越来越多地依靠一些在线的服务来进行远程授课、出勤管理、信息收集等,回想过去几年,在学校使用过的各种平台数不胜数,除了超星,还有PTA、学者网、至善网、雨课堂、奕报告、今日xx、对分易、我在校园等等,这些平台有几个非常显著的共性特征:

  • 学校或学院或某门课程钦定使用这个平台
  • 为了与考勤、成绩考核等挂钩,学校向这些平台提供学生姓名、学号、学院专业等信息,或由学生自行在平台填写这些信息
  • 学校主动向这些平台提供一些与学生有关的信息,如成绩、校卡余额、消费记录等
  • 平台过度收集过多的隐私信息,如家庭住址、家庭联系方式、误差为10米的经纬度等
  • 所有这些数据存储在第三方平台上,学校与学生本身并不能掌控这些数据

再看看本次超星学习通的脱裤(拖库,即指网站遭到入侵后,黑客窃取其数据库文件),被泄露的正是上述学校或学生本身向这些平台提供的学校/组织名称、姓名学号学院专业等信息,以及对应的密码(据官方说并非明文)。可以说,通过学校近乎强制性地推广,学生几乎没有选择余地地向这些平台提供了大量的隐私信息,同时学校本身对提供出去的信息也没有经过严格的考虑,最后也是最严重地,所有这些数据以不一定达到业界安全标准的方式存储在并不可信的第三方,也没有提供注销账号等功能使得用户可以主动删除自己的数据。

这些平台只是冰山一角,在高校日常管理中,还有非常多有可能导致隐私泄露的高危行为,包括但不限于:

  • 使用公开平台(如问卷星、腾讯文档等)收集学生信息,包括专业班级学号、联系方式、家庭信息、就业信息等
  • 在公开渠道(如微信群聊等)不加节制地明文传输、公开含有隐私信息的表格等
  • 将含有隐私信息的文件不加节制地散布到不应有权掌握这些文件的人手中(如学生助理等),对数据的存储没有明确的规章制度(如明确仅能用于什么用途、用后应删除、不能扩散等规定)
  • 一些校内学生自行开发的平台,未使用加密或使用过期的加密方式存储数据,数据管理不规范等

国内一直有一种论调,认为“用户愿意用隐私换取便利”,在国内互联网环境中,信息安全意识普遍不足,从厂商到组织使用者再到用户本身都没有意识到隐私和信息安全的重要性。在高校中,即使在计算机相关专业,一些安全的做法也会被认为是“复杂”、“多此一举”,隐私和信息安全的推广还有很长的路要走。

微薄之力

这一节的标题之所以叫微薄之力,是因为接下来我提的建议若要实际推行将会遇到客观和主观的各种压力,但关于隐私和信息安全我已经压抑已久,由于已经毕业,我也尝试在年级大群中发声,尝试改变这一现状,也取得了一定的成效。

从学校的角度,作为可能泄露隐私信息的活动的“发起者”,有以下这些努力可以尝试:

  • 完善信息化建设,私有化部署远程授课、出勤管理、信息收集等系统,确保隐私数据掌握在学校本身的服务器而非第三方,同时
    • 对上述系统的访问作一定限制,如限制校园网访问(校外访问可提供隧道等方式)
    • 对上述系统作充分安全评估,安排专人运维,及时修补漏洞
    • 对上述系统存储的数据定时备份,并订立完善的数据管理机制,限制数据的获取
  • 建设学生信息管理系统,对诸如身份证号、联系方式、家庭住址等隐私信息进行统一存储和管理,在学校各系统间互联互通,避免在管理过程中需要学生重复填报相关信息;同时设立权限机制,限制对这些信息的获取(甚至是辅导员也无法直接查看某个学生隐私信息,有需要时通过申请向上级获得仅申请的这个人的查看权限)。
  • 不使用任何第三方系统收集学生任何信息,不在任何第三方平台公开学生的任何信息

从学生的角度,作为隐私泄露的可能受害者,有这些努力可以尝试:

  • 完善自己的安全意识和知识,在不同系统上使用不同的强密码,仅提供必要的信息,使用安全的网络访问等
  • 认识到什么数据属于个人隐私,坚决不能主动提供或被他人公开
  • 拒绝使用任何第三方平台,拒绝向第三方平台提供任何信息
  • 提醒辅导员不要在级群等公开渠道发布含有个人信息的文件,提醒学生助理等不要将含有个人信息的文件不加权限地发布在如腾讯文档、上传至百度网盘等

如果是有良心的企业,有以下努力可以尝试:

  • 向学校等组织提供私有化部署方案,提供上门部署服务等,将数据留在学校;如有条件,可将系统开源
  • 自觉提升系统安全强度,及时修补漏洞,按照业界标准对数据进行加密等
  • 提供注销功能,让用户可以选择彻底删除(非软删除)所有在系统上存储的信息

Happy Ending or Tragic Ending?

超星事件的走向,我认为有以下两种可能

  • 超星技术真的菜到没意识到自己被入侵
  • 超星知道自己被脱裤了,但是可以靠关系降热度压热搜,互联网很健忘,下周就没事了

从经验来看,后者的可能性比较大。一个用户体验如此拉胯、技术水平如此低下的平台竟能在全国各地高校、中小学推广,其中涉及的利益链条不言而喻。从超星的声明中也看到很巧妙的一点,声明中只字不提专业班级学号、联系方式的泄露,矛头直指密码,只说密码单向加密没有泄露。对于惯于“用户愿意用隐私换取便利”的大众网民,他们不一定意识到除了密码以外许多东西都属于个人隐私,用户没有被踩到痛处,自然容易淡忘。

但另一方面,与之前CSDN被脱裤不同,这次的泄露涉及到的群体较为广泛,可谓是”火出圈“了,引起的注意也比较大。希望这次被波及到的用户能够真的痛定思痛,着手提升自己的隐私和信息安全意识。

篇外

本次超星事件不幸(但可以说几乎是必然)地波及到了本人所在的学校(目前已毕业)。经查,本人在超星使用学校+学号+密码的方式登录,绑定了常用的手机号码,所幸使用的密码是常用密码的一个变体,即使密码明文泄露造成的影响也有限。

☑️ ☆

Migrate to macOS

Migrate to macOS

常用应用迁移

App

名称 支持平台 解决方案
Termius macOS,Win,iOS/iPad OS,Android 直接使用
Navicat Premium macOS,Win DBeaver / JB Datagrip
滴答清单 macOS,Win,iOS/iPad OS,Android 直接使用
钉钉 macOS,Win,iOS/iPad OS,Android 直接使用
Foxmail macOS,Win 直接使用/另寻替代
JetBrains全家桶 macOS,Win 直接使用
Postman macOS,Win 直接使用
uTools macOS,Win 直接使用
Cubox macOS,Win,iOS/iPad OS,Android 直接使用
Office macOS,Win,iOS/iPad OS,Android 直接使用/另寻替代
Docker Desktop macOS,Win 直接使用
Another Redis Desktop Manager macOS,Win 直接使用
☑️ ⭐

【notification-gateway-lite】项目简介

notification-gateway-lite

简介

notification-gateway-lite 是一个非常轻量的通知网关,可以聚合各种推送渠道,使用 Serverless 部署,几乎零成本运行。

更新

特性

  • 等同于免费、开源、可自建的 新版Server酱,没有任何限制,痛快推送
  • 支持各种常见的推送渠道,如Bark、企业微信等
  • 支持部署成腾讯云Serverless,几乎零成本运行
  • 解决因为群晖DSM奇怪的webhook设置方式而无法接入一些推送渠道的问题

目前支持的通知方式

可能会支持的推送方式

  • 钉钉
  • pushplus (已停止运营)

如果有需要的通知方式,请提交 issue

部署方式

接口文档

在线版接口文档接口文档

示例应用

微信交流群

☑️ ☆

Python Cheatsheet

argparse

命令行不传参数时默认显示帮助信息

args = parser.parse_args(args=None if sys.argv[1:] else ["--help"])

pandas

去重

data = data.drop_duplicates()
🔲 ☆

Docker部署PHP应用几个坑点

官方Docker镜像默认没有启用PDO

在部署ThinkPHP框架应用时报错提示could not find driver,但没有显示任何有助于debug的信息(连mysql连接错误的那个数字+错误都没有)。搜索引擎找到PDO没有正确配置时可能会出现该问题(https://blog.csdn.net/weixin_42325823/article/details/106278947),使用phpinfo()查看确实PDO没有开启。进一步搜索后发现需要自行启动(https://stackoverflow.com/questions/37526509/how-to-install-pdo-driver-in-php-docker-image),同时dockerhub上也有相应的说明(https://hub.docker.com/_/php,见How to install more PHP extensions)。在Dockerfile中增加以下两行即可。

RUN docker-php-ext-install pdo pdo_mysql mysqli && docker-php-ext-enable pdo pdo_mysql mysqliRUN sed -i 's/^;extension=pdo_mysql$/extension=pdo_mysql/' /usr/local/etc/php/php.ini-production && mv /usr/local/etc/php/php.ini-production /usr/local/etc/php/php.ini

runtime目录需要给予777权限

没啥好说,就

RUN chmod -R 777 runtime
☑️ ⭐

双指针题集锦

LeetCode 977. 有序数组的平方

给你一个按 非递减顺序 排序的整数数组 nums,返回 每个数字的平方 组成的新数组,要求也按 非递减顺序 排序。

https://leetcode-cn.com/problems/squares-of-a-sorted-array/

思路

既然已知给的数组是排好序的,棘手的地方就是负数,因此考虑用两个指针,分别从最大和最小两端出发,比较两端大小,将较大的结果放入结果数组的最后,然后指针前进。这个方法比起另一种先找到正负边界再进行归并排序的方法更加优雅,而且不需要考虑边界的问题,实现更为简单。还有比较基础的,先算好所有平方的值再进行排序的方法,比较拉,不多赘述。

这题的解题模板跟二分搜索的模板类似,只需要多做一个变通,加入一个pos指示当前结果数组的位置。

时间复杂度为O(n),空间复杂度为O(1)。

题解

class Solution {    public int[] sortedSquares(int[] nums) {        int left = 0, right = nums.length - 1, pos = nums.length - 1;        int[] res = new int[nums.length];        while (left <= right) {            if (nums[left] * nums[left] > nums[right] * nums[right]) {                res[pos] = nums[left] * nums[left];                left++;            } else {                res[pos] = nums[right] * nums[right];                right--;            }            pos--;        }        return res;    }}

LeetCode 189. 轮转数组

给你一个数组,将数组中的元素向右轮转 k 个位置,其中 k 是非负数。

https://leetcode-cn.com/problems/rotate-array/

思路

当我们将数组的元素向右移动 k 次后,尾部 k mod n 个元素会移动至数组头部,其余元素向后移动 k mod n 个位置。因此完成轮转只需要三步:

  1. 将整个数组翻转,此时尾部k mod n个元素在前面;
  2. 翻转[0, k mod n - 1],使得原来尾部的元素按正常顺序排列;
  3. 翻转[k mod n, n-1],使得原来头部的元素按正常顺序排列。

题解

class Solution {    public void rotate(int[] nums, int k) {        k %= nums.length;        reverse(nums, 0, nums.length - 1);        reverse(nums, 0, k - 1);        reverse(nums, k, nums.length - 1);    }    public void reverse(int[] nums, int start, int end) {        while (start < end) {            int temp = nums[start];            nums[start] = nums[end];            nums[end] = temp;            start++; end--;        }    }}

LeetCode 283. 移动零

给定一个数组 nums,编写一个函数将所有 0 移动到数组的末尾,同时保持非零元素的相对顺序。

请注意 ,必须在不复制数组的情况下原地对数组进行操作。

https://leetcode-cn.com/problems/move-zeroes/

思路

容易想到用快慢指针,快的指针一直往前走,遇到非0时停下,与慢指针交换,然后慢指针往前走一步,快指针继续往前走,直到快指针走完循环中止。

一开始想法有点偏差,想把慢指针初始化为0,快指针初始化为1,用当慢指针遇到零,快指针往前走到非零,与慢指针交换,然后慢指针往前走一步,直到快指针走完循环中止。比这个正解多套了一层,且没法很好地管理边界情况。

题解

class Solution {    public void moveZeroes(int[] nums) {        int slow = 0, fast = 0;        while (fast < nums.length) {            if (nums[fast] != 0) {                int temp = nums[slow];                nums[slow] = nums[fast];                nums[fast] = temp;                slow++;            }            fast++;        }    }}

LeetCode 167. 两数之和 II - 输入有序数组

给你一个下标从 1 开始的整数数组 numbers ,该数组已按 非递减顺序排列  ,请你从数组中找出满足相加之和等于目标数 target 的两个数。如果设这两个数分别是 numbers[index1] 和 numbers[index2] ,则 1 <= index1 < index2 <= numbers.length 。

以长度为 2 的整数数组 [index1, index2] 的形式返回这两个整数的下标 index1 和 index2。

你可以假设每个输入 只对应唯一的答案 ,而且你 不可以 重复使用相同的元素。

你所设计的解决方案必须只使用常量级的额外空间。

思路

指针从两遍开始走,如果左右指针相加大于目标值,说明右边太大了,这时候右边指针-1;如果小于目标值,说明左边太小了,左边指针+1;两个指针相加等于目标值即为答案。

题解

class Solution {    public int[] twoSum(int[] numbers, int target) {        int slow = 0, fast = numbers.length - 1;        while (slow < fast) {            int cur = numbers[slow] + numbers[fast];            if (cur > target) {                fast--;            } else if (cur < target) {                slow++;            } else if (cur == target) {                return new int[] {slow + 1, fast + 1};            }        }        return new int[] {-1, -1};    }}
☑️ ☆

Pico Neo 3 简单评

最近到手了一个Pico Neo 3(6+256,零售顶配),字节子公司的产品,体验了几天,列一些游戏的体验和一些关于VR的想法。

游戏体验

以下几个都是steam串流玩的,主要还是看电脑性能,以及网络性能,我的2060+ac2100足够应付。

  • Beat Saber:简直就是家庭版跳舞机,玩起来太爽太上头了,很解压,我愿称之为VR上最好玩的游戏
  • H3VR:打枪的游戏,男孩子都爱。有各种枪械和投掷物,有很完善的配件和弹药、可摧毁环境系统。枪械的枪栓、弹匣、保险、都是可以操作的,所有的配件都能操作,灯能发光激光有红点,甚至倍镜的归零、倍率都能调整。有很多沙盒,有游戏型的有靶场类的,推荐室内靶场和后院靶场。枪械方面我很喜欢左轮,换弹方式实在太帅了。对不熟悉枪械的国人来说可能上手会有困难,建议先通过新手关熟悉枪械的基本操作(也还是需要枪械基本常识)。
  • Half-life:Alyx:据说是神作,个人感觉一般,虽然玩过其他动作类的游戏,还是觉得晕。特色是可交互的物品特别多,作为VR经典作品还是值得玩玩。

后续的是在Pico商店下载的,先锋通行证白嫖的游戏。

  • SuperhotVR:好像也是比较早的VR作品,动作型很强。可以出拳、扔物品、开枪,特色是子弹时间(移动时间才会流动)。玩起来也很爽,只是动作太多容易累,加上保存机制很奇怪,一关死了要从头开始没有checkpoint,很难受。
  • Red Matter:解谜类游戏,不是特别烧脑,但画质较差,而且移动也还是晕。
  • Contractors VR:打枪类游戏,有限几款枪械,有点像csgo那种场景,枪械手感也很真实,但是移动实在太晕,玩不下去。

简单评价一下VR

互动方式很新颖,很像以前的体感游戏,然后头显又给你把人融进去。可以看电影,躺着看像Bed House,体验还不错,可以访问NAS,电视和投影的平级替换(?)游戏很好玩,尤其是枪械类的,拟真度很高,能满足你的一切幻想。(Pico)有一个运动中心,可以统计运动了多久消耗了多少能量,就当在家多活动身体吧。

需要比较大的地方游玩,大约是33的空间会比较合适,55更加理想,我目前只能划出2*2左右的空间,游玩的时候经常打到周边的东西或者冲出安全区域(我臂展1.8m)。动作类的游戏移动方式非常晕,主要有两种:选择地点tp(瞬移),割裂感很强;摇杆移动,移动的速度和方向都很不自然,尤其是人站在原地摇杆向后,没习惯的时候都会一个踉跄站不稳,习惯一段时间之后还是会产生晕车的感觉,大脑一直在排斥不自然的移动。因此站桩类的游戏尤其好玩(节奏光剑和H3VR)。也是视觉上的,即使Pico已经是4K屏,还是觉得颗粒感很强(感觉等效1080p都没有),眼睛看久了很累。另外,可能由于性能限制,游戏内贴图很糊,完全不现实,甚至有点难受,因此像superhotVR或者节奏光剑这种反而不觉得画质很差。

头显的电池续航一般,满电可以游玩三四个小时左右,有点强行戒网瘾那意思。(Pico)平台上自带的游戏不多,而且基本都是小游戏,没有大作,有很多抄袭、缝合怪,差点意思。VR视频资源不多。爱奇艺VR就是2D视频+影院效果,没啥意思;可以装bilibili和其他安卓应用,当VR手机用,可以看b站刷抖音;有一个app可以看现场,有CBA球赛、音乐剧之类的,蛮有意思。

Porn:pornhub、xhamster等都有专门VR分区,实测可看,临场感颇强,但清晰度极低(1080p、1440p、1920p),自带厚码,且很多都是180,不是全景;有一些专门的vr porn site,基本都要收费

❌