Go 1.27 泛型方法详解:Go 泛型终于可以写在 Method 上了

Go 1.27 于 2026 年 8 月 19 日正式发布,其中最受关注的语言特性之一,就是 Generic Methods(泛型方法)

在 Go 1.18 引入泛型之后,我们可以定义泛型类型和泛型函数,但一直不能直接给普通类型的方法声明额外的类型参数。

Go 1.27 正式补上了这一能力。


一、Go 1.27 到底增加了什么?

在 Go 1.18 之后,我们已经可以这样定义泛型类型:

type List[T any] struct {
    items []T
}

也可以定义泛型函数:

func Map[T any, R any](items []T, fn func(T) R) []R {
    result := make([]R, 0, len(items))

    for _, item := range items {
        result = append(result, fn(item))
    }

    return result
}

但是,在 Go 1.26 以及更早的版本中,下面这种写法是不允许的:

func (l List[T]) Map[R any](fn func(T) R) List[R] {
    // ...
}

也就是说:

Go 1.18 ~ Go 1.26

泛型类型     ✅
泛型函数     ✅
泛型方法     ❌

而 Go 1.27 开始:

Go 1.27

泛型类型     ✅
泛型函数     ✅
泛型方法     ✅

Go 官方已经明确说明,Go 1.27 允许具体类型(concrete type)的 Method 声明自己的类型参数


二、为什么以前没有泛型方法?

这个问题其实非常有意思。

Go 在 Go 1.18 引入泛型时,并不是“忘记实现泛型方法”,而是经过了设计上的权衡。

例如:

type List[T any] struct {
    items []T
}

我们可以定义:

func Map[T any, R any](items []T, fn func(T) R) []R {
    // ...
}

但是不能:

func (l List[T]) Map[R any](fn func(T) R) List[R] {
    // Go 1.18 ~ 1.26 不允许
}

以前的 Go 泛型设计认为,泛型方法尤其是泛型接口方法会带来较复杂的实现问题。

例如假设存在:

type Empty struct{}

func (Empty) Nop[T any](x T) T {
    return x
}

那么当一个 Empty 被放进接口以后,运行时到底需要检查多少种 Nop 方法?

例如:

interface {
    Nop(string) string
}

或者:

interface {
    Nop(int) int
}

或者:

interface {
    Nop(io.Reader) io.Reader
}

泛型方法理论上可以产生无限多种实例化形式。

因此,Go 早期的泛型设计没有支持这种能力。

Go 1.27 的设计发生了变化:

允许具体类型的方法拥有自己的类型参数,但仍然不允许接口方法声明类型参数。

这是理解 Go 1.27 泛型方法最重要的一点。


三、Go 1.27 泛型方法的基本语法

Go 1.27 的泛型方法语法非常直观:

func (receiver ReceiverType) MethodName[T Constraint](arg T) T {
    // ...
}

例如:

type Box struct {
    value any
}

func (b Box) Get[T any]() T {
    return b.value.(T)
}

调用:

box := Box{
    value: 100,
}

value := box.Get[int]()

fmt.Println(value)

输出:

100

这里需要注意:

type Box struct {
    value any
}

和:

func (b Box) Get[T any]() T

是两个完全独立的类型参数。

TGet 方法自己的类型参数。


四、泛型类型 + 泛型方法

Go 1.27 最有价值的地方之一,就是可以同时结合:

  • 泛型类型
  • Receiver 类型参数
  • Method 类型参数

例如:

type List[T any] struct {
    items []T
}

func (l List[T]) Map[R any](fn func(T) R) List[R] {
    result := make([]R, 0, len(l.items))

    for _, item := range l.items {
        result = append(result, fn(item))
    }

    return List[R]{
        items: result,
    }
}

这里实际上存在两套类型参数:

List[T]

中的:

T

是类型本身的类型参数。

而:

Map[R any]

中的:

R

是方法自己的类型参数。

可以理解成:

List[T]
   │
   └── T:List 的类型参数

Map[R]
   │
   └── R:Map 方法自己的类型参数

五、Map 方法实战

完整代码:

package main

import "fmt"

type List[T any] struct {
    items []T
}

func NewList[T any](items ...T) List[T] {
    return List[T]{
        items: items,
    }
}

func (l List[T]) Map[R any](fn func(T) R) List[R] {
    result := make([]R, 0, len(l.items))

    for _, item := range l.items {
        result = append(result, fn(item))
    }

    return List[R]{
        items: result,
    }
}

func main() {
    numbers := NewList(1, 2, 3, 4)

    strings := numbers.Map(func(n int) string {
        return fmt.Sprintf("number-%d", n)
    })

    fmt.Println(strings.items)
}

输出:

[number-1 number-2 number-3 number-4]

这里:

numbers := NewList(1, 2, 3, 4)

得到:

List[int]

然后:

numbers.Map(...)

通过:

R any

把:

int

转换成:

string

最终得到:

List[string]

六、为什么泛型方法比泛型函数更舒服?

在 Go 1.18 ~ 1.26 中,我们通常只能这么写:

func MapList[T any, R any](
    list List[T],
    fn func(T) R,
) List[R] {
    // ...
}

调用:

result := MapList(
    MapList(
        NewList(1, 2, 3),
        func(n int) int {
            return n + 1
        },
    ),
    func(n int) string {
        return fmt.Sprintf("%d", n)
    },
)

