作者都是各自领域经过审查的专家,并撰写他们有经验的主题. 我们所有的内容都经过同行评审,并由同一领域的Toptal专家验证.
布兰登·霍格的头像

布兰登火车司机

验证专家 in 工程

Brendon在设计和交付从社交网络初创公司到企业电信解决方案的软件方面有15年以上的经验.

专业知识

以前在

爱立信
分享

什么是Go编程语言?

相对较新的 Go编程语言 整齐地坐落在风景的中央, 提供了许多好的功能,并故意省略了许多不好的功能. 它编译得很快, 运行fast-ish, 包括运行时和垃圾收集, 拥有简单的静态类型系统和动态接口, 也是一个优秀的标准库. 这就是为什么这么多开发人员热衷于学习Go编程.

Golang教程:logo插画

选择OOP

OOP是Go故意忽略的特性之一. 它没有子类, 所以没有继承方块,超调用或虚方法来绊倒你. 尽管如此,OOP的许多有用部分可以通过其他方式获得.

*Mixins*可以通过匿名嵌入结构体来实现, 允许在包含结构体上直接调用它们的方法(参见 嵌入). 这种方式的推广方法称为“转发”, 它与子类化不一样:方法仍然会在内部调用, 嵌入式结构.

嵌入也不意味着多态性. 而A可能 a ' B ',这并不意味着它 is a ' B '——接受' B '的函数不会接受' a '. 为此,我们需要 接口,我们稍后会简短地讲到.

与此同时,Golang对可能导致混淆和bug的特性采取强硬立场. 它省略了诸如继承和多态性之类的OOP习惯用法, 支持组合和简单接口. 它淡化了异常处理,支持返回值中的显式错误. 只有一种正确的方式来布置Go代码,由 go的 工具. 等等......。.

为什么要学Golang ??

Go也是一门很棒的写作语言 并发程序:具有许多独立运行部件的程序. 一个明显的例子是web服务器:每个请求都单独运行, 但是请求通常需要共享资源,比如会话, 缓存, 或者通知队列. 这意味着 熟练的Go程序员 需要处理对这些资源的并发访问.

虽然Golang有一组优秀的低级特性来处理并发性, 直接使用它们会变得很复杂. 在很多情况下, 在这些低级机制之上的一些可重用抽象使生活变得容易得多.

在今天的Go编程教程中, 我们将研究一个这样的抽象:一个包装器,它可以将任何数据结构转换为 事务服务. 我们用a 基金 以类型为例——为我们的创业公司的剩余资金创建一个简单的商店, 我们在哪里可以查余额和取款.

为了在实践中证明这一点, 我们将逐步构建该服务, 一路上弄得一团糟,然后再清理干净. 随着我们在Go编程教程中的进步, 我们会遇到很多很酷的Go语言特性, 包括:

  • 结构类型和方法
  • 单元测试和基准测试
  • 例程和通道
  • 接口和动态类型

以身作则:建立一个简单的基金

让我们编写一些Golang代码来跟踪我们的创业公司的资金. 基金从一个给定的余额开始, 资金只能提取(我们稍后会算出收入).

这张图描述了一个使用Go编程语言的简单例程示例.

围棋是刻意的 面向对象语言:没有类、对象或继承. 相反,我们将声明 结构体类型 被称为 基金,其中包含一个用于创建新基金结构的简单函数和两个公共方法.

基金.go

计划资助

基金结构{
    // 平衡是未导出的(私有),因为它是小写的
    平衡int
}

//一个常规函数返回一个指向基金的指针
function New基金(initial平衡 int)
    //我们可以返回一个指向新结构体的指针,而不用担心
    //它是在堆栈上还是堆上:Go为我们计算.
    返回 &基金{
        平衡:initial平衡,
    }
}

//方法以一个*receiver*开头,在本例中是一个基金指针
function (f *基金) 平衡() int {
    返回f.平衡
}

function (f *基金)提取(金额int) {
    f.余额-=金额
}

使用基准测试

接下来我们需要一种测试方法 基金. 我们将使用Go的程序,而不是编写单独的程序 测试包,它为单元测试和基准测试提供了一个框架. 简单的逻辑 基金 不值得为它编写单元测试, 但是因为我们以后会讨论很多同时使用基金的问题, 编写基准是有意义的.

