分类 技术志 下的文章

Go类型系统:有何与众不同

本文永久链接 – https://tonybai.com/2022/12/18/go-type-system

Go是一门强类型的静态编程语言。使用Go编程,我们的每一行代码几乎都离不开类型。因此,深入学习Go,我们首先要对Go的类型系统(type system)有一个全面和深入的认知。Go类型系统可以给予我们一个全局整体的视角,以帮助我们更好地学习和理解Go语言中那些具体的与类型相关的内容。

一. 什么是类型系统

作为拥有一定Go编程经验的Gopher来说,大家对Go语言中的类型是有一定了解的,比如:Go内置了原生整型类型、浮点类型、复数类型、字符串类型、函数类型,提供了数组、切片、map、struct、channel等复合类型以及代表行为抽象的接口类型。通过Go提供的type关键字,我们还可以自定义类型等等。

那么大家是否想过这样的问题:为什么会有类型?类型可以带来哪些好处呢?回顾编程语言的发展史(见下图),我们发现:类型是高级语言有别于机器语言与低级语言的一种重要的抽象

从机器的视角来看,无论什么类型数据都是0101的二进制数据,但程序员直接用机器语言编码难度非常大且效率极其低下;汇编语言将层次提升到了面向多字节数据的编码,汇编指令的操作数都是固定长度字节的,比如:movb操作的是一个字节,movl操作的是四个字节。汇编指令并不关心真实存储的是什么数据,只是在各个地址之间搬移特定长度的数据。显然汇编的抽象层次依旧不高,直接用汇编写程序依然有很大难度以及较为低效。

高级语言之所以高级,就是因为它建立了类型这一重要抽象,类型抽象为开发者屏蔽了机器层面数据的复杂表示。类型下面的复杂的字节和bit操作由高级语言的编译器和运行时协助完成,开发人员只需面向类型进行编码即可,也就是说类型成为了开发者与编译器之间的“操作界面”

面向类型编程,开发者就要了解类型的能力、其所代表的抽象的含义以及遵循类型的使用规则/约束。类型决定了你可以在该类型实例中存储的值的范围;类型决定了你可以对该类型进行的操作;类型决定了该类型的变量需要的存储空间;类型决定了与其他类型间建立连接的方法:组合、“继承”还是接口实现等。

那么类型的这些能力、规则与约束是谁赋予的呢?没错,正是编程语言的类型系统

类型系统是高级语言的核心,它存在于语言规范中,向开发者明确了类型的能力、使用规则与约束;它存在于编译器中,保证开发者对类型的正确合规使用;它也存在于语言运行时里,为类型提供如多态这样的动态能力

可以说,高级编程语言用类型系统赋能类型并管理类型。不过,不同语言的类型系统的设计与实现是有较大差别的,那么Go语言的类型系统又有哪些与众不同之处呢?我们接下来就来重点看看Go的类型系统。

二. Go的类型系统

下面我们从类型定义、类型推导、类型检查、类型连接等多个方面说明一下Go类型系统具备的能力与不足。

1. 类型定义

大家知道Go支持几乎所有类型,下面是Go spec中的类型分类的列表截图:

同时,Go还支持使用type关键字定义的自定义类型以及类型别名(type alias):

type CustomType int // 底层类型为原生类型int的自定义类型CustomType

type S struct {
    a int
    b string
} // 基于struct的自定义类型S

type IntAlias = int // int的类型别名IntAlias

注:自定义类型与其底层类型(underlying type)是两个完全不同的类型,而类型别名并未引入新类型,与原类型等价。

不过有两种在其他语言中常见的类型,Go类型系统没有给予支持,一种是union联合类型,在这种类型中,其所有字段共享同一个内存空间:

// C代码

// 定义一个名为num的union类型
// 其三个成员m, ch, f共享同一个内存空间
// C编译器会以最大的字段的size为num类型变量分配内存空间
union num {
    int m;
    char ch;
    double f;
};
union num a, b, c; // 声明三个union类型变量

另外一种是enum枚举类型,不过enum枚举类型可一定程度上用const(可选加iota)来模拟:

// C语法
enum Weekday {
        SUNDAY,
        MONDAY,
        TUESDAY,
        WEDNESDAY,
        THURSDAY,
        FRIDAY,
        SATURDAY
};

// Go模拟实现Weekday
type Weekday int