这种写法有一个明显的问题:

链式调用不够自然。

代码是从里向外执行的。

Go 1.27 可以直接写:

result := NewList(1, 2, 3).
    Map(func(n int) int {
        return n + 1
    }).
    Map(func(n int) string {
        return fmt.Sprintf("%d", n)
    })

这种写法更符合我们对对象方法调用的直觉:

List[int]
   ↓
Map
   ↓
List[int]
   ↓
Map
   ↓
List[string]

Go 官方的 Generic Methods 介绍也使用了类似的 List.Map 示例,强调泛型方法能够让链式操作更加自然。


七、泛型方法中的类型推断

Go 1.27 同样支持泛型方法的类型推断。

例如:

type List[T any] struct {
    items []T
}

func (l List[T]) Map[R any](fn func(T) R) List[R] {
    result := make([]R, 0, len(l.items))

    for _, item := range l.items {
        result = append(result, fn(item))
    }

    return List[R]{
        items: result,
    }
}

调用:

numbers := NewList(1, 2, 3)

strings := numbers.Map(func(n int) string {
    return fmt.Sprintf("%d", n)
})

我们没有写:

Map[string]

Go 编译器可以从:

func(n int) string

推断出:

T = int
R = string

因此最终返回:

List[string]

八、泛型方法可以拥有多个类型参数

例如:

type Pair struct {
    first  any
    second any
}

func (p Pair) Convert[A any, B any](
    first func(any) A,
    second func(any) B,
) (A, B) {
    return first(p.first), second(p.second)
}

使用:

pair := Pair{
    first:  100,
    second: "hello",
}

a, b := pair.Convert(
    func(v any) int {
        return v.(int)
    },
    func(v any) string {
        return v.(string)
    },
)

最终:

a = 100
b = hello

因此,泛型方法并不要求只有一个类型参数。


九、泛型方法可以使用类型约束

泛型方法同样可以使用 Go 的类型约束。

例如定义一个数字类型约束:

type Number interface {
    ~int | ~int64 | ~float32 | ~float64
}

然后:

type Calculator struct {
    value int
}

func (c Calculator) Add[T Number](value T) T {
    return value + T(c.value)
}

使用:

calculator := Calculator{
    value: 10,
}

a := calculator.Add(20)
b := calculator.Add(3.14)

fmt.Println(a)
fmt.Println(b)

类型推断会自动确定:

T = int

以及:

T = float64

十、泛型方法与泛型类型参数不是一回事

这是实际开发中特别容易混淆的地方。

例如:

type Container[T any] struct {
    value T
}

这里的:

T

属于:

Container

而:

func (c Container[T]) Convert[R any]() R {
    // ...
}

这里的:

R

属于:

Convert

可以理解为:

Container[T]
      │
      │
      ▼
Convert[R]

因此:

Container[int]

调用:

Convert[string]()

并不意味着:

T = string

实际上:

T = int
R = string

这是两个不同的类型参数。


十一、泛型方法与接口

这里是 Go 1.27 泛型方法最重要的限制之一。

虽然 Go 1.27 支持:

type User struct{}

func (u User) Convert[T any](value T) T {
    return value
}

但是不能定义:

type Converter interface {
    Convert[T any](value T) T
}

也就是说:

具体类型的泛型方法       ✅

接口中的泛型方法         ❌

官方 Go 1.27 Release Notes 明确指出:

接口的方法不能声明类型参数,同时接口方法也不能由泛型方法实现。

因此下面代码依然是错误的:

type Converter interface {
    Convert[T any](value T) T
}

这不是 Go 1.27 泛型方法的遗漏,而是当前设计中的明确限制。


十二、为什么接口仍然不能拥有泛型方法?

假设:

type Converter interface {
    Convert[T any](value T) T
}

那么一个类型:

type MyConverter struct{}

理论上需要实现:

Convert[int]
Convert[string]
Convert[float64]
Convert[User]
Convert[Order]
...

问题在于:

类型系统和接口实现检查应该如何处理无限种可能的类型实例?

这会给接口的 method set、接口类型断言以及运行时实现机制带来非常大的复杂度。

因此 Go 1.27 采取的是一种折中的方案:

泛型方法:

普通具体类型       ✅
泛型类型的方法     ✅
接口方法           ❌

这也是 Go 1.27 泛型方法和其他语言泛型方法设计之间比较重要的区别。


十三、泛型方法和泛型函数应该怎么选择?

Go 1.27 以后,我们经常会面临一个选择:

func Map[T, R any](...)

还是:

func (list List[T]) Map[R any](...)

一般可以按照下面的思路选择。

1. 操作明显属于某个类型

推荐使用泛型方法:

type List[T any] struct {
    items []T
}

func (l List[T]) Map[R any](fn func(T) R) List[R] {
    // ...
}

调用:

list.Map(...)

这种 API 更符合面向对象的数据组织方式。


2. 操作不属于某个具体类型

推荐使用泛型函数:

func Max[T cmp.Ordered](a, b T) T {
    if a > b {
        return a
    }

    return b
}

调用:

Max(10, 20)

这种情况下没有必要人为创建一个类型,然后把函数挂到这个类型上。


十四、泛型方法特别适合链式 API

泛型方法一个非常明显的应用场景就是:

数据处理链

例如:

type Collection[T any] struct {
    items []T
}

定义:

func (c Collection[T]) Map[R any](fn func(T) R) Collection[R] {
    result := make([]R, 0, len(c.items))

    for _, item := range c.items {
        result = append(result, fn(item))
    }

    return Collection[R]{
        items: result,
    }
}

再定义:

func (c Collection[T]) Filter(fn func(T) bool) Collection[T] {
    result := make([]T, 0)

    for _, item := range c.items {
        if fn(item) {
            result = append(result, item)
        }
    }

    return Collection[T]{
        items: result,
    }
}

那么可以:

numbers := NewCollection(
    1, 2, 3, 4, 5, 6,
)

result := numbers.
    Filter(func(n int) bool {
        return n%2 == 0
    }).
    Map(func(n int) string {
        return fmt.Sprintf("number-%d", n)
    })

数据类型变化过程:

Collection[int]
       │
       │ Filter
       ▼
Collection[int]
       │
       │ Map
       ▼
Collection[string]

这就是泛型方法非常典型的应用场景。


十五、method expression 也支持泛型方法

Go 1.27 不仅支持直接调用泛型方法,也支持将泛型方法转换成函数。

例如:

type List[T any] struct {
    items []T
}

func (l List[T]) Map[R any](fn func(T) R) List[R] {
    // ...
}

可以使用:

f := List[int].Map[string]

此时:

f

就是一个具体的函数。

可以理解为:

func(
    List[int],
    func(int) string,
) List[string]

然后:

result := f(
    NewList(1, 2, 3),
    func(n int) string {
        return fmt.Sprintf("%d", n)
    },
)

Go 官方也特别提到了这一点:泛型方法可以通过 method expression 转换成对应的函数形式。


十六、一个完整的泛型集合类示例

下面实现一个简单的集合类型:

package main

import "fmt"

type Collection[T any] struct {
    items []T
}

func NewCollection[T any](items ...T) Collection[T] {
    return Collection[T]{
        items: items,
    }
}

func (c Collection[T]) Map[R any](
    fn func(T) R,
) Collection[R] {
    result := make([]R, 0, len(c.items))

    for _, item := range c.items {
        result = append(result, fn(item))
    }

    return Collection[R]{
        items: result,
    }
}

func (c Collection[T]) Filter(
    fn func(T) bool,
) Collection[T] {
    result := make([]T, 0)

    for _, item := range c.items {
        if fn(item) {
            result = append(result, item)
        }
    }

    return Collection[T]{
        items: result,
    }
}

func (c Collection[T]) ForEach(
    fn func(T),
) {
    for _, item := range c.items {
        fn(item)
    }
}

func main() {
    numbers := NewCollection(
        1, 2, 3, 4, 5, 6,
    )

    result := numbers.
        Filter(func(n int) bool {
            return n%2 == 0
        }).
        Map(func(n int) string {
            return fmt.Sprintf("number-%d", n)
        })

    result.ForEach(func(s string) {
        fmt.Println(s)
    })
}

输出:

number-2
number-4
number-6

这个例子很好地体现了 Go 1.27 泛型方法的优势:

Collection[int]
       │
       ├── Filter
       │
       ▼
Collection[int]
       │
       ├── Map
       │
       ▼
Collection[string]
       │
       └── ForEach

十七、Go 1.26 与 Go 1.27 对比

假设我们需要实现:

List[int]
    ↓
转换
List[string]

Go 1.26

由于没有泛型方法,只能把转换操作放到函数中:

func Map[T any, R any](
    list List[T],
    fn func(T) R,
) List[R] {
    // ...
}

调用:

result := Map(
    list,
    func(n int) string {
        return strconv.Itoa(n)
    },
)

Go 1.27

可以直接写:

func (l List[T]) Map[R any](
    fn func(T) R,
) List[R] {
    // ...
}

调用:

result := list.Map(
    func(n int) string {
        return strconv.Itoa(n)
    },
)

最大的区别并不是“以前做不到,现在做到了”。

因为 Go 1.18 以后,通过泛型函数其实已经能够完成类似工作。

真正的变化是:

泛型操作终于可以自然地组织到类型的方法集合中。

这使得 API 设计更加自然,也让链式调用更加清晰。


十八、Go 1.27 泛型方法的几个注意事项

1. 方法自己的类型参数和 Receiver 类型参数不同

例如:

type Box[T any] struct {
    value T
}

func (b Box[T]) Convert[R any]() R {
    // ...
}

这里有:

T → Box 的类型参数

R → Convert 的类型参数

不要混淆。


2. 接口仍然不能声明泛型方法

错误:

type Converter interface {
    Convert[T any](value T) T
}

Go 1.27 仍然不支持。


3. 不要为了泛型方法强行创建类型

例如:

type Utils struct{}

func (Utils) Max[T cmp.Ordered](a, b T) T {
    // ...
}

这种设计通常没有必要。

如果一个操作本身与某个具体类型没有关系:

func Max[T cmp.Ordered](a, b T) T {
    // ...
}

泛型函数往往更合适。


十九、Go 1.27 泛型方法最大的意义

Go 1.18 的泛型解决的是:

减少重复代码。

Go 1.27 的泛型方法进一步解决的是:

让泛型能力更自然地组织到类型 API 中。