基准测试类似于单元测试,但它包含一个循环,可以多次运行相同的代码(在我们的例子中, 基金.取(1)). 这允许框架计算每次迭代所需的时间, 平均磁盘寻道的瞬态差异, 缓存错过, 进程调度, 以及其他不可预测的因素.

测试框架希望每个基准测试至少运行1秒(默认情况下)。. 为了确保这一点, 它将多次调用基准, 每次传入不断增加的“迭代次数”值 b.N Field),直到运行至少需要一秒钟.

目前,我们的基准只是存入一些钱,然后每次取出一美元.

基金_test.go

计划资助

导入“测试”

基金基准基金(b *测试.B) {
    //每次迭代多少,添加多少美元
    = New基金(b).N)

    //一次烧一个,直到全部烧完
    for i := 0; i < b.N; i++ {
        基金.取(1)
    }

    如果基金.平衡() != 0 {
        b.错误("余额不是零:",基金.平衡())
    }
}

现在让我们运行它:

$ go测试台 . 资金
测试:警告:没有要运行的测试
通过
基准取款2000000000.69 ns / op
好的,资金3.576s

进展顺利. 我们跑了20亿(!)迭代,并且对平衡的最终检查是正确的. 我们可以忽略“没有测试要运行”的警告, 它指的是我们没有编写的单元测试(在本Golang教程后面的Go编程示例中), 警告被剪掉).

Go中的并发访问

现在让我们将基准设置为并发的, 来模拟不同的用户同时取款. 为此,我们将生成十个程序,每个程序提取十分之一的钱.

我们如何在Go语言中构建多个并发程序?

在Go语言中,Go例程是实现并发的基本构建块. 他们是 绿色线程 轻量级线程由Go运行时管理,而不是由操作系统管理. 这意味着您可以运行数千(或数百万)个线程,而不会产生任何显著的开销. 派生出 go 关键字,并且总是以函数(或方法调用)开始:

//立即返回,不等待' DoSomething() '完成
去DoSomething ()

通常,我们只需要几行代码就可以生成一个简短的一次性函数. 在这种情况下,我们可以使用闭包来代替函数名:

Go func () {
    // ... 做的东西 ...
}() //必须是函数*调用*,所以记住()

一旦生成了所有的例程,我们需要一种等待它们完成的方法. 我们可以自己造一个 渠道,但我们还没有遇到这些,所以这就跳过了.

现在,我们可以用 WaitGroup 在Go的标准库中输入,它的存在就是为了这个目的. 我们将创建一个名为"wg)和呼叫 wg.添加(1) 在产卵每个工蚁之前,记录有多少工蚁. 然后工人们会报告使用情况 wg.(完成). 同时,在主例程中,我们可以说 wg.Wait () 阻塞,直到每个工人都完成.

在下一个示例中的worker例程中,我们将使用 推迟 打电话给 wg.(完成).

推迟 接受一个函数(或方法)调用,并在当前函数返回之前立即运行它, 在其他事情都做完之后. 这是方便的清理:

func () {
    资源.锁 ()
    推迟资源.解锁()

    //使用资源做一些事情
}()

这样我们就可以很容易地匹配 解锁 与它的 ,便于阅读. 更重要的是,延迟函数将运行 即使有恐慌 在main函数中(在其他语言中我们可能通过try-finally来处理).

最后,延迟函数将在 反向 他们被召唤到的秩序, 这意味着我们可以很好地进行嵌套清理(类似于C语言的“嵌套”) 转到年代和 labelS,但整洁多了):

func () {
    db.Connect ()
    推迟db.断开()

    //如果Begin出现恐慌,只有db.断开()将执行
    事务.开始()
    推迟Transactions.Close ()

    //从这里开始,事务.Close ()将首先运行,
    //然后是db.断开()

    // ...
}()

好了,说了这么多,这是新版本:

基金_test.go

计划资助

导入(
    “同步”
    “测试”
)

const WORKERS = 10

基准提取(b *测试).B) {
    //跳过N = 1
    如果b.N < WORKERS {
        返回
    }

    //每次迭代多少,添加多少美元
    = New基金(b).N)

    //随便假设b.N整除干净
    b . dollarsPerFounder:=.N / workers

    //不需要初始化WaitGroup结构
    //(它们的“零值”已经可以使用了).
    //因此,我们只声明一个,然后使用它.
    Var wg sync.WaitGroup

    for i := 0; i < WORKERS; i++ {
        //让waitgroup知道我们正在添加一个go例程
        wg.添加(1)
        
        //作为闭包派生一个创建工作者
        Go func () {
            //当函数结束时,标记此worker已完成
            推迟工作组.(完成)

            for i := 0; i < dollarsPerFounder; i++ {
                基金.取(1)
            }
            
        }() //记得调用闭包!
    }

    //等待所有的worker完成
    wg.Wait ()

    如果基金.平衡() != 0 {
        b.错误("余额不是零:",基金.平衡())
    }
}