const (
        Sunday Weekday = iota
        Monday
        Tuesday
        Wednesday
        Thursday
        Friday
        Saturday
)

Go从1.18版本开始支持泛型,这让Go类型系统具备定义带有类型参数(type parameters)的类型以及函数的能力。

2. 类型推导

Go类型系统支持自动类型推导能力,编译器可以推断出变量或函数的类型,而不需要我们明确指定:

var s = "hello" // s是string类型
a := 128        // a是int类型
f := 4.3567     // f是float64类型

除了支持普通类型推导,Go还支持泛型的自动类型实参推导,下面是一个来自go spec的例子:

func scale[Number ~int64|~float64|~complex128](v []Number, s Number) []Number

var vector []float64
scaledVector := scale(vector, 42)

例子中,通过scale调用时传入的实参类型,编译器可以自动推导出scale的类型参数Number的实参为float64。更多关于Go泛型的语法细节,可以参考《Go语言第一课》专栏的泛型篇

3. 类型检查

Go是一门强类型静态编程语言,意味着每个变量在使用之前都必须声明其类型。有了类型后,我们就可以按照Go类型系统规定的针对这个类型有效操作对其进行操作。

Go编译器以及运行时会分别在编译期间和运行期间对变量类型作检查,目的是确保操作只用于正确的类型,并且类型系统的规则被程序所遵守,保证类型安全等。

Go是强类型语言,并且没有隐式类型转换,所有类型转换都要以明确意图的显式类型转换来实施,Go编译器会在编译期间对类型转换进行检查,只有底层类型兼容的两个类型才可以实施显式转型:

type T1 int
type T2 struct{}

var i int = 5
var t T1
var s T2
t = i     // 错误,不是同一类型
t = T1(i) // ok,底层类型兼容
s = T2(t) // 错误,底层类型不兼容

除了编译期间的静态检查之外,Go类型系统还支持运行时动态类型检查,比如:检查传给接口变量的类型实例是否实现了该接口;在运行时对数组、切片类型的下标边界进行检查,确保下标不越界,保证内存安全等。

不过Go也提供了绕过类型系统检查的手段,比如unsafe.Pointer以及反射等。

4. 类型连接

Go并非经典OO语言,它的类型虽然可以拥有自己的方法(method),但Go却没有提供经典OO中的复杂的继承层次结构,没有父类,没有子类,更没有供类型初始化的构造函数。在Go的类型系统中,类型之间建立连接的方式只有组合,通过类型嵌入(type embedding),我们可以实现各类组合,可以嵌入非接口类型,亦可以嵌入接口来定义新组合后的类型。

通过类型组合,我们可以将各种类型连接在一起,共同对外提供聚合后的行为,包括多态能力。Go中标准的多态能力由interface类型实现,方法在运行时被分派,这取决于传给接口类型变量的具体类型。比如下面例子中AnimalQuackInForest中的Quack会依据传入的具体类型实例而分派,先后分派给Duck.Quack、Dog.Quack和Bird.Quack:

type QuackableAnimal interface {
    Quack()
}

type Duck struct{}

func (Duck) Quack() {
    println("duck quack!")
}

type Dog struct{}

func (Dog) Quack() {
    println("dog quack!")
}

type Bird struct{}

func (Bird) Quack() {
    println("bird quack!")
}                         

func AnimalQuackInForest(a QuackableAnimal) {
    a.Quack()
}                         

func main() {
    animals := []QuackableAnimal{new(Duck), new(Dog), new(Bird)}
    for _, animal := range animals {
        AnimalQuackInForest(animal)
    }
}

注:类型与接口之间的实现关系是隐式的,类型无需使用类implements关键字显式告知要实现的interface类型。

Go中的函数是一等公民,函数类型也可展现出一定的运行时多态能力,函数类型实例的最终执行结果取决于运行时传入的函数对象值。

三. 小结

Go提供了强大而又有趣的类型系统,不过Go没有提供enum、union类型,也不支持运算符重载(operator overloading)、函数重载、结构化错误处理以及可选/默认函数参数等。这与Go的设计者做出的保持Go简单的决策不无关系。同时类型系统在保证Go这门的语言的安全性方面也是功不可没。

如果你认真对待Go编程,你应该投入时间,了解它的类型系统和它的特殊性,这将是非常值得你花时间的。