两者可以简单理解为:

Go 1.18

泛型类型
    +
泛型函数

发展到:

Go 1.27

泛型类型
    +
泛型函数
    +
泛型方法

这使得很多以前只能写成:

Map(list, fn)
Filter(list, fn)
Reduce(list, fn)

的 API,可以自然地变成:

list.
    Filter(fn).
    Map(fn).
    Reduce(fn)

尤其是在:

  • 集合操作
  • 数据转换
  • 数据处理 Pipeline
  • ORM 查询构造
  • Builder
  • Stream API
  • 数据结构
  • 工具类 API

等场景中,泛型方法都具有比较明显的价值。


二十、总结

Go 1.27 正式加入了 Generic Methods(泛型方法)

最核心的语法就是:

func (receiver Receiver[T]) Method[R any](value R) R {
    // ...
}

它允许:

type List[T any] struct {
    items []T
}

func (l List[T]) Map[R any](fn func(T) R) List[R] {
    // ...
}

从而实现:

list.Map(...)

而不是过去的:

Map(list, ...)

不过需要特别注意:

具体类型的泛型方法       ✅
泛型类型的泛型方法       ✅
泛型函数                 ✅
接口中的泛型方法         ❌
泛型方法实现接口泛型方法 ❌

因此,Go 1.27 的泛型方法并不是把 Go 变成一个完全自由的“泛型方法语言”,而是在保持 Go 类型系统和接口设计原则的前提下,增加了一项非常实用的能力。

对于已经大量使用 Go 泛型的项目来说,Go 1.27 最大的变化之一,就是可以开始重新审视过去为了绕开“泛型方法不存在”而设计的 API:

Map(list, fn)

很多时候现在可以重新设计成:

list.Map(fn)

这会让代码更加符合 Go 的方法组织方式,也让链式数据处理更加自然。


参考资料

  • Go 1.27 Release Notes:官方发布说明
  • Go Blog:Generic Methods
  • Go 1.27 Release History

Go 1.27.0 的正式发布时间为 2026 年 8 月 19 日

官方关于 Generic Methods 的专门文章发布于 2026 年 8 月 26 日

2026-08-30 · xiejiahe · Golang
Electron 多实例多窗口进程启动缓慢

最近遇到一个问题,默认Electron双击exe的时候会创建一个新的实例进程,当第2个实例时首次加载会缓慢。

起初以为是主进程某个功能阻塞导致渲染缓慢,后面也排查了很久,AI 也解决不了。

后来一步一步删除代码逐步排查影响范围,发现只要使用了 localStorage/sessionStorage 就会导致缓慢,

原因

卡顿的窗口是由于在第一个应用程序仍在运行时启动了第二个应用程序副本造成的。两个副本都使用同一个 userData 文件夹。第一个副本持有会话存储数据库的锁,因此第二个副本需要等待大约四秒钟才能获得锁,之后才会放弃并转而使用内存。您的页面在加载时也会读取会话存储,因此也会等待一段时间。这是 Chromium 在所有平台上的工作方式。Chrome 不允许在同一个用户配置文件下启动第二个副本。

解决方案

在启动时调用 app.requestSingleInstanceLock(),如果返回 false 则退出。

不要利用默认方式去启动多进程实例,需要退出,通过主进程去调用创建窗口实现。

2026-08-30 · xiejiahe · JavaScript
免费商店零售销售系统POS系统

这是什么

这是一款由 Go 语言开发的商店零售销售系统,给所有中小店铺企业免费使用,目前该系统正在完善,欢迎使用该系统的用户提供建议。

支持 POS 扫码枪。

该项目支持跨平台,无需安装下载即用

下载

https://github.com/liuzi6612/open-gold/releases

2026-06-13 · xiejiahe · 实用工具
史上最详细的 ethers.js web3 入门教程

史上最详细的 ethers.js web3 入门教程

https://github.com/liuzi6612/ethers-tutorial

2025-05-26 · xiejiahe · WEB3
在线制作您的 Project 窗口

分享一个可以免费在线制作你的 Project 漂亮窗口

你可能需要在你自己的 Project 分享自己的产品,你可能会使用截图的方式,这太糟糕了,并不好看。

使用 Beautiful window 可以制作好看的窗口,效果:

在线制作地址 https://xjh22222228.github.io/beautiful-window/ 你可能需要科学上网才能打开,但他是完全免费

此项目开源在 Github 上 https://github.com/xjh22222228/beautiful-window

2024-09-16 · xiejiahe · 实用工具
创建一个强大且免费的导航网站

分享一个作者维护多年的导航网站,叫做发现导航,从2018年维护至今,是目前全球开源且持续维护的项目,目前拥有1000+使用者。 Github项目, 2.2KStar+

效果

具体效果点这里 https://nav3.cn

特性

三不需:无需数据库无需服务器无需成本