我们可以预测这里会发生什么. 工人们都将执行任务 撤回 在彼此的上面. 在里面, f.余额-=金额 将读取余额,减去1,然后写回来吗. 但有时两个或更多的工人会同时读取相同的余额, 做同样的减法, 我们最终会得到错误的总数. 正确的?

$ go测试台 . 资金
基准提取2000000000 2.01 ns / op
好的,资金4.220s

不,它还是过去了. 这里发生了什么??

记住,程序是 绿色线程 -它们由Go运行时管理,而不是由操作系统管理. 运行时可以跨多少个操作系统线程调度程序. 在写这个Go语言教程的时候, Go不会尝试猜测应该使用多少操作系统线程, 如果我们想要多于一个, 我们不得不这么说. 最后, 当前运行时不会抢占运行例程——运行例程将继续运行,直到它做了一些表明它准备中断的事情(比如与通道交互)。.

所有这些都意味着,尽管我们的基准现在是并发的,但它不是 平行. 一次只有一个工人会运行,它会一直运行到完成为止. 我们可以通过告诉Go使用更多的线程来改变这一点 GOMAXPROCS 环境变量.

$ GOMAXPROCS=4 go测试台 . 资金
benchmark - withdraw -4—FAIL: benchmark - withdraw -4
    account_test.go:39:余额不是0:4238
好的,资金0.007s

这是更好的. 现在我们显然失去了一些提款,正如我们所料.

在这个Go编程示例中,多个并行程序的结果是不利的.

让它成为一个服务器

在这一点上,我们有多种选择. 我们可以在基金周围添加显式互斥锁或读写锁. 我们可以使用带有版本号的比较与交换. 我们可以全力以赴,用一个 CRDT 方案(可能取代 平衡 字段,其中包含每个客户机的事务列表,并从中计算余额).

但我们现在不会做这些事情,因为它们要么混乱,要么可怕,要么两者兼而有之. 相反,我们会决定基金应该是 服务器. 什么是服务器? 它是你交谈的对象. 在Go中,事物通过通道交流.

通道是程序之间的基本通信机制. 值被发送到通道(使用 channel <- value),并且可以在另一侧接收(带有 value = <- channel). 通道是“常规安全的”, 这意味着任意数量的例程可以同时向它们发送和接收.

缓冲

在某些情况下,缓冲通信通道可能是一种性能优化, 但它应该非常小心地使用(并进行基准测试)!).

然而,缓冲通道也有一些与通信无关的用途.

例如, 常见的节流习惯用法创建一个缓冲区大小为' 10 '的通道,然后立即向其发送10个令牌. 然后生成任意数量的工作程序, 在开始工作之前,每个通道都从通道接收一个令牌, 然后送回去. 然后,无论有多少工人,只有10人会同时工作.

默认情况下,Go通道为 无缓冲的. 这意味着将值发送到通道将被阻塞,直到另一个线程程序准备好立即接收它. Go还支持通道的固定缓冲区大小(使用 make(chan someType, bufferSize)). 然而,对于正常使用,这是 通常是个坏主意.

想象一下我们的基金有一个web服务器,每个请求都进行提款. 当事情很忙的时候 基金Server 我就跟不上了, 试图发送到其命令通道的请求将开始阻塞并等待. 此时,我们可以在服务器中强制执行最大请求计数, 并返回一个合理的错误代码(如 503服务不可用)向超过限额的客户提供服务. 当服务器过载时,这是可能的最佳行为.

在通道中添加缓冲会降低这种行为的确定性. 基于客户端更早看到的信息,我们很容易出现未处理命令的长队列(可能对于已经超时的上游请求)。. 这同样适用于许多其他情况, 比如当接收方跟不上发送方的速度时,对TCP施加反压.

在任何情况下,对于我们的Go示例,我们将坚持使用默认的无缓冲行为.

我们将使用一个通道向我们的 基金Server. 每个基准工作器都会向通道发送命令,但只有服务器会接收它们.