四. 参考资料

  • Type Systems in Software Explained With Examples – https://thevaluable.dev/type-system-software-explained-example/
  • The Go type system for newcomers – https://rakyll.org/typesystem/
  • Deep Dive Into the Go Type System – https://code.tutsplus.com/tutorials/deep-dive-into-the-go-type-system–cms-29065
  • Understanding Golang Type System – https://thenewstack.io/understanding-golang-type-system/
  • A Closer Look at Golang From an Architect’s Perspective – https://thenewstack.io/a-closer-look-at-golang-from-an-architects-perspective/
  • https://go101.org/article/type-system-overview.html
  • https://baziotis.cs.illinois.edu/compilers/the-weird-type-system-of-golang.html
  • https://blog.ankuranand.com/2018/11/29/a-closer-look-at-go-golang-type-system/
  • 《Type Systems for Programming Languages》 – https://ropas.snu.ac.kr/~kwang/520/pierce_book.pdf
  • 《Programming with Types》 – https://book.douban.com/subject/35325133/
  • Type Systems in Programming Languages – https://www.tektutorialshub.com/programming/type-systems-in-programming-languages/
  • 《Category Theory for Programmers》 – https://book.douban.com/subject/30357114/
  • Type system(维基百科) – https://en.wikipedia.org/wiki/Type_system
  • 类型系统的比较 – https://en.wikipedia.org/wiki/Comparison_of_type_systems

“Gopher部落”知识星球旨在打造一个精品Go学习和进阶社群!高品质首发Go技术文章,“三天”首发阅读权,每年两期Go语言发展现状分析,每天提前1小时阅读到新鲜的Gopher日报,网课、技术专栏、图书内容前瞻,六小时内必答保证等满足你关于Go语言生态的所有需求!2022年,Gopher部落全面改版,将持续分享Go语言与Go应用领域的知识、技巧与实践,并增加诸多互动形式。欢迎大家加入!

img{512x368}
img{512x368}

img{512x368}
img{512x368}

著名云主机服务厂商DigitalOcean发布最新的主机计划,入门级Droplet配置升级为:1 core CPU、1G内存、25G高速SSD,价格5$/月。有使用DigitalOcean需求的朋友,可以打开这个链接地址:https://m.do.co/c/bff6eed92687 开启你的DO主机之路。

Gopher Daily(Gopher每日新闻)归档仓库 – https://github.com/bigwhite/gopherdaily

我的联系方式:

  • 微博(暂不可用):https://weibo.com/bigwhite20xx
  • 微博2:https://weibo.com/u/6484441286
  • 博客:tonybai.com
  • github: https://github.com/bigwhite

商务合作方式:撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。

Go为什么能成功

本文永久链接 – https://tonybai.com/2022/12/07/why-go-succeed


大家在入门Go语言时,多埋头于Go语法,忙于练手或快速完成公司的项目,无暇思考。

但当大家到了要进阶,要冲刺高级阶段的时候,我建议你不能再稀里糊涂了。既然入了Go这个坑,在进入高级阶段前,我们最好在门口的“影壁墙”前驻留一下。

仔细思考一下我们投入这么多精力研究的Go为什么能成功,后续还能否持续成功下去。你要有自己的基本的判断,自我暗示也好,坚定信心也罢,我们要为继续攀登Go高峰进行蓄能

一. 头脑风暴一下Go成功的因素

相信无论针对哪个gopher群体做头脑风暴,让大家列举Go成功的因素,大家的主流答案也无外乎下图中这些:

图中的各个因素与Go的成功都不无干系,但是究竟哪个或哪几个是决定性的呢?

二. Go成功的根本因素

很显然,这个问题是没有标准答案,是见仁见智的。这里我列举一下我的观点,供大家参考。

直接上结论,我认为Go成功的根本因素就一个:Google

为什么这么说呢?下面我们展开来看(见下图)!

我将Go社区比做一支军队,而Go就是Go社区的武器,与其他编程语言搏杀,占地盘(fans)。下面我们就来解构一下这支军队的构成以及为什么这支军队目前有诸多成功案例!

1. Google为Go社区提供了统帅与武器

众所周知,2007年Google的三名员工Robert Griesemer、Rob Pike和Ken Thompson(retire很早,精神上领袖,给予Go名誉上的背书)一起发明了Go语言,2009年Go开源后,Go社区逐渐形成。统帅是一支军队的灵魂,他们做出了影响Go和Go社区的最初的也是最重要的决策和这背后的Go设计哲学!

