众所周知,字节跳动内部的后端开发大多数都是使用 go 语言的,那么一般 go 语言的面试会问哪些问题?
这个一般分为两个层次,初中级开发(1-1、1-2)和高级开发(2-1、2-2),不同级别的面试一般要求是不一样的。对于初中级开发,一般会问一些语言层面的东西,一些常用的基础原理和一些算法,但是高级开发就没那么简单了。下面我为读者分享一段面试的经历。
面试官:你平常使用什么编程语言比较多?
面试者:go。
面试官:好的,那我们聊一下一些 go 相关的问题吧。
面试者:好的。
面试官:用过 fallthrough 关键字吗?这个关键字的作用是什么?
面试者:其他语言中,switch-case 结构中一般都需要在每个 case 分支结束处显式的调用 break 语句以防止 前一个 case 分支被贯穿后调用下一个 case 分支的逻辑,go 编译器从语法层面上消除了这种重复的工作,让开发者更轻松;但有时候我们的场景就是需要贯穿多个 case,但是编译器默认是不贯穿的,这个时候 fallthrough 就起作用了,让某个 case 分支再次贯穿到下一个 case 分支。
面试官:go 中除了加 Mutex 锁以外还有哪些方式安全读写共享变量?
面试者:go 中 Goroutine 可以通过 Channel 进行安全读写共享变量。
面试官:无缓冲 Chan 的发送和接收是否同步?
面试者:举两个例子:
// 无缓冲的channel由于没有缓冲发送和接收需要同步.
ch := make(chan int)
//有缓冲channel不要求发送和接收操作同步.
ch := make(chan int, 2)因此 channel 无缓冲时,发送阻塞直到数据被接收,接收阻塞直到读到数据;channel有缓冲时,当缓冲满时发送阻塞,当缓冲空时接收阻塞。
面试官:请谈一谈 go 语言的并发机制以及它所使用的CSP并发模型。
面试者:CSP 模型是上个世纪七十年代提出的,不同于传统的多线程通过共享内存来通信,CSP 讲究的是“以通信的方式来共享内存”。用于描述两个独立的并发实体通过共享的通讯 channel (管道)进行通信的并发模型。CSP 中 channel 是第一类对象,它不关注发送消息的实体,而关注与发送消息时使用的 channel。
go 中 channel 是被单独创建并且可以在进程之间传递,它的通信模式类似于 boss-worker 模式的,一个实体通过将消息发送到 channel 中,然后又监听这个 channel 的实体处理,两个实体之间是匿名的,这个就实现实体中间的解耦,其中 channel 是同步的一个消息被发送到 channel 中,最终是一定要被另外的实体消费掉的,在实现原理上其实类似一个阻塞的消息队列。
Goroutine 是 go 实际并发执行的实体,它底层是使用协程(coroutine)实现并发,coroutine 是一种运行在用户态的用户线程,类似于 greenthread,go 底层选择使用 coroutine 的出发点是因为,它具有以下特点:
用户空间 避免了内核态和用户态的切换导致的成本。
可以由语言和框架层进行调度。
更小的栈空间允许创建大量的实例。
go 中的 Goroutine 的特性:
Golang 内部有三个对象:P 对象(processor) 代表上下文(或者可以认为是 CPU),M(work thread) 代表工作线程,G 对象(goroutine)。
正常情况下一个 CPU 对象启一个工作线程对象,线程去检查并执行 goroutine 对象。碰到 goroutine 对象阻塞的时候,会启动一个新的工作线程,以充分利用cpu资源。所有有时候线程对象会比处理器对象多很多。
G(Goroutine):我们所说的协程,为用户级的轻量级线程,每个Goroutine对象中的sched保存着其上下文信息.
M(Machine):对内核级线程的封装,数量对应真实的CPU数(真正干活的对象).
P(Processor):即为G和M的调度对象,用来调度G和M之间的关联关系,其数量可通过 GOMAXPROCS() 来设置,默认为核心数.
在单核情况下,所有Goroutine运行在同一个线程(M0)中,每一个线程维护一个上下文(P),任何时刻,一个上下文中只有一个Goroutine,其他Goroutine在runqueue中等待。
一个 Goroutine 运行完自己的时间片后,让出上下文,自己回到 runqueue中。
当正在运行的G0阻塞的时候(可以需要IO),会再创建一个线程(M1),P转到新的线程中去运行。
当 M0 返回时,它会尝试从其他线程中“偷”一个上下文过来,如果没有偷到,会把 Goroutine 放到 Global runqueue 中去,然后把自己放入线程缓存中。上下文会定时检查Global runqueue。
go 的 CSP 并发模型,是通过 Goroutine 和 Channel 来实现的。Goroutine 是 go 语言中并发的执行单位。有点抽象,其实就是和传统概念上的”线程“类似,可以理解为”线程“。Channel 是 go 语言中各个并发结构体(Goroutine)之前的通信机制。通常 Channel,是各个 Goroutine 之间通信的”管道“,有点类似于Linux中的管道。通信机制channel也很方便,传数据用channel <- data,取数据用<-channel。在通信过程中,传数据channel <- data和取数据<-channel必然会成对出现,因为这边传,那边取,两个goroutine之间才会实现通信。而且不管传还是取,必阻塞,直到另外的goroutine传或者取为止。
面试官:嗯,不错,了解的很深入。那 go 中有哪些常用的并发模型?
面试者:Golang 中常用的并发模型有三种:
通过channel通知实现并发控制
无缓冲的通道指的是通道的大小为0,也就是说,这种类型的通道在接收前没有能力保存任何值,它要求发送 goroutine 和接收 goroutine 同时准备好,才可以完成发送和接收操作。
从上面无缓冲的通道定义来看,发送 goroutine 和接收 gouroutine 必须是同步的,同时准备后,如果没有同时准备好的话,先执行的操作就会阻塞等待,直到另一个相对应的操作准备好为止。这种无缓冲的通道我们也称之为同步通道。
func main() {
ch := make(chan struct{})
go func() {
fmt.Println("start working")
time.Sleep(time.Second * 1)
ch <- struct{}{}
}()
<-ch
fmt.Println("finished")
}func main(){
var wg sync.WaitGroup
var urls = []string{
"http://www.golang.org/",
"http://www.google.com/",
}
for _, url := range urls {
wg.Add(1)
go func(url string) {
defer wg.Done()
http.Get(url)
}(url)
}
wg.Wait()
}func main(){
wg := sync.WaitGroup{}
for i := 0; i < 5; i++ {
wg.Add(1)
go func(wg sync.WaitGroup, i int) {
fmt.Printf("i:%d", i)
wg.Done()
}(wg, i)
}
wg.Wait()
fmt.Println("exit")
}i:1i:3i:2i:0i:4fatal error: all goroutines are asleep - deadlock!
goroutine 1 [semacquire]:
sync.runtime_Semacquire(0xc000094018)
/home/keke/soft/go/src/runtime/sema.go:56 +0x39
sync.(*WaitGroup).Wait(0xc000094010)
/home/keke/soft/go/src/sync/waitgroup.go:130 +0x64
main.main()
/home/keke/go/Test/wait.go:17 +0xab
exit status 2// A Context carries a deadline, cancelation signal, and request-scoped values
// across API boundaries. Its methods are safe for simultaneous use by multiple
// goroutines.
type Context interface {
// Done returns a channel that is closed when this `Context` is canceled
// or times out.
Done() <-chan struct{}
// Err indicates why this Context was canceled, after the Done channel
// is closed.
Err() error
// Deadline returns the time when this Context will be canceled, if any.
Deadline() (deadline time.Time, ok bool)
// Value returns the value associated with key or nil if none.
Value(key interface{}) interface{}
}一个 Context 不能拥有 Cancel 方法,同时我们也只能 Done channel 接收数据。其中的原因是一致的:接收取消信号的函数和发送信号的函数通常不是一个。典型的场景是:父操作为子操作操作启动 goroutine,子操作也就不能取消父操作。
Context 对象是线程安全的,你可以把一个 Context 对象传递给任意个数的 gorotuine,对它执行 取消 操作时,所有 goroutine 都会接收到取消信号。
Value() 方法允许 Context 对象携带request作用域的数据,该数据必须是线程安全的。
Deadline() 设置该context cancel的时间点
Err() 在Done() 之后,返回context 取消的原因。
Done() 返回一个只能接受数据的channel类型,当该context关闭或者超时时间到了的时候,该channel就会有一个取消信号
context 包的核心是 struct Context,接口声明如下:
context 包主要是用来处理多个 goroutine 之间共享数据,及多个 goroutine 的管理。
通常,在一些简单场景下使用 channel 和 WaitGroup 已经足够了,但是当面临一些复杂多变的网络并发场景下 channel 和 WaitGroup 显得有些力不从心了。比如一个网络请求 Request,每个 Request 都需要开启一个 goroutine 做一些事情,这些 goroutine 又可能会开启其他的 goroutine,比如数据库和RPC服务。所以我们需要一种可以跟踪 goroutine 的方案,才可以达到控制他们的目的,这就是Go语言为我们提供的 Context,称之为上下文非常贴切,它就是goroutine 的上下文。它是包括一个程序的运行环境、现场和快照等。每个程序要运行时,都需要知道当前程序的运行状态,通常Go 将这些封装在一个 Context 里,再将它传给要执行的 goroutine 。
在Go 1.7 以后引进的强大的Context上下文,实现并发控制
这个第一个修改方式:将匿名函数中 wg 的传入类型改为 *sync.WaitGrou,这样就能引用到正确的WaitGroup了。这个第二个修改方式:将匿名函数中的 wg 的传入参数去掉,因为Go支持闭包类型,在匿名函数中可以直接使用外面的 wg 变量
因此 Wait 就死锁了。
它提示所有的 goroutine 都已经睡眠了,出现了死锁。这是因为 wg 给拷贝传递到了 goroutine 中,导致只有 Add 操作,其实 Done操作是在 wg 的副本执行的。
在Golang官网中对于WaitGroup介绍是
A WaitGroup must not be copied after first use,在 WaitGroup 第一次使用后,不能被拷贝在主 goroutine 中 Add(delta int) 索要等待goroutine 的数量。在每一个 goroutine 完成后 Done() 表示这一个goroutine 已经完成,当所有的 goroutine 都完成后,在主 goroutine 中 WaitGroup 返回返回。
Add, 可以添加或减少 goroutine的数量.
Done, 相当于Add(-1).
Wait, 执行后会堵塞主线程,直到WaitGroup 里的值减至0.
Goroutine是异步执行的,有的时候为了防止在结束mian函数的时候结束掉Goroutine,所以需要同步等待,这个时候就需要用 WaitGroup了,在 sync 包中,提供了 WaitGroup ,它会等待它收集的所有 goroutine 任务全部完成。在WaitGroup里主要有三个方法:
通过sync包中的WaitGroup实现并发控制
当主 goroutine 运行到 <-ch 接受 channel 的值的时候,如果该 channel 中没有数据,就会一直阻塞等待,直到有值。这样就可以简单实现并发控制
面试官:Golang GC 有了解吗?GC 时会发生什么?
面试者:内存管理是程序员开发应用的一大难题。传统的系统级编程语言(主要指C/C++)中,程序开发者必须对内存小心的进行管理操作,控制内存的申请及释放。因为稍有不慎,就可能产生内存泄露问题,这种问题不易发现并且难以定位,一直成为困扰程序开发者的噩梦。如何解决这个头疼的问题呢?
过去一般采用两种办法:
内存泄露检测工具。这种工具的原理一般是静态代码扫描,通过扫描程序检测可能出现内存泄露的代码段。然而检测工具难免有疏漏和不足,只能起到辅助作用。
智能指针。这是 c++ 中引入的自动内存管理方法,通过拥有自动内存管理功能的指针对象来引用对象,程序员不用太关注内存的释放,而达到内存自动释放的目的。这种方法是采用最广泛的做法,但是对程序开发者有一定的学习成本(并非语言层面的原生支持),而且一旦有忘记使用的场景依然无法避免内存泄露。
为了解决这个问题,后来开发出来的几乎所有新语言(java,python,php等等)都引入了语言层面的自动内存管理 – 也就是语言的使用者只用关注内存的申请而不必关心内存的释放,内存释放由虚拟机(virtual machine)或运行时(runtime)来自动进行管理。而这种对不再使用的内存资源进行自动回收的行为就被称为垃圾回收。
常用的垃圾回收的方法:
引用计数(reference counting)
这是最简单的一种垃圾回收算法,和之前提到的智能指针异曲同工。对每个对象维护一个引用计数,当引用该对象的对象被销毁或更新时被引用对象的引用计数自动减一,当被引用对象被创建或被赋值给其他对象时引用计数自动加一。当引用计数为0时则立即回收对象。
这种方法的优点是实现简单,并且内存的回收很及时。这种算法在内存比较紧张和实时性比较高的系统中使用的比较广泛,如ios cocoa框架,php,python等。
但是简单引用计数算法也有明显的缺点:
频繁更新引用计数降低了性能。
一种简单的解决方法就是编译器将相邻的引用计数更新操作合并到一次更新;还有一种方法是针对频繁发生的临时变量引用不进行计数,而是在引用达到0时通过扫描堆栈确认是否还有临时对象引用而决定是否释放,等等还有很多其他方法。
2. 循环引用。
当对象间发生循环引用时引用链中的对象都无法得到释放。最明显的解决办法是避免产生循环引用,如cocoa引入了strong指针和weak指针两种指针类型。或者系统检测循环引用并主动打破循环链。当然这也增加了垃圾回收的复杂度。
标记-清除(mark and sweep)
标记-清除(mark and sweep)分为两步,标记从根变量开始迭代到遍历所有被引用的对象,对能够通过应用遍历访问到的对象都进行标记为“被引用”;标记完成后进行清除操作,对没有标记过的内存进行回收(回收同时可能伴有碎片整理操作)。这种方法解决了引用计数的不足,但是也有比较明显的问题:每次启动垃圾回收都会暂停当前所有的正常代码执行,回收使系统响应能力大大降低!当然后续也出现了很多mark&sweep算法的变种(如三色标记法)优化了这个问题。
分代搜集(generation)
java的jvm 就使用的分代回收的思路。在面向对象编程语言中,绝大多数对象的生命周期都非常短。分代收集的基本思想是,将堆划分为两个或多个称为代(generation)的空间。新创建的对象存放在称为新生代(young generation)中(一般来说,新生代的大小会比 老年代小很多),随着垃圾回收的重复执行,生命周期较长的对象会被提升(promotion)到老年代中(这里用到了一个分类的思路,这个是也是科学思考的一个基本思路)。
因此,新生代垃圾回收和老年代垃圾回收两种不同的垃圾回收方式应运而生,分别用于对各自空间中的对象执行垃圾回收。新生代垃圾回收的速度非常快,比老年代快几个数量级,即使新生代垃圾回收的频率更高,执行效率也仍然比老年代垃圾回收强,这是因为大多数对象的生命周期都很短,根本无需提升到老年代。
Golang 1.5后,采取的是“非分代的、非移动的、并发的、三色的”标记清除垃圾回收算法。
golang 中的 gc 基本上是标记清除的过程:
gc的过程一共分为四个阶段:
栈扫描(开始时STW)
第一次标记(并发)
第二次标记(STW)
清除(并发)
整个进程空间里申请每个对象占据的内存可以视为一个图,初始状态下每个内存对象都是白色标记。
先STW,做一些准备工作,比如 enable write barrier。然后取消STW,将扫描任务作为多个并发的goroutine立即入队给调度器,进而被CPU处理
第一轮先扫描root对象,包括全局指针和 goroutine 栈上的指针,标记为灰色放入队列
第二轮将第一步队列中的对象引用的对象置为灰色加入队列,一个对象引用的所有对象都置灰并加入队列后,这个对象才能置为黑色并从队列之中取出。循环往复,最后队列为空时,整个图剩下的白色内存空间即不可到达的对象,即没有被引用的对象;
第三轮再次STW,将第二轮过程中新增对象申请的内存进行标记(灰色),这里使用了write barrier(写屏障)去记录
Golang gc 优化的核心就是尽量使得 STW(Stop The World) 的时间越来越短。
面试官:问个小细节, JSON 标准库对 nil slice 和 空 slice 的处理是一致的吗?
面试者:首先 JSON 标准库对 nil slice 和 空 slice 的处理是不一致的。
通常错误的用法,会报数组越界的错误,因为只是声明了slice,却没有给实例化的对象。
var slice []int
slice[1] = 0slice := make([]int,0)
slice := []int{}总之,nil slice 和 empty slice是不同的东西,需要我们加以区分的。
当我们查询或者处理一个空的列表的时候,这非常有用,它会告诉我们返回的是一个列表,但是列表内没有任何值。
empty slice 是指slice不为nil,但是slice没有值,slice的底层的空间是空的,此时的定义如下:
此时slice的值是nil,这种情况可以用于需要返回slice的函数,当函数出现异常的时候,保证函数依然会有nil的返回值。
面试官:了解过选项模式吗?能否写一段代码实现一个函数选项模式?
面试者:
var defaultStuffClientOptions = StuffClientOptions{
Retries: 3,
Timeout: 2,
}
type StuffClientOption func(*StuffClientOptions)
type StuffClientOptions struct {
Retries int //number of times to retry the request before giving up
Timeout int //connection timeout in seconds
}
func WithRetries(r int) StuffClientOption {
return func(o *StuffClientOptions) {
o.Retries = r
}
}
func WithTimeout(t int) StuffClientOption {
return func(o *StuffClientOptions) {
o.Timeout = t
}
}
type StuffClient interface {
DoStuff() error
}
type stuffClient struct {
conn Connection
timeout int
retries int
}
type Connection struct {}
func NewStuffClient(conn Connection, opts ...StuffClientOption) StuffClient {
options := defaultStuffClientOptions
for _, o := range opts {
o(&options)
}
return &stuffClient{
conn: conn,
timeout: options.Timeout,
retries: options.Retries,
}
}
func (c stuffClient) DoStuff() error {
return nil
}面试官:写的不错,请说说选项者模式的优点。
面试者:选项模式是 go 语法所特有的,也是 go 语言的创始人所推崇的,可以做到灵活的给接口提供参数,且参数的数量可以自定义,同时屏蔽了一些不需要对接口使用者的细节。
当然,任何一门语言入门不难,学好却不容易。我给大家推荐极客时间的 Go 进阶训练营,这个课程,目标是让你在 3 个月时间内,快速掌握、吃透 Go 语言。