我们可以直接将基金类型转换为服务器实现, 但是那样会很混乱——我们会把并发处理和业务逻辑混在一起. 相反,我们将保持基金类型不变,并制作 基金Server 一个单独的包装.

像任何服务器一样, 包装器将有一个主循环,在其中等待命令, 然后依次回应. 这里还有一个细节需要说明:命令的类型.

在这个Go编程教程中,基金被用作服务器的示意图.

指针

我们可以让命令通道接受指向命令的指针(' chan *办理ionComm和 '). 为什么我们没有?

在程序程之间传递指针是有风险的,因为任何一个程序程都可能修改它. 它的效率也往往较低, 因为其他例程可能在不同的CPU核心上运行(这意味着更多的缓存无效).

只要可能,最好传递普通值.

在下面的下一节中, 我们将发送几个不同的命令, 每个都有自己的结构类型. 我们希望服务器的Comm和s通道能够接受它们中的任何一个. 在面向对象语言中,我们可以通过多态性来实现这一点:让通道采用超类, 其中各个命令类型是子类. 在Go语言中,我们使用 接口 而不是.

接口是一组方法签名. 实现所有这些方法的任何类型都可以被视为该接口(无需声明这样做). 我们的第一次运行, 我们的命令结构实际上不会公开任何方法, 所以我们将使用空接口, 接口{}. 因为它没有要求, 任何 值(包括像整数这样的原始值)满足空接口. 这不是理想的——我们只想接受命令结构——但我们稍后会回到它.

现在,让我们开始为Go服务器搭建脚手架:

服务器.go

计划资助

类型基金Server struct {
    命令chan 接口{}
    基金的基金
}

函数New基金Server(initial平衡 int) *基金服务器{
    服务器:= &基金Server {
        // make()创建通道、映射和片等内置函数
        命令:make(chan 接口{}),
        基金:New基金 (initial平衡),
    }

    //立即退出服务器的主循环
    去服务器.循环()
    返回服务器
}

func (s *基金Server)循环(){
    //内置的"range"子句可以遍历通道,
    //除此之外
    对于命令:= range s.命令{
    
        //处理命令
        
    }
}

现在让我们为命令添加几个Golang结构类型:

退出命令struct {
    int数量
}

type 平衡Comm和 struct {
    响应chan
}

撤回Comm和 只包含要取款的金额. 没有回应. 的 平衡Comm和 有响应,所以它包含一个发送它的通道. 这确保了响应总是到达正确的位置, 即使我们的基金后来决定做出不正常的反应.

现在我们可以编写服务器的主循环了:

func (s *基金Server)循环(){
    对于命令:= range s.命令{

        // comm和只是一个接口{},但是我们可以检查它的实际类型
        开关命令.(类型){

        案例撤回Comm和:
            //然后使用“类型断言”来转换它
            退出:=命令.(撤回Comm和)
            s.基金.退出(退出.量)

        案例平衡Comm和:
            get平衡:=命令.(平衡Comm和)
            余额:= s.基金.平衡()
            get平衡.Response <- 平衡

        默认值:
            恐慌(fmt.Sprintf("无法识别的命令:%v",命令))
        }
    }
}

嗯. 这有点难看. 我们正在切换命令类型,使用类型断言,并且可能崩溃. 让我们继续前进,更新基准以使用服务器.

基准提取(b *测试).B) {
    // ...

    服务器:= New基金Server(b.N)

    // ...

    //生成工人
    for i := 0; i < WORKERS; i++ {
        wg.添加(1)
        Go func () {
            推迟工作组.(完成)
            for i := 0; i < dollarsPerFounder; i++ {
                服务器.Comm和s <- 撤回Comm和{ Amount: 1 }
            }
        }()
    }

    // ...

    平衡ResponseChan:= make(chan int)
    服务器.Comm和s <- 平衡Comm和{ Response: 平衡ResponseChan }
    平衡 := <- 平衡ResponseChan

    如果平衡 != 0 {
        b.错误("平衡 is 不 zero:", 平衡)
    }
}

这也有点难看,尤其是当我们检查平衡的时候. 不要介意. 让我们试一试:

$ GOMAXPROCS=4 go测试台 . 资金
基准抽屉式- 465 ns/op
好的,资金2.822s

好得多,我们不再损失提款了. 但是代码越来越难读,而且还有更严重的问题. 如果我们发行 平衡Comm和 然后忘记阅读响应,我们的基金服务器将永远阻止发送它. 让我们把东西整理一下.