a) 设计决策

在2022年,Go团队在美国计算机学会通讯(Communications of the ACM)期刊上发表paper:《Go编程语言与环境》,对当年做出的诸多决策做了细致说明,这里对其中两个最重要的决策做简单说明:

  • Go旨在成为一个编程环境

Go语言之父们认为语言特性仅是编程语言的一部分,而编程环境特性与语言特性同等重要,这些环境特性包括:库、工具、惯例和针对软件工程的整体做法,它们都对使用Go语言编程提供了支持,不可或缺。而这些环境特性恰恰是在传统的编程语言设计中并没有受到应有的重视的。

这样的决策让Go在开源之初就为开发者提供了使用Go进行编程所需的几乎一切:包括功能丰富、开箱即用的标准库以及全面的工具集,代码格式化、代码静态检查、依赖关系管理、构建(包括跨平台交叉编译)、测试、性能剖析、查看和生成文档等,并且这些工具集在今天都统一放在了go命令的下面。这个决策也帮助Go在开源后吸引了第一批Go社区用户。

  • Go的一致性的表现

Go的一个目标是让它在不同的实现、执行环境中,甚至在不同的时间内表现出相同的行为。所以,Go语言尽可能地规定了一致的结果。比如:Go程序生命周期内一致的性能(相对于使用JIT慢启动的语言)、一致的GC的开销等。甚至对于最常见的编程错误提供了明确定义的语义,这有助于可理解性和调试,而不是像C/C++中那样,充斥着各种未定义的行为。

而我认为最重要的一致性则是从2012年发布的Go 1.0开始,Go团队公开承诺只对语言和标准库进行向后兼容的修改,这样程序在编译到较新的Go版本时可以继续运行而不发生变化。这一承诺对业界产生了吸引力,它不仅鼓励了那些长声明周期的工程项目(比如Google内部的一些大型项目或者像Kubernetes这样的社区顶级项目),也鼓励了其他努力,如书籍、培训课程和第三方软件包的繁荣生态系统。这一一致性的决策也为Go招募了相当数量的拥趸。 Go1兼容性,同样可以避免社区分裂(像python2/python3那样),即便是10多年来变更最大的泛型语法落地,也没有违反Go1兼容性,这实属不易。

b) 设计哲学

上述的设计决策的背后蕴含着Go语言之父们的设计哲学。

  • 简单

Tony Hoare在1980年图灵奖演讲中说了这样的观点:“我的结论是,构建软件设计有两种方法:一种方法是让它变得如此简单,显然没有缺陷,另一种方法是让它变得如此复杂,以至于没有明显的缺陷。第一种方法要困难得多。它需要同样的技能,奉献,洞察力,甚至灵感,就像发现作为自然复杂现象基础的简单物理定律一样。它还要求愿意接受受物理,逻辑和技术限制的目标,并在无法实现冲突目标时接受妥协。”

Go选择的正是Tony Hoare演进中的第一种构建软件的设计方法。Rob Pike说过的一句Go流行谚语“less is exponentially more”与此异曲同工。Go的语法简单,API简单,这些为Gopher提供了极大便利,但这些简单的背后其实是Go团队长时间的复杂的思考与实现,努力将语法和API简化为最小、最有用、最接近本质的努力工作。

同时,简单意味着可读性、可维护性,意味着代码的清晰。另一句Go谚语“Clear is better than clever”告诫Gopher们编写平淡如水的Go代码才是“政治正确”的,不要炫技。

  • 并发

多核时代,Go将并发作为语言内置特性。Go内置并发原语,包括goroutine、channel、select等。

Go鼓励在较高级别使用并发性,特别是通过通信的方式。我们耳熟能详的一句Go谚语是“Don’t communicate by sharing memory. Share memory by communicating”就是并发哲学的外在体现。

  • 组合

Go拥有类型,类型可以有method,这似乎像是一种面向对象style的实现,但Go并没有OO语言那种类型层次体系(type hierarchy),在Go中,组合才是Go类型之间建立联系的最主要手段,而interface和类型嵌入恰是这种组合哲学的具体体现。

  • 面向工程