01 讲师是谁?
讲师叫毛剑,目前在一家大型互联网视频内容公司担任资深总监和技术专家的职位。
讲师曾参与了大型巨石架构到微服务的完整转型,他们使用 Go 语言作为主要的开发语言,构建了微服务的框架,同时,围绕分布式服务的可用性和数据一致性进行了大量的设计,并结合开源生态补齐了 Go 在微服务可视化上的功能。在演进架构的同时,他们还使用 Go 语言开发了配套的中间件,比如:消息队列、缓存代理、对象存储等等。
这十多年来,毛剑主要专注于服务端研发以及高性能、高可用的系统架构设计这些领域,对 Go 语言算得上是相当熟悉,他开源的行业内比较有影响力的 Go 项目有:goim 分布式 IM 长连接广播服务、bfs 分布式小文件存储以及 Kratos Go 微服务框架。
毛老师是忠实的 Go 语言粉丝,也是一名 Go 语言老手,曾在 GitHub 上开源了几个基于 Go 语言的项目,如果你想了解他的技术实力,可以点进去看看,毕竟,Talk is cheap,Show you the code。毛剑的 GitHub 主页:https://github.com/Terry-Mao
作为 Go 语言的布道师,毛剑也经常参加和 Go 有关的社区分享和演讲,包括 QCon、ArchSummit、Gopher China,GITC 等等这些业界知名的会议。
作为公司的技术专家,他还负责公司的 Go 工程师招聘,所以非常了解一线互联网公司对于 Go 工程师的能力要求,以及 Go 工程师在成长过程中,可能会遇到的问题。
02 课程内容以及设计思路是什么?
整个训练营的内容,可以分为三大模块,先是 Go 语言编程实践、再到工程实践、最后是架构实践。
每个模块下面又可以分为 4 到 5 个子模块,当然,每个子模块又会细分出篇幅不等的更小的模块,总体算下来,差不多会有四五十个细分模块。
我们先说第一个大模块,Go 语言编程实践,会先从语言本身来讲 Runtime 的原理,包括 Goroutine、Channel 和 GC 等等,只有了解了这几个最关键的 Go 语言特性,才能让我们更好地开展,如:并发编程、网络编程以及异常处理等这些子模块。
讲完 Go 语言本身的重点和难点之后,会开始讲 Go 语言的工程化实践,工程化意味着我们需要大规模落地到业务开发上,好的项目规范也更利于长期的迭代。这里面呢就包括如何设计项目结构,如何进行 API 设计和包设计,还有配置文件和单元测试等等工程化环节需要解决的重点问题。
最后一个大模块,就是架构设计了,我们会讲到当前最流行的分布式架构和微服务架构,当然,也少不了相关的中间件的讲解。
我们从 Go 语言实践到工程实践,最后到一个完整的大型分布式系统的实践,由浅到深,从语言到工程,从工程到架构,每个子模块毛剑都会结合他这近 10 年的项目经验和积累来进行案例讲解,而不仅仅是单纯讲述理论知识。因为从实践中出理论,从理论践行实践,一直是他学习成长的模式。