发现导航 的理念就是做一款无需依赖后端服务既简单又方便,没有繁杂的配置和数据库等配置概念, 做到开箱即用。

  • 🍰 内置 800+ 优质网站
  • 🍰 支持 码云 Gitee
  • 🍰 支持从浏览器书签导入
  • 🍰 支持将数据导出到浏览器书签
  • 🍰 支持用户提交收录
  • 🍰 支持自有部署/Fork
  • 🍰 丰富的资源配置系统
  • 🍰 支持 SEO 搜索引擎
  • 🍰 支持网站关联多个网址
  • 🍰 支持检测网站存活状态
  • 🍰 支持配置仅自己可见
  • 🍰 自动抓取网站图标/名称/描述
  • 🍰 支持暗黑模式
  • 🍰 支持后台管理, 无需部署
  • 🍰 支持多种浏览模式,创新
  • 🍰 支持足迹记忆
  • 🍰 支持多种搜索查询
  • 🍰 支持自定义引擎搜索
  • 🍰 多款高颜值主题切换
  • 🍰 强大的响应式系统
  • 🍰 多种 Loading 加载动画
  • 🍰 多种卡片风格设计
  • 🍰 完全纯静态, 提供自动化部署功能
  • 🍰 三叉树分类、结构清晰、分类清晰

像数321一样简单

1、打开 https://github.com/xjh22222228/nav 点击右上角Fork到自己仓库

2、确认

3、创建 Token 后续访问后台需要, 打开 https://github.com/settings/tokens/new

输入 TOKEN,过期时间选为不过期, 权限全部勾选,TOKEN 只要不泄露是安全的。

复制并保存TOKEN,如果这一步不保存就查看不了

4、打开 https://github.com/你的用户名/nav/settings/secrets/actions/new 把链接的用户名改成你的Github

Name填写 TOKEN 大写, Secret 填写刚刚保存的 TOKEN

5、编辑根目录 nav.config.yaml 文件,只需要修改仓库地址,把ID改成Github ID

6、这个时候Github Action 会自动启动

7、完成!坐等2分钟,打开链接 https://你的用户名.github.io/nav

后台

项目包含后台,不需要额外部署,只需要将链接后面增加 system => https://nav3.cn/system

2024-08-13 · xiejiahe · 实用工具
netlify应用部署服务开启history模式

如果你的应用服务启用了history路由模式,服务需要重定向到 index.html

netlify 默认不启用,启用需要在 public 目录新建 _redirects 文件内容:

/* /index.html 200

netlify.toml

[[redirects]]
  from = "/*"
  to = "/index.html"
  status = 200

参考:https://docs.netlify.com/routing/redirects/rewrites-proxies/#history-pushstate-and-single-page-apps

具体可以参考此项目 https://github.com/xjh22222228/tomato-work

2024-06-05 · xiejiahe · HTML
小程序生成二维码

找了下目前小程序没有跨端小程序生成二维码的库, 花了点时间整合了一下

小程序生成二维码支持微信、支付宝 等小程序使用。

具体使用移步到仓库:

https://github.com/xjh22222228/qrcode-mini

2024-05-05 · xiejiahe · JavaScript HTML
Chrome 一些实用功能

截图

可以对选中的 DOM 节点进行截图:

或者使用快捷键 shift+command+p 然后输入 screenshot 进行全屏截图。

copy

这是浏览器控制台内置的方法,主要用来剪贴内容。 比如说我们要把 localStorage 里的数据都拷贝出来,如果一个一个key/value 考出来就很费劲。 copy 就可以很好的解决这个问题。

$0

在控制台调试时需要对某个元素进行操作时一般会使用 document.querySelector(element) ,这种方式不仅长,还得知道元素选择器。

$0 则可以获取选中的目标元素。

快捷键 H

切换隐藏选中元素,实际上 Chrome 添加了一个类, visibility: hidden !important

$、$$

这2个方法实际上是 document.querySelector / document.querySelectorAll 简写。

重新发起请求

在一些场景定位BUG时想要对某个请求重新发起,如果不知道这个技巧,一般都是刷新页面。

选中需要重新发起请求栈右键:

生成页面二维码

这个功能我用得也挺多的, 主要是移动端调试直接扫一扫即可,而不需复制链接。

$_

获取最后一次执行结果,减少了赋值操作很方便。

2022-01-12 · xiejiahe · 其他
Go - 指针声明

在大部分时候初始化变量我们会这么做:

var n int
// 或者
n := 0

上面的代码实际上是等价的,没有问题, 但是在切片就不同了,看下面例子:

var sliceN1 []int // 结果为 nil

sliceN2 := []int{} // 结果为 []

这TM是一个坑啊,官方是这么解释的:

When storage is allocated for a variable, either through a declaration or a call of new, or when a new value is created, either through a composite literal or a call of make, and no explicit initialization is provided, the variable or value is given a default value. Each element of such a variable or value is set to the zero value for its type: false for booleans, 0 for numeric types, “” for strings, and nil for pointers, functions, interfaces, slices, channels, and maps. This initialization is done recursively, so for instance each element of an array of structs will have its fields zeroed if no value is specified.

官方文档指出切片在没有初始化的情况下切换的零值是 nil , 第一种是未初始化,第二种是初始化为空,所以这2种声明都是不等价的,也就是 nil != slice。 在开发过程中需要注意。

还有一种区别就是在 json 转换时 nil 会被当成 null, 而另外一种是被当初 [] 空数组。 在WEB开发中完全是坑。

var n []string
r, _ := json.Marshal(n)
fmt.Println(string(r)) // "null"


n := []string{}
r, _ := json.Marshal(n)
fmt.Println(string(r)) // "[]"

如果我们使用第二种初始化,就会被 Goland 认为是一种冗余的写法,会让你转换为第一种 var, 这样就又出刚刚说的问题了。

有人说这是 Goland 的 Bug, Goland 团队认为 nil切片在功能上等同于零长度切片,即使它不指向任何内容

解决办法是使用 make

var n []int = make([]int, 0) // 全局
sliceN := make([]int, 0) // 函数内

我的建议就是在声明切片时都使用 make 来初始化,防止入坑。

2022-01-09 · xiejiahe · Golang
go1.16 - go:embed 指令用法

go1.16 出了 go:embed 指令, 了解后发现很强!!!

作用

go:embed 是一个指令,用来嵌入静态文件到二进制文件当中。

我们在部署时会打包成一个二进制文件,如果应用包含一些配置文件比如 config.ini / index.html config.yaml 等一系列静态文件就比较麻烦了,在以往的做法就是将这些静态文件放到二进制同一个目录里头,通过文件系统进行读取, 或者是写在 .go 文件里, 就很不方便。

现在可以通过 go:embed 指令将这些静态文件打包在二进制文件里,也就是只有一个二进制文件。

规则

在使用 go:embed 指令之前先了解一下有哪些限制。

  • 如果 go:embed 指令跟着单个文件,只支持 string / []byte / embed.FS
  • 如果 go:embed 指令跟着多个文件或文件夹,只支持 embed.FS
  • go:embed 指令下一行必须紧跟着嵌入变量
  • 只支持全局变量
  • 只支持 string / []byte / embed.FS, 其他类型包括别名都不支持
  • 嵌入文件路径不支持绝对路径,不支持路径中包含 . / .., 当前路径使用 *

来个典型的例子

2.txt 文件内容:

hello golang
package main

import (
    _ "embed"
    "fmt"
)

//go:embed 2.txt
var s string
func main() {
    fmt.Println(s)
    // hello golang
}

注意 // 注释后面不能有空格

[]byte

字节切片类型:

package main

import (
    _ "embed"
    "fmt"
)

//go:embed 2.txt
var b []byte
func main() {
    fmt.Println(b)
    // [104 101 108 108 111 32 103 111 108 97 110 103]
}

嵌入文件系统

package main

import (
    "embed"
    "fmt"
)

//go:embed 2.txt
var f embed.FS

func main() {
    s, _ := f.ReadFile("2.txt")
    fmt.Println(string(s))
    // hello golang
}

嵌入多个文件

当嵌入多个文件只能使用 embed.FS 类型,不支持其他类型。

package main

import (
    "embed"
    "fmt"
)

//go:embed 1.txt 2.txt
var f embed.FS
func main() {
    s1, _ := f.ReadFile("1.txt")
    s2, _ := f.ReadFile("2.txt")
    fmt.Println(string(s1))
    fmt.Println(string(s2))
}

或者指令分开多行写也可以

package main

import (
    "embed"
    "fmt"
)

//go:embed 1.txt
//go:embed 2.txt
var f embed.FS
func main() {
    s1, _ := f.ReadFile("1.txt")
    s2, _ := f.ReadFile("2.txt")
    fmt.Println(string(s1))
    fmt.Println(string(s2))
}

FS 是只读的

FS 目前提供了3个API:

  • func (f FS) Open(name string) (fs.File, error)
  • func (f FS) ReadDir(name string) ([]fs.DirEntry, error)
  • func (f FS) ReadFile(name string) ([]byte, error)

这3个都是只读的,没有可写的接口, 也就是不存在竞态问题,可以并发读取。

embed.FS 实现了 1.16 io/fs.FS 新的接口:

package main

import (
    "embed"
    "fmt"
    "io/fs"
)

//go:embed 1.txt
var f embed.FS
func main() {
    content, _ := fs.ReadFile(f, "1.txt")
    fmt.Println(string(content))
}

嵌入文件夹

采用 / 斜杠来访问目录下的文件,windows 也一样。

当嵌入的是文件夹时除了 ._ 开头的文件和文件夹都会被嵌入即使在程序中没有使用。

package main

import (
    "embed"
    "fmt"
    "io/fs"
)

//go:embed test
var f embed.FS
func main() {
    content, _ := fs.ReadFile(f, "test/1.txt")
    fmt.Println(string(content))
    // hello golang
}
2021-02-28 · xiejiahe · Golang
快速获取你的电脑WIFI密码

快速获取你的电脑WIFI密码,并在需要时生成WiFi的QR码,以便手机轻松连接。

效果

安装

Mac:

curl -fsSL https://raw.sevencdn.com/xjh22222228/wifi/main/install.sh | bash

Windows:

Download

./wifi.exe

使用

mac

# 获取WIFI密码
$ wifi

# 打印二维码
$ wifi -q

# 生成二维码图片
$ wifi -i

# 获取 SSID
$ wifi -s

windows 一样

./wifi.exe

./wifi.exe -q
2021-01-31 · xiejiahe · Golang
GOSH - Golang实用程序库

GOSH - Golang实用程序库

推荐一个 Golang 的实用程序库,GOSH

提供了常用的字符串操作、随机生成数据操作,数学操作等, 让开发效率增倍。

文档地址

2021-01-16 · xiejiahe · Golang
go mod 基本操作

如果你用的 go版本 >= 1.13 GOMODULE 默认会启用,低于此版本的话升级吧。

初始化

如果项目需要 go module 就必须初始化一次。

go mod init 模块名

如果是开源项目放在 github 上:

# 注意不要带协议
go mod init github.com/xjh22222228/gosh

生成后会在项目下生成 go.mod 文件, 大概长这样

module github.com/xjh22222228/gosh

go 1.15

下载项目依赖

如果开了 go module 下载依赖会跟以往不太一样

go get ./...

更新依赖

更新项目下所有依赖最新版本

go get -u

也可以指定要更新的包

go get -u github.com/xjh22222228/gosh

纠正依赖

删除用不到的依赖包和下载用到的依赖包。

go mod tidy -v

生成vendor

在项目下生成 vendor 目录,也就是依赖包。

go mod vendor

验证go.mod依赖是否正确

$ go mod verify
all modules verified
2020-12-30 · xiejiahe · Golang
Github加速镜像
2020-11-29 · xiejiahe · git/svn
用Go写的一个检查Python依赖包最新版本命令行工具

用Go写的一个检查Python依赖包最新版本命令行工具

全称 python-check-updates 简称 pcu

为什么用 Go 写

主要原因是 Go 非常适合写工具类,因为不需要安装环境即可运行,加上更快,体积也小。

项目开源戳 https://github.com/xjh22222228/python-check-updates

这个项目只是用来玩的,直接用 pip 更好。

PCU

Find the latest version of your requirements.txt current dependency package.

Installation

Deno ships as a single executable with no dependencies. You can install it using the installers below, or download a release binary from the releases page.

Shell (Mac, Linux):

curl -fsSL https://raw.githubusercontent.com/xjh22222228/python-check-updates/main/install.sh | bash

Usage

Show any new dependencies for the project in the current directory:

$ pcu
Checking /opt/requirements_test.txt
13 / 13 [--------------------------] 100.00% 1 p/s
 pytest-cov                                       →   2.10.1
 pytest-mock                           ==2.0.0    →    3.3.1
 httpbin                               ==0.7.0    →    0.7.0
 pytest-httpbin                           <1.0    →    1.0.0
 MySQL-python                          ==1.2.5    →    1.2.5
 aliyun-python-sdk-cdn                 ==2.3.1    →    3.5.8
 XlsxWriter                            ==0.9.3    →    1.3.7
 aliyun-python-sdk-core                ==2.3.2    →  2.13.29
 wheel                                            →   0.35.1
 service-identity                     ==16.0.0    →   18.1.0
 pytest                       >=2.8.0,<=3.10.1    →    6.1.2
 zope.interface                        ==4.3.2    →    5.2.0

Done in  1 s.

Options

-f, --file                   Specify the file name of the check dependency
 package, default 'requirements.txt'
-v, --version                output the version number
-c, --check                  Check the latest version
-h, --help                   display help for command

Example

$ pcu -f=requirements_test.txt

$ pcu -v
2020-11-29 · xiejiahe · Golang
APP-IOS接入微信登录

基于Flutter。

由于笔者不是专业的APP开发,在这方面较弱,在接入微信登录遇到不少坑,基本都是配置问题,在这里记录下过程。


xcode: 12.0.1 mac: 10.15.5

IOS接入微信登录必须具备以下几点(缺一不可):

  • Universal Links (微信用于拉取APP)
  • 一个域名,必须支持Https
  • 微信第三方开放平台

1、新建一个文件名叫 apple-app-site-association 没有任何后缀, 并写入以下配置

{
    "applinks": {
        "apps": [],
        "details": [
            {
                "appID": "1UPF2ET6AP.com.example.app.scp",
                "paths": [ "/help/*" ]
            }
        ]
    }
}

需要修改 appIDpaths

  • appID由 teamId.Bundle ID组成, teamId 在APPLE开发者中心可以找到
  • paths (help是域名下的目录可以任意修改,后面必须带通配符,因为微信会随机在后面带字符串)

2、将文件放在域名根目录下。

打开你的域名比如 https://www.example.com/apple-app-site-association 如果文件被下载说明这步没问题。

3、使用 safari 浏览器打开 https://www.example.com/help/123 前面修改为你的域名,help 是前面在 paths 指定的, 123是乱写的随机字符串,打开后如果出现类似下面说明正常:

4、打开微信开放平台,找到IOS配置,下面必须全部都要配置

5、打开APPLE开发者中心,找到 Associated Domains 这个选项将它开启。

XCODE配置

1、使用 XCODE 打开 Runner.xcodeproj 文件。

如图配置 Domains, applinks 前缀是固定的,后面就前面配置的 Universal Links

2、配置 LSApplicationQueriesSchemes。

这个干嘛用的呢,主要是添加微信的 scheme 到白名单上去,这一步少不了,如果不添加没有办法知道微信是否已安装(尽管已安装了)。

找到 info.plist 文件, 将下面这一块内容添加到 dict 节点的末位,如下:

<key>LSApplicationQueriesSchemes</key>
<array>
  <string>wechat</string>
  <string>weixin</string>
  <string>weixinULAPI</string>
</array>

如果不知道这个文件在哪里,直接在当前工程下搜索这个文件, 一定有。

总结

接入这微信登录费了不少心思,少一步就要找各种问题进行排除,太蛋疼,还好终于搞定了,祝后面的小老弟们一切顺利哈。

参考

2020-11-16 · xiejiahe · Flutter
关于Docker无法在宿主执行容器命令

很多情况需要在 宿主 运行容器内的命令, 比如需要在宿主运行容器 nginx / redis / 等命令

有2种方法可以执行容器命令:

1、用内联重定向 EOF

docker exec -it c6261194df8e /bin/bash << EOF
touch /home/inner.txt
EOF

2、docker 参数-c 可以执行容器shell

docker exec -it c6261194df8e /bin/bash -c "touch /home/inner.txt"

用上面2种方法会发现虽然执行了,但是并没有在容器内创建文件,不信可以试试。

目前我还没知道什么问题。

执行Shell脚本解决

既然无法在宿主内执行容器内的Shell,那是不是可以执行容器Shell脚本?

写 Shell 脚本得有编辑器,容器默认不带 vim,所以得先安装:

apt-get update && apt-get install vim

Shell 脚本, 这里是Demo,实际参照自己需要执行的Shell。

ci.sh 内容如下:

#!/bin/bash

echo "hello" >> /home/inner.txt

来试试

docker exec -it c6261194df8e /bin/bash -c "/bin/bash /opt/ci.sh"

然后进入容器会发现 /home 目录下确实有 inner.txt 文件,说明这思路是可以的。

对了,执行容器内的外建命令记得用绝对路径执行,例如:

# 必须
/bin/nginx -s reload

# 这样不行的,因为有些外建命令没有加入环境变量
nginx -s reload
2020-11-14 · xiejiahe · 其他
Golang中的标准库Log

在我们编写程序时是离不开日志的,在Go标准库中提供了一个 log 模块。

比较常用的方法有下面这几个:

ln 结尾的只是多了个换行符 \n, f 结尾的是格式化文本。

  • Panicln / Panic / Panicf - 抛出异常并终止程序
  • Println / Print / Printf - 打印信息
  • Fatalln / Fatal / Fatalf - 打印信息并退出程序

很多新手不知道这些方法和 fmt 包中的方法有什么区别。

实际上上面那3个都只是 fmt 包中的语法糖

Panic

抛出异常信息并终止程序。

示例代码:

package main

import (
	"log"
)

func main() {
	log.Panic("error")
}

底层实现是这样子的:

可以看到这其实只是一个语法糖而已

func Panic(v ...interface{}) {
	s := fmt.Sprint(v...)
	std.Output(2, s)
	panic(s)
}

如果说能不能用全局方法 panic 呢? 不行, 因为panic 只接受一个参数:

panic("error")

所以为什么会有 log.Panic 这个方法了吧。

Fatal和Panic区别

FatalPanic 都是用来打印信息然后终止程序,不知道何时用哪个?

  • Panic: 抛出异常终止程序,一般来说在调试时想获得更多错误信息那就使用 Panic
  • Fatal: 这其实只是打印信息后正常退出程序不会提供更多额外信息给你

所以在程序中使用 Fatal 是没什么意义的,所以不推荐使用。

创建日志文件

创建日志文件的好处在于可以持久化,在出现问题的时候可以快速定位。

创建日志文件需要借助 os 模块和 log 模块。

示例代码,以注释说明:

package main

import (
	"log"
	"os"
)

func main() {
  // 打开一个文件
  // 第一个参数是写入日志文件的位置
  // 第二个参数是需要以什么权限进行操作,多个mode用 | 符号分隔代表多个
  // 最后参数是权限的八进制表示法
	file, err := os.OpenFile("./log.txt", os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0)

	if err != nil {
		log.Panicln("文件打开错误:", err)
	}

	defer file.Close()

  // 用 New 方法实例化
  // 第一个参数是 io.Writer 接口,只要实现了该接口都支持,文件就是
  // 第二个参数是 前缀信息 一般用于区别日志是属哪个分类
  // 最后一个参数是 标志, 也就是系统额外附加信息 LstdFlags 包含了日志和时间
	ilog := log.New(file, "Error:", log.LstdFlags)
	ilog.Println("输出错误信息")
}

标志有以下(都是以 L 开头):

参数 描述
Ldate 本地时区日期 2020/11/11
Ltime 本地时区时间 01:23:23
Lmicroseconds 微妙 01:23:23.123123
Llongfile 打印完整文件路径和行号
Lshortfile 打印文件名和行号
LUTC 如果设置了日期或时间,请使用UTC而不是本地时区
LstdFlags Ldate和Ltime的结合,日期和时间一起打印
Lmsgprefix 将Flag信息移至消息前面

打印行号

golang打印日志行号有时非常有用,因为默认 fmt 是不打印行号的,给调试带来不便。

有了上面的Flag信息就好实现了, 借助 Llongfile 即可。

package main

import (
	"log"
	"os"
)

func main() {
	ilog := log.New(os.Stdout, "Info:", log.Llongfile)
  ilog.Println("Hello")
  // 输出:Info:/Users/main.go:10: Hello
}

强大的第三方日志模块

目前比较流行的是这两个日志系统,可以去了解下

2020-11-07 · xiejiahe · Golang
Goland Unresolved dependency

每次新建项目都会遇到这个提示,明明安装依赖包了, 能用,就是爆红。

解决办法是在 Goland 设置里将 Go Modules 开启即可。

2020-09-23 · xiejiahe · Golang