2012年, Go开源元年,Rob Pike就在SPLASH 2012大会上以“Google的Go:为软件工程服务的语言设计”为题,讲解了Go是如何围绕Google内部存在的软件工程问题进行有针对性的语言设计的。可以看出,Go从诞生伊始就将解决软件工程领域问题作为语言的目标。同时,我们看到面向工程这个哲学与上面的旨在成为一个编程环境的决策息息相关。

除了统帅之外,Go社区的治理架构也是以Google“将领”为核心的,我们继续来看。

2. Google出钱:以Google“将领”(googler and ex-googler)为核心的Go社区治理架构

Go开源10年了,Go社区形成了以Googler和ex-googler(前google员工)为核心的Go社区治理架构,这些人就是上图中的那些“将领”,他们是Go项目某个细分领域,比如:编译器、运行时goroutine调度、GC、内存管理、网络、安全等的领头人。根据Go项目一名产品经理的描述:2021年,Google Go项目的专职人员多达50多人,Google这个“亲爹”在金钱的投入上显然表现的十分大方,不得不承认:在编程语言领域里,有个有钱的“亲爹”就是好

这种以googler和Ex-googler为开源社区治理核心的架构决定了Go社区采用的是一种我称之为“民主集中制”的决策机制。在Go社区你不要幻想会有绝对的公平投票,Go项目决策向来是由少数Googler和ex-googler主导的。这样意味着很多情况下,核心治理团队的人提出的proposal以及Google内部gopher提出proposal很容易被accept,而来自外部社区的proposal要想被accept,可能难度就要大一些。怎么说呢?Google的方案不一定总是最好的,但我们也不能不承认多数情况下,Googler提的proposal还是更优的,并且通常这些proposal对应的实现都已经在google内部测试过了,甚至和Go决策组在公司内部“吹过风”,如果你是Go社区的决策人,你会怎么做呢?你是更相信Googler还是外部一个没有任何背景的gopher呢?

我觉得在Google依然引领IT前沿的今天以及未来若干年,这种机制可能还是有利于Go的蓬勃发展的。

3. Google为Go社区提供战场/试验场

就像上面所说的那样,Go是有着非常鲜明Google烙印的编程语言,除了Go语言之父都来自google,Go社区治理架构的核心都来自Google和前google员工外,Google内部为Go的设计提供了足够的一流的问题域,也为Go的真实应用提供了试验场和真实战场,即便Go至今没有成为Google内部的第一语言。面向Google的一手且一流问题域,让Go设计者和Go开发者能够获得一手的反馈,从而对Go做进一步的打磨。

举几个例子:

  • Google内部的单一代码仓库让Go最初设计了不带版本的go get(后在社区的强烈要求下引入了go module,go get才支持版本号);
  • googler反馈,google内部工具超好用,这一定程度让Go团队认识到向Gopher提供完善的go工具链的重要性;
  • Google内部的多核与网络服务让Go设计者决定内置原生goroutine以应对多核时代的应用开发;
  • Google内性能与开发效率并重让Go设计者决定设计一门带gc的静态编程语言,将内存管理、并发管理下沉到runtime,这与近两年出现的服务网格, dapr等概念的思路一致;

  • Google内部大规模人员协作让Go决定面向软件工程,不仅要设计好语言特性,还要提供体验良好的编程环境(工具链、标准库等);
  • Google超大规模的系统构建慢让Go决定提供快速的构建能力,为此对包格式与包依赖做了精心的设计;
  • Google内部长期维护的系统(生命周期长) 让Go团队决定支持Go1兼容性并提供支持重构的语法,比如type alias等;
  • Google认为安全十分重要,促使Go提供了go sumdb和对sbom的良好支持;

同时Google内部系统为了支持Go的内部试验也是不遗余力,比如:每当Go发布大版本的RC版本,甚至是Beta版本时,Google App Engine都会首当其冲的充当“小白鼠”,在生产环境支持尚未发布正式版的Go。

另外Google在业内的领先性也让“近水楼台”的Go受益,比如像容器调度编排这样的平台,Google十年前就有了(borg),后续Googler以另起开源项目的方式将其中经验外溢输出,让Kubernetes最终选择了Go作为开发语言,从而成为Go的最大的也是最典型的成功战例。

综上,我们看到Google对Go成功的决定性作用,这种作用可决不能被理解为简单的金钱上的支撑。

三. Go语言演进历史