滑动查看课程大纲👆
03 上课形式和课时量?
Go 进阶训练营将全程直播授课的形式进行教学,每周四晚上 8:00-10:00、周六晚上 7:00-10:00 进行直播,共 13 周。期间有任何问题可以随时与讲师互动,同时我们会通过助教答疑、随堂自测、阶段考试、不定期分享等帮你巩固学习。
04 训练营的学习服务有哪些?
考虑到大家在学习过程中经常遇到的障碍,比如“遇到问题无法及时解决、拖延症、缺乏实战”等等,毛剑在训练营中也专门设计了相关的学习服务:

欢迎你加入这个关注长期成长的社区,让终身学习成为成长过程中的一种好习惯。
在「Go 进阶训练营」,学员、老师、助教、平台组成了一个有机整体,毛剑老师不希望加入这个有机体的个人,学到一半走散了,还没有成功就走散了,所以他和极客时间会努力建设一个让学员能够长期发展和终身学习的“社区”。在这个社区里,大家可以长期互动,资源和信息充分交流共享,最终实现共同成功。
05 学完你能到什么水平?
Go 进阶训练营课程设计对标字节跳动 2-2 级胜任力模型,教学贴近大厂真实场景。课程中的实践驱动,不仅可以系统提升你的代码硬实力,同时也会培养你进入一线互联网大厂必备的思维能力。
课程培养目标是大厂的资深 Go 工程师、高级 Go 开发工程师、Go 技术专家。按照老师系统性的规划,用心学习,可以帮你摆脱低效和痛苦的自学,事半功倍,学完后可以达到一线互联网大厂中高级 Go 开发工程师的水平。
06 训练营有就业服务吗?
当然有,极客时间团队会给每位毕业之后有跳槽需求的同学,一年内提供两次互联网大厂、600+ 鲲鹏会企业的内推服务。这项服务不局限北上广深,任何地区都可以。帮助打通你到一线大厂的最后一公里,拿到心仪的工作机会,到更好的公司拿更高的薪水。
07 如何加入 Go 进阶训练营?
「Go 进阶训练营」第 0 期,采用全程直播授课的形式,将用 13 周时间,带你掌握云计算时代首选编程语言,成为未来 3 年抢手的后端开发人才。
扫描下方二维码,进入报名入口,现在报名享受¥50 优惠,报名立减。