让它成为一种服务

服务器是你可以与之交谈的东西. 什么是服务? 服务是你与之对话的东西 带有API的. 而不是让客户端代码直接使用命令通道, 我们将使通道未导出(私有),并将可用的命令封装在函数中.

类型基金Server struct {
    命令chan 接口{} //小写名称,未导出
    // ...
}

func (s *基金Server) 平衡() int {
    responseChan:= make(chan int)
    s.comm和s <- 平衡Comm和{ Response: responseChan }
    返回 <- responseChan
}

func (s *基金Server)提取(金额int) {
    s.comm和s <- 撤回Comm和{ Amount: amount }
}

现在我们的基准会说 服务器.取(1)平衡:=服务器.平衡(), 并且不太可能意外地向它发送无效命令或忘记读取响应.

下面是在这个示例Go语言程序中使用基金作为服务的样子.

对于这些命令,仍然有很多额外的样板文件,但我们将在后面讨论.

Transactions

最终,钱总会花光. 让我们达成一致,当我们的资金只剩下最后10美元时,我们将停止取款, 把钱花在公共披萨上,庆祝或同情周围的人. 我们的基准将反映如下:

//生成工人
for i := 0; i < WORKERS; i++ {
    wg.添加(1)
    Go func () {
        推迟工作组.(完成)
        for i := 0; i < dollarsPerFounder; i++ {

            //当我们只剩下披萨的钱时就停下来
            如果服务器.平衡() <= 10 {
                打破
            }
            服务器.取(1)
        }
    }()
}

// ...

平衡:=服务器.平衡()
如果平衡 != 10 {
    b.错误(“余额不是10美元:”,余额)
}

这次我们真的可以预测结果了.

$ GOMAXPROCS=4 go测试台 . 资金
benchmark - withdraw -4—FAIL: benchmark - withdraw -4
    基金_test.余额不是10美元
好的,资金0.009s

我们回到了开始的地方——几个工作人员可以同时读取余额,然后所有人都更新它. 为了解决这个问题,我们可以在基金中加入一些逻辑,比如a minimum平衡 属性,或者添加另一个名为 撤回IfOverXDollars. 这两个想法都很糟糕. 我们的协议是我们之间的,不是基金的财产. 我们应该将其保留在应用程序逻辑中.

我们真正需要的是 Transactions,就像数据库事务一样. 由于我们的服务一次只执行一个命令,所以这非常简单. 我们将添加 办理 包含回调(闭包)的命令. 服务器将在自己的例程中执行该回调,并传入raw 基金. 然后回调函数就可以安全地对 基金.

信号量和错误

在下一个例子中,我们做错了两件小事.

第一个, 我们使用' 完成 '通道作为信号量,让调用代码知道它的事务何时完成. 这很好,但是为什么通道类型是' bool '? 我们只会发送“true”来表示“done”(发送“false”是什么意思呢??). 我们真正想要的是一个单状态值(一个没有值的值?). 在Go中,我们可以使用空的结构体类型来实现这一点:. 这还具有使用更少内存的优点. 在这个例子中,我们将坚持使用' bool ',以免看起来太吓人.

第二,我们的事务回调没有返回任何东西. 正如我们稍后将看到的,我们可以使用作用域技巧将值从回调中获取到调用代码中. 然而, 真实系统中的事务有时可能会失败, 所以Go约定是让事务返回一个“错误”(然后检查它是否在调用代码中为“nil”).

我们现在也不这么做,因为我们没有任何错误要生成.
//为可读性定义回调函数
类型Transactions人(基金*基金)

//添加一个带有回调和信号量通道的新命令类型
类型办理ionComm和 struct {
    办理人办理人
    完成 chan bool
}

// ...

//像其他命令一样,将它整齐地封装在API方法中
func (s *基金Server) 事务 (transactor) {
    命令:= 办理ionComm和{
        办理人:办理人,
        完成:make(chan bool),
    }
    s.comm和s <- comm和
    <- comm和.完成
}

// ...

func (s *基金Server)循环(){
    对于命令:= range s.命令{
        开关命令.(类型){
        // ...

        案例办理ionComm和:
            事务:=命令.(办理ionComm和)
            事务.办理人的.基金)
            事务.完成 <- true

        // ...
        }
    }
}

我们的事务回调不直接返回任何东西, 但是Go语言使得直接从闭包中获取值变得很容易, 我们会在基准测试中这样做 pizzaTime 钱不够用时的标志:

pizzaTime:= false
for i := 0; i < dollarsPerFounder; i++ {

    服务器.Transactions(func(基金*基金){
        如果基金.平衡() <= 10 {
            //在外部作用域中设置它
            pizzaTime = true
            返回
        }
        基金.取(1)
    })

    如果pizzaTime {
        打破
    }
}

并检查它是否有效:

$ GOMAXPROCS=4 go测试台 . 资金
基准提款-4 500万775 ns/op
好的,资金4.637s

只有Transactions

你可能已经发现了一个机会来进一步清理. 因为我们有通用的 办理 命令,我们不需要 撤回Comm和 or 平衡Comm和 了. 我们用Transactions的形式重写它们:

func (s *基金Server) 平衡() int {
    Var余额
    s.Transactions(函数(f *基金){
        余额= f.平衡()
    })
    恢复平衡
}

func (s *基金Server)提取(金额int) {
    s.Transactions(函数(f *基金){
        f.收回(数量)
    })
}

现在服务器接受的唯一命令是 办理ionComm和,所以我们可以把整个 接口{} 在它的实现中混乱,并让它只接受事务命令:

类型基金Server struct {
    命令chan 办理ionComm和
    基金*基金
}

func (s *基金Server)循环(){
    对于事务:=范围s.命令{
        //现在我们不需要任何类型切换的混乱
        事务.办理人的.基金)
        事务.完成 <- true
    }
}

更好的.

我们还可以做最后一步. 除了它的便利功能 平衡撤回,服务实现不再绑定到 基金. 而不是管理 基金,它可以管理一个 接口{} 并用于包装 任何东西. 但是,每个事务回调都必须转换 接口{} 回到实际值:

类型Transactions函数(接口{})

服务器.事务(func(managedValue接口{}){
    基金:= managedValue.(*基金)
    //用资金做事 ...
})

这是丑陋且容易出错的. 我们真正需要的是编译时泛型, 因此,我们可以为特定类型(例如:)“模板”出服务器 *基金).

不幸的是,Go还不支持泛型. 预计最终会到达, 一旦有人为它找出一些合理的Golang语法和语义. 与此同时, 仔细的接口设计通常会消除对泛型的需求, 如果没有,我们可以使用类型断言(在运行时检查)。.

我们结束了吗??

是的.

好吧,不.

例如:

  • 事务中的恐慌将会终止整个服务.

  • 没有暂停. 永远不会返回的事务将永远阻塞服务.

  • 如果我们的基金增加了一些新的字段,并且在更新它们的过程中Transactions崩溃了, 我们会有不一致状态.

  • 事务能够泄漏托管的数据 基金 对象,这不是很好.

  • 没有合理的方法可以跨多个基金进行Transactions(比如从一个基金中取款,在另一个基金中存款)。. 我们不能只嵌套事务,因为这会导致死锁.

  • 异步运行一个事务现在需要一个新的程序和大量的混乱. 与此相关的是,我们可能希望能够读到最新的 基金 状态,而长时间运行的事务正在进行中.

在下一篇Go编程语言教程中,我们将探讨解决这些问题的一些方法.

了解基本知识

  • Go语言是用什么编写的?

    Go编程语言规范是一份用英语编写的文档, 而Go的标准库和编译器是用Go自己编写的.

  • Go是用来做什么的?

    Go是一种通用编程语言,可用于各种用途. Go可以在后端用作web服务器, 并被用于构建码头工人, Kubernetes, 和Heroku CLI.

  • 哪些公司正在使用Go?

    Go被许多著名的科技公司使用, 最著名的是谷歌, Dropbox, 码头工人, Kubernetes, Heroku, 还有更多.

聘请Toptal这方面的专家.
现在雇佣
布兰登·霍格的头像
布兰登火车司机
验证专家 in 工程

位于 台北,台湾

成员自 2013年10月16日

作者简介

Brendon在设计和交付从社交网络初创公司到企业电信解决方案的软件方面有15年以上的经验.

Toptal作者都是各自领域经过审查的专家,并撰写他们有经验的主题. 我们所有的内容都经过同行评审,并由同一领域的Toptal专家验证.

专业知识

以前在

爱立信

世界级的文章,每周发一次.

订阅意味着同意我们的 隐私政策

世界级的文章,每周发一次.

订阅意味着同意我们的 隐私政策

Toptal开发者

加入总冠军® 社区.