进入Go高级阶段后,对Go语言的演化历史要知道,当然能做到如数家珍更佳,即便不能,也要能记住Go语言的主要演化历史:

  • 2007年9月,Go语言诞生;
  • 2009年11月,Go正式开源;
  • 2012年3月,Go 1.0发布,同时Go1兼容性承诺官宣;
  • 2014年12月,Go 1.4版本发布,这是最后一个编译器和runtime由C语言实现的版本;
  • 2015年8月,Go 1.5版本发布,这个版本Go实现了自举(用go编译go),同时编译器和runtime中的绝大部分c代码都换成了go,新版gc让延迟大幅降低;
  • 2018年8月,Go 1.11版本发布,go module被正式引入;
  • 2022年3月,Go 1.18版本发布,Go泛型语法正式落地。

四. 小结

C++之父说过:“世上只有两种编程语言:一种是总是被人抱怨的,一种是从来没人用的”。

Go属于前者。世界上没有完美的编程语言,Go经过十年的打磨已经有了长足的进步,并且取得了不错的战绩,尤其是在云基础设施和云原生因公领域,就连Rob Pike也承认Go确实已成为云基础架构的语言。而这个Go走向成功的过程中,Google起着根本性的作用。

不过中国古语有云:成也萧何,败也萧何!目前Google仍然引领IT技术前沿,这对Go的发展来说是一个利好,也会不断推动Go向着好的方向发展。

但我大胆预测一下:“成也Google,败也Google”,一旦Google开始走下坡路的那天,Go语言成功的根基就不在了,Go还能像今天这样顺风顺水么?如果Go社区治理结构不重构,很可能不会再有今天这样的良好状态。大家觉得呢?

五. 参考资料

-《Go编程语言与环境:万字长文复盘导致Go语言成功的那些设计决策
-《Go内存模型》- https://research.swtch.com/gomm
-《Go语言真正的问题》 – https://vanitynotes.com/posts/20221101-the-real-problem-with-go


“Gopher部落”知识星球旨在打造一个精品Go学习和进阶社群!高品质首发Go技术文
章,“三天”首发阅读权,每年两期Go语言发展现状分析,每天提前1小时阅读到新鲜的Gopher日报,网课、技术专栏、图书内容前瞻,六小时内必>答保证等满足你关于Go语言生态的所有需求!2022年,Gopher部落全面改版,将持续分享Go语言与Go应用领域的知识、技巧与实践,并增加诸多互
动形式。欢迎大家加入!

img{512x368}
img{512x368}

img{512x368}
img{512x368}

Gopher Daily(Gopher每日新闻)归档仓库 – https://github.com/bigwhite/gopherdaily

我的联系方式:

  • 微博(暂不可用):https://weibo.com/bigwhite20xx
  • 微博2:https://weibo.com/u/6484441286
  • 博客:tonybai.com
  • github: https://github.com/bigwhite

商务合作方式:撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。

如发现本站页面被黑,比如:挂载广告、挖矿等恶意代码,请朋友们及时联系我。十分感谢! Go语言第一课 Go语言精进之路1 Go语言精进之路2 Go语言编程指南
商务合作请联系bigwhite.cn AT aliyun.com

欢迎使用邮件订阅我的博客

输入邮箱订阅本站,只要有新文章发布,就会第一时间发送邮件通知你哦!

这里是 Tony Bai的个人Blog,欢迎访问、订阅和留言! 订阅Feed请点击上面图片

如果您觉得这里的文章对您有帮助,请扫描上方二维码进行捐赠 ,加油后的Tony Bai将会为您呈现更多精彩的文章,谢谢!

如果您希望通过微信捐赠,请用微信客户端扫描下方赞赏码:

如果您希望通过比特币或以太币捐赠,可以扫描下方二维码:

比特币:

以太币:

如果您喜欢通过微信浏览本站内容,可以扫描下方二维码,订阅本站官方微信订阅号“iamtonybai”;点击二维码,可直达本人官方微博主页^_^:
本站Powered by Digital Ocean VPS。
选择Digital Ocean VPS主机,即可获得10美元现金充值,可 免费使用两个月哟! 著名主机提供商Linode 10$优惠码:linode10,在 这里注册即可免费获 得。阿里云推荐码: 1WFZ0V立享9折!


View Tony Bai's profile on LinkedIn
DigitalOcean Referral Badge

文章

评论

  • 正在加载...

分类

标签

归档



View My Stats