08 不管你报不报名这个训练营,最后送一波免费的优质资料
扫码添加运营小姐姐悠悠,可以了解更多课程详情。
同时我们还准备了一些见面礼,大家可以免费领取,里面有:
-
毛剑老师精选 Go 预习资料 -
Golang 高清技术图谱 -
Linux 基础知识体系图 -
100+ 大厂软件开发案例
可以帮助你建立完整的知识体系,规划具体的学习路线,同时夯实你的 Linux 基础;还可获得阿里、美团等一线互联网公司独家案例拆解。

免费领取 Go 技术图谱等资料👆
09 极客时间大型公开直播推荐
近几年,大厂对 Go 工程师的需求数量逐渐增大,你是否也蠢蠢欲动地想要加入?但是该如何通过面试?面试官会问哪些问题?大厂的 Go 工程师需要构建怎样的能力模型?
本周日,极客时间大型公开直播,左耳朵耗子陈皓将为你分享他眼中的 Go 语言,以及为什么 Go 语言是新一代的云计算技术。
毛剑将站在面试官的角度为你分享大厂面试 Go 工程师更看重哪些能力。
同时,字节跳动抖音架构团队资深面试官也将莅临现场,为你分享字节团队的工作氛围,解读 Go 工程师的招聘 JD。现场更会开启简历直投通道,让你的简历不再石沉大海,只要你够优秀,就能快速获得字节跳动的面试机会。
同时,周日晚直播间将进行 3 波抽奖,奖品非常丰厚:Cherry 机械键盘、索尼蓝牙耳机、小爱同学音箱、红米手环、小米电动牙刷、Go 经典书籍等等。

扫码预约大型公开直播👆
👇一起成为未来 3 年抢手的后端开发人才

