Текущий опыт компании основан на платформе Beego, и для бесперебойного обновления услуг мы включили модуль Beego Grace для поддержки горячего обновления. Все работало нормально, пока однажды лидер не выкинул несколько служебных логов, сказав программе продолжать сообщать об ошибках:
2018/03/08 17:49:34 20848 Received SIGINT.
2018/03/08 17:49:34 20848 [::]:5490 Listener closed.
2018/03/08 17:49:34 20848 Waiting for connections to finish...
2018/03/08 17:49:34 [C] [asm_amd64.s:2337] ListenAndServe: accept tcp [::]:5490: use of closed network connection 20848
Проблема в строке 4, каждый раз, когда служба закрывается, сообщается об использовании закрытого сетевого подключения. Само собой разумеется, что сетевое соединение должно быть закрыто в это время, и процесс завершился.Как я могу все еще принимать порт 5490? Выполните поиск в списке проблем Beego, этот вопрос уже задавался (#2809), ниже никто не ответил, и поиском не найти, остался только последний инструмент: посмотреть исходный код.
1. Льготный режим
Во-первых, несомненно, что если вы не включите Grace-режим, то эти логи не будут набираться, а будут заканчиваться напрямую. Итак, сначала нам нужно иметь некоторое представление о режиме Grace Beego. На официальном сайте Beego есть некоторое представление об этом:Льготный модуль. Грубо говоря, они относятся к:Grace_restart_in_golangИдея этой статьи реализует функцию горячего обновления.Статья очень длинная,и идеи очень понятны.Общий процесс выглядит следующим образом:
Китайский перевод с открытым исходным кодом — GracefulRestartЭто объяснение китайского перевода легче понять. Чтобы понять принцип горячего обновления, мы можем подробно изучить код. все отbeego.Run()
Начинать.
beego.Run()
Создайте объект BeeApp и вызовитеBeeApp.Run()
воплощать в жизнь. У метода Run есть разные режимы запуска, здесь мы сосредоточимся только на части Grace.
func (app *App) Run() {
addr := BConfig.Listen.HTTPAddr
...
// run graceful mode
if BConfig.Listen.Graceful {
...
if BConfig.Listen.EnableHTTP {
go func() {
// 创建了GraceServer 是对http.Server的一层封装
server := grace.NewServer(addr, app.Handlers)
...
if err := server.ListenAndServe(); err != nil {
logs.Critical("ListenAndServe: ", err, fmt.Sprintf("%d", os.Getpid()))
endRunning <- true
}
}()
}
<-endRunning
return
}
...
}
Вы можете видеть в коде,logs.Critical("ListenAndServe: ", err, fmt.Sprintf("%d", os.Getpid()))
Это источник, который печатает приведенный выше журнал:
2018/03/08 17:49:34 [C] [asm_amd64.s:2337] ListenAndServe: accept tcp [::]:5490: use of closed network connection 20848
тогда почему он возвращаетсяuse of closed network connection
Как насчет этой ошибки? следовать заListenAndServe()
Посмотреть в методе:
func (srv *Server) ListenAndServe() (err error) {
...
// 处理上图中的热升级信号(fork子进程),SIGINT、SIGTERM信号(进程结束信号)
go srv.handleSignals()
...
// 如果是子进程执行,Getppid()拿到父进程pid,并且Kill
if srv.isChild {
process, err := os.FindProcess(os.Getppid())
if err != nil {
log.Println(err)
return err
}
err = process.Kill()
if err != nil {
return err
}
}
log.Println(os.Getpid(), srv.Addr)
return srv.Serve()
}
следовать заServe()
метод:
func (srv *Server) Serve() (err error) {
srv.state = StateRunning
//这里我们传入了一个GraceListener,对net.Listener做了封装,在后面会用到。
err = srv.Server.Serve(srv.GraceListener)
log.Println(syscall.Getpid(), "Waiting for connections to finish...")
//此处会等待所有连接处理完成,对应图中的父进程结束流程。
srv.wg.Wait()
srv.state = StateTerminate
return
}
все равно перезвонилhttp.Server.Serve()
метод, см. этот метод:
func (srv *Server) Serve(l net.Listener) error {
defer l.Close()
...
for {
//代码在这里阻塞,如果没有连接进来的话。
rw, e := l.Accept()
if e != nil {
select {
//正常的结束流程
case <-srv.getDoneChan():
return ErrServerClosed
default:
}
...
//不正常的结束流程
return e
}
//开启一个go程处理新连接
c := srv.newConn(rw)
go c.serve(ctx)
}
}
Если это нормальный конец, мы должны получитьErrServerClosed
, хоть это и Ошибка, но похоже наio.EOF
, это нормальный процесс закрытия, проблема в том, что мы получаем неServerClosed
, ноuse of closed connection
,Зависит отl.Accept()
метод возвращает, то мы вводимAccept()
проверить это.
2.Accept
семья
Я сказал раньше,l.Accept()
серединаl
ЯвляетсяGraceListener
, то мы идем прямо, чтобы увидеть егоAccept()
метод.
func (gl *graceListener) Accept() (c net.Conn, err error) {
//调AcceptTCP()
tc, err := gl.Listener.(*net.TCPListener).AcceptTCP()
if err != nil {
return
}
...
//每次新来一个连接+1,当连接处理完成时-1。 前面wg.Wait()等的就是这个值减为0。
gl.server.wg.Add(1)
return
}
все равно перезвонилnet.TCPListener
изAcceptTCP()
,идти сTCPListener
см. это нижеAcceptTCP()
:
func (l *TCPListener) AcceptTCP() (*TCPConn, error) {
...
c, err := l.accept()
if err != nil {
return nil, &OpError{Op: "accept", Net: l.fd.net, Source: nil, Addr: l.fd.laddr, Err: err}
}
return c, nil
}
Здесь мы видим, чтоOpError
, который имеет видaccept tcp [::]:5490: use of closed network connection
показано, сaccept
начни, естьnet
,addr
информация и еще одноErr
Вроде в пакете ошибки нет.Ошибка высылается отсюда.l.accept()
Взгляни:
func (ln *TCPListener) accept() (*TCPConn, error) {
fd, err := ln.fd.accept()
if err != nil {
return nil, err
}
//创建了新的TCP连接
return newTCPConn(fd), nil
}
Настроено здесьfd.accept()
Это требует небольшого знания системы UNIX,fd
которыйfile descriptor
(файловый дескриптор), в UNIX все является файлом, соединение Socket, процесс можно рассматривать как файл, технология горячего обновления, которую мы представили на предыдущем рисунке, причина, по которой дочерний процесс может получить родительский Socket соединение процесса также является родительским процессом в процессе разветвления дочернего процесса, и файл его собственного соединения Socket передается дочернему процессу в качестве параметра запуска, чтобы дочерний процесс мог принять новый запрос через этот файл, мы напрямую входимln.fd.accept()
посмотри на этоfd
В чем загадка:
func (fd *netFD) accept() (netfd *netFD, err error) {
d, rsa, errcall, err := fd.pfd.Accept()
if err != nil {
if errcall != "" {
err = wrapSyscallError(errcall, err)
}
//有可能
return nil, err
}
if netfd, err = newFD(d, fd.family, fd.sotype, fd.net); err != nil {
poll.CloseFunc(d)
//有可能
return nil, err
}
if err = netfd.init(); err != nil {
fd.Close()
//有可能
return nil, err
}
...
return netfd, nil
}
Приведенные выше три кода могут возвращатьerr
, Запустив службу локально, обнаруживается, что после запуска службы соединение не приходит, (то есть код теперь находится вAccept()
блок, и не перешел к следующему потоку управления), то отправьте ему сигнал SIGINT, и он все равно напечатаетuse of closed connection
log, в котором указано этоerr
отfd.pfd.Accept()
Метод брошен, перейдите к этому методу, чтобы увидеть подробности:
// Accept wraps the accept network call.
func (fd *FD) Accept() (int, syscall.Sockaddr, string, error) {
...
if err := fd.pd.prepareRead(fd.isFile); err != nil {
return -1, nil, "", err
}
for {
//这个accept()是对accept系统调用的封装方法
s, rsa, errcall, err := accept(fd.Sysfd)
...
switch err {
case syscall.EAGAIN:
if fd.pd.pollable() {
if err = fd.pd.waitRead(fd.isFile); err == nil {
continue
}
}
...
}
return -1, nil, errcall, err
}
}
в функцииaccept()
правдаaccept
Инкапсуляция системных вызовов выходит за рамки этой статьи, и это большое знание системы UNIX.Accept
На этом семейство подошло к концу, и мы пришли к выводу, что эта ошибка выдается системой UNIX и не имеет никакого отношения к нашему сервису и языку Go. Этот вывод, несомненно, является подходом в стиле страуса.Хотя ошибка выдается системой, система не сообщит об ошибке без причины.Должно быть что-то не так с операцией, из-за которой система сообщила об ошибке. Позвольте мне подробнее рассмотреть этот метод, сначалаprepareRead()
подготовьтесь, а затем приступайтеaccept
системный вызов, хотя вряд ли застрянет на этомprepareRead()
метод (потому что после запуска нашего сервиса он может нормально принимать и обрабатывать подключения), но мы все равно можем пойти и посмотреть, что здесь сделаноprepare
.
func (pd *pollDesc) prepareRead(isFile bool) error {
return pd.prepare('r', isFile)
}
func (pd *pollDesc) prepare(mode int, isFile bool) error {
if pd.runtimeCtx == 0 {
return nil
}
res := runtime_pollReset(pd.runtimeCtx, mode)
return convertErr(res, isFile)
}
наконец позвонилRuntime
изruntime_pollReset()
Сбросить поллер ввода-вывода и преобразоватьRuntime
Ошибка брошена, введите этоconvertErr()
Взгляни:
func convertErr(res int, isFile bool) error {
switch res {
case 0:
return nil
case 1:
return errClosing(isFile)
case 2:
return ErrTimeout
}
...
}
// ErrNetClosing is returned when a network descriptor is used after it has been closed.
var ErrNetClosing = errors.New("use of closed network connection")
// Return the appropriate closing error based on isFile.
func errClosing(isFile bool) error {
if isFile {
return ErrFileClosing
}
return ErrNetClosing
}
Наконец, мы бросаем это в невозможноеErr
нашел этоErr
:use of closed network connection
. Это небольшой поворот, потому что мы знаем, чтоprepareRead()
не возвращаетсяErr
, иначе наш сервис не сможет запуститься и прослушать порт, так что хоть это место и декларирует этоErrNetClosing
объект, но не брошенный здесьErr
. Из предыдущего анализа мы пришли к выводу, чтоErr
то, что система возвращает нам. Здесь анализ снова прерывается, но мы можем расширить наши идеи и увидеть это.ErrNetClosing
О чем говорится в комментарии: о том, что при использовании закрытого сетевого дескриптора этотError
будет возвращено, то мы ищем этоErrNetClosing
используется там, вprepareRead()
Рядом с методом мы нашли следующий метод:
//修改了上述变量ErrNetClosing的值
var ErrNetClosing = errors.New("use of closed network connection --- 改")
func (pd *pollDesc) wait(mode int, isFile bool) error {
...
res := runtime_pollWait(pd.runtimeCtx, mode)
println(" 没错,错误就是我抛出来的 res:", res)
//如果res为1 ,抛出错误ErrNetClosing。如果res为0,err = nil。
return convertErr(res, isFile)
}
Приведенный выше код является моей модификацией исходного кода, плюс некоторые журналы, а теперь повторно запустите, отправьтеSIGINT
Подать сигнал и посмотреть, какие изменения в логе:
Примечание. Если вы изменяете код в стандартной библиотеке go, вам нужно перейти к сборке -a, добавить параметр a, что означает, что весь код перекомпилируется, включая код из стандартной библиотеки go.
没错,错误就是我抛出来的 res:0
没错,错误就是我抛出来的 res:0
2018/03/09 11:42:57 31164 0.0.0.0:5490
2018/03/09 11:43:09 31164 Received SIGINT.
2018/03/09 11:43:09 31164 [::]:5490 Listener closed.
2018/03/09 11:43:09 31164 Waiting for connections to finish...
没错,错误就是我抛出来的 res:1
2018/03/09 11:43:09 [C] [asm_amd64.s:2337] ListenAndServe: accept tcp [::]:5490: use of closed network connection --- 改 31164
когдаres=0
когда все в порядке, при приемеSIGINT
, как видно из лога,res
Это время равно 1, что приводит кconvertErr()
вернуть ошибкуErrNetClosing
. Это выглядит такwait()
Это реальный источник этой ошибки. Однако, хотя мы его и нашли, это не решило нашего вопроса, почему res устанавливается системой в 1, что в итоге приводит к получениюErrNetClosing
? Что именно вызывает эту ошибку? Также см. этоwait()
метода, не может не возникнуть новых вопросов: почемуAccept()
Семья окончательно заблокироваласьAccept
При системном вызове ошибка такаяwait()
возвращение,wait()
иAccept()
Как связана семья?
3. Сетевое программированиеAccept
,Wait
Кажется, Go сделал для нас очень много, мы знаем только, что сервер находится вListenAndServe()
В блокировке и далее находится вfd.Accept()
Метод блокируется, ожидая установления соединения. В предыдущем анализе мы обнаружилиwait()
Метод, несомненно, ожидание может выразить смысл блокировки ожидания больше, чем принять, давайте посмотрим еще разfd.Accept()
метод:
// Accept wraps the accept network call.
func (fd *FD) Accept() (int, syscall.Sockaddr, string, error) {
...
if err := fd.pd.prepareRead(fd.isFile); err != nil {
return -1, nil, "", err
}
for {
//这个accept()是对accept系统调用的封装方法,其实它是返回了的,没有阻塞
s, rsa, errcall, err := accept(fd.Sysfd)
...
switch err {
case syscall.EAGAIN:
if fd.pd.pollable() {
//这里调用了waitRead(),说明了上面的accept()方法其实没有阻塞,真正的阻塞在这里。
if err = fd.pd.waitRead(fd.isFile); err == nil {
continue
}
}
}
return -1, nil, errcall, err
}
}
func (pd *pollDesc) waitRead(isFile bool) error {
return pd.wait('r', isFile)
}
Наконец, мы нашлиwaitRead()
, что точноwait()
Слой инкапсуляции для метода. Ситуация стала ясной, что мы пойманы в сети, будем заблокированы вAccept()
метод в этом мышлении, былAccept
В цепочке вызовов нет результата, и теперь сеть заблокирована вAccept()
Это предложение правильное, но оно справедливо для кода прикладного уровня.fd.Accept()
вернется, когда закончитsyscall.EAGAIN
этоErr
, который фиксирует именно этоsyscall.EAGAIN
, пусть наш код остановится наwaitRead()
, пока не будет установлено соединение. Так,wait()
метод правильныйruntime
изruntime_pollWait
Один слой инкапсуляции, вы знаетеwait()
Конкретное содержаниеruntime
Найдите в упаковке.
//go:linkname poll_runtime_pollWait internal/poll.runtime_pollWait
func poll_runtime_pollWait(pd *pollDesc, mode int) int {
err := netpollcheckerr(pd, int32(mode))
if err != 0 {
return err
}
...
//代码将会阻塞在这里,如果netpollblock()不返回true,代码将一直循环。
for !netpollblock(pd, int32(mode), false) {
//循环会不断检查是否有错误,有错误则退出
err = netpollcheckerr(pd, int32(mode))
if err != 0 {
return err
}
}
return 0
}
func netpollcheckerr(pd *pollDesc, mode int32) int {
if pd.closing {
//pd关闭会导致这里返回 1
return 1 // errClosing
}
if (mode == 'r' && pd.rd < 0) || (mode == 'w' && pd.wd < 0) {
return 2 // errTimeout
}
return 0
}
До сих пор мы нашлиres=1
откуда именноnetpollcheckerr()
1 возвращается, в результате чего res получает 1, таким образом выбрасываяErrNetClosing
ошибка, из-за этой ошибки мы получилиuse of closed netword connection
Этот журнал. На этом цепочка обрывается, но теперь мы знаем только, что это такое, и не знаем, почему. К какому процессу привелиpd.closing
сталtrue
? Повлияет ли эта ошибка на нашу бизнес-логику, можно ли ее игнорировать? Почему тот же сервис закрыт, только сервис с включенным режимом Graceful выдает эту ошибку? Ясно, что для того, чтобы развеять эти сомнения, мы должны изменить курс и вернуться кListenAndServe()
, как служба поискаClosed
из.
4. Close
цепь
func (srv *Server) ListenAndServe() (err error) {
...
// 处理上图中的热升级信号(fork子进程),SIGINT、SIGTERM信号(进程结束信号)
go srv.handleSignals()
//如果是子进程,getListener()拿到的还是父进程的监听器,如果是父进程,创建新的监听器。
l, err := srv.getListener(addr)
if err != nil {
log.Println(err)
return err
}
//对监听器做包装
srv.GraceListener = newGraceListener(l, srv)
...
}
func (srv *Server) handleSignals() {
var sig os.Signal
signal.Notify(
srv.sigChan,
hookableSignals...,
)
pid := syscall.Getpid()
for {
sig = <-srv.sigChan
...
switch sig {
case syscall.SIGHUP:
log.Println(pid, "Received SIGHUP. forking.")
err := srv.fork()
...
case syscall.SIGINT:
log.Println(pid, "Received SIGINT.")
srv.shutdown()
case syscall.SIGTERM:
log.Println(pid, "Received SIGTERM.")
srv.shutdown()
...
}
}
можно увидеть здесьListenAndServe()
При вызове аgoroutine
, дождитесь прихода системного сигнала после полученияSIGHUP
сигнал, немедленно разветвить дочерний процесс; если полученSIGINT
илиSIGTERM
сигнал, он позвонитstv.shutdown()
закрыть соединение и посмотретьstv.shutdown()
Что сделал:
func (srv *Server) shutdown() {
...
srv.state = StateShuttingDown
if DefaultTimeout >= 0 {
go srv.serverTimeout(DefaultTimeout)
}
err := srv.GraceListener.Close()
...
}
func (srv *Server) serverTimeout(d time.Duration) {
...
time.Sleep(d)
//当d时间过去后,进程结束休眠,强制将计数器置0
for {
if srv.state == StateTerminate {
break
}
//计数器减1
srv.wg.Done()
}
}
звонил сюдаGraceListener
изClose()
, как мы уже говорили, этоGraceListener
на самом деле правдаTCPListener
упаковка. в основном вAccept()
Добавляем счетчик в , когда приходит соединение, счетчик увеличивается на 1, обработка соединения завершена, счетчик уменьшается на 1. Конечно если есть задержка в сети, или у клиента соединение приостановлено и счетчик не 0, послеDefaultTimeout
(60 секунд),serverTimeout()
установит счетчик на 0. Итак, входимClose()
метод, чтобы увидеть, как отключить мониторинг.
func (gl *graceListener) Close() error {
if gl.stopped {
return syscall.EINVAL
}
//简单的向stop channer 发送nil信号
gl.stop <- nil
//等待TCPListener.Close()执行完毕
return <-gl.stop
}
func newGraceListener(l net.Listener, srv *Server) (el *graceListener) {
el = &graceListener{
Listener: l,
stop: make(chan error),
server: srv,
}
//开启一个goroutine,不阻塞代码
go func() {
//等待Close()的nil信号
<-el.stop
el.stopped = true
el.stop <- el.Listener.Close()
}()
return
}
участие здесьgoroutine
связь междуGraceListener
Когда у вас есть конечный сигнал, код будет<-el.stop
блокировать доgl.stop<-nil
оператор выполняется,stoped
имеет значение true и вызываетTCPListener.Close()
и дождитесь завершения его выполнения. ТакTCPListener.Close()
Что ты опять сделал?
func (l *TCPListener) Close() error {
...
if err := l.close(); err != nil {
return &OpError{Op: "close", Net: l.fd.net, Source: nil, Addr: l.fd.laddr, Err: err}
}
return nil
}
func (ln *TCPListener) close() error {
return ln.fd.Close()
}
Close()
метод правильныйclose()
Инкапсуляция метода, поскольку в Go нет модификатора доступа, регистр первой буквы метода указывает, является ли метод общедоступным или закрытым. такClose()
-close()
Это также особенность Go.close()
метод называетсяfd.close()
, мы проанализировали ранее, этоfd
ссылается на файловый дескриптор, который является текущимSocket
соединение, это соединение закрыто, и мы не можем принимать новые соединения.
func (fd *netFD) Close() error {
runtime.SetFinalizer(fd, nil)
return fd.pfd.Close()
}
func (fd *FD) Close() error {
...
fd.pd.evict()
return fd.decref()
}
func (pd *pollDesc) evict() {
if pd.runtimeCtx == 0 {
return
}
runtime_pollUnblock(pd.runtimeCtx)
}
Наконец позвонилruntime_pollUnblock()
, смотрите прямо наruntime
Исходный код пакета:
func poll_runtime_pollUnblock(pd *pollDesc) {
lock(&pd.lock)
if pd.closing {
throw("runtime: unblock on closing polldesc")
}
pd.closing = true
...
}
Помните наш анализWait
процесс сказалpd.closing
, это дляtrue
приводит кres=1
:
//如果netpollblock()不返回true,代码将一直循环
for !netpollblock(pd, int32(mode), false) {
//循环会不断检查是否有错误,有错误则退出
err = netpollcheckerr(pd, int32(mode))
...
}
---------
if pd.closing {
//pd关闭会导致这里返回 1
return 1 // errClosing
}
Теперь понятно, что происходит, код вышеpd.closing
установить значение true иAccpet()
Существующийgoroutine
Убедитесь, что это значение изменилось, поэтому оно завершаетсяAccept()
обрабатывать и сообщать об ошибках, этиgoroutine
Отношение показано на следующем рисунке:
Трое на картинке вышеgoroutine
, крайний справаshutdown()
Начните искать, шаг за шагом, чтобы завершить процесс выключения. далеко слеваAccept()
Существующийgoroutine
Как правило, если мы закончим программу нормально, этоgoroutine
вернется нормально. Но для достиженияGraceful
режим и два новыхgoroutine
, именно эти два вновь созданныхgoroutine
сотрудничать, чтобы закрыть текущее сетевое соединение,Accept()
Существующийgoroutine
Не зная о текущей ситуации, подумал, что это было неожиданное отключение, из-за которого мы получили эту линию.Err
бревно. Однако это отключение никак не повлияет на существующую программу, потому что мы знаем, что этот процесс активно осуществляется нами самими.
5. Постскриптум
Процесс разбора логов Beego закончен.Статья очень длинная,и здорово,что есть возможность увидеть последних друзей.Хоть я и думал разделить ее на две части,но взвесив решил сдаться,потому что эти анализы связаны до и после, что также является моим образом мышления.Если письменные отчеты разделены, всегда будет ощущение, что их насильно прервали, и, по-видимому, читатели найдут это неприятным. В целях заботы о читателях и людях, читающих с мобильных телефонов, код, приведенный в этой статье, максимально лаконичен, а аннотация сделана в том месте, где сделана аннотация.Надеюсь, что в эту эпоху информационного взрыва , читатели (в том числе и я в будущем) смогут извлечь из него уроки как можно скорее.Извлеките ключевую информацию.
Когда я собирался написать эту статью, это было не так долго, как сейчас, но когда Accept заблокирован, система сообщит об ошибке «использование сетевого подключения» после fd.close(), но это не влияет на бизнес. логика. Но по мере продвижения статьи возникал один вопрос за другим, что побуждало меня к более глубокому поиску ответа, что также является неожиданным процессом обучения. Недавно это также заставило меня задуматься о том, как найти ответ на вопрос. В сегодняшней высоко инкапсулированной технологии программирования должны ли мы шпионить за нижней частью языка? Или на уровне операционной системы? Или углубиться в ассемблер и перебрать каждый регистр, каждую инструкцию? Предшественники и мы нагромождали горы компьютеров все выше и выше.Если мы будем стоять на высоком месте, то однажды мы не сможем увидеть сцену под горой, а может быть, мы не узнаем нижние вещи в наших вся жизнь. Недавно я услышал, что JS сейчас по-разному инкапсулируется, и даже многие языки перед запуском компилируются в JS.Некоторые люди говорят, что JS становится чуть ли не фронтенд-сборкой. Я также слышал о Flutter, Этот фреймворк инкапсулирует процесс разработки Android и iOS и предоставляет высокоуровневый интерфейс, совместимый с разработкой двух платформ. Я не могу не быть немного запутанным.Модные технологии появляются одна за другой.В прошлом году RN и Weex были еще популярны.В этом году вышел большой киллер,разработанный на разных платформах.Он обновляется каждый год.
Процесс анализа на этот раз имеет небольшое представление об этих проблемах.При анализе процесса Accept системный вызов Accept не идет дальше.При анализе функции ожидания анализ останавливается по достижении времени выполнения. для тех системных ядер вообще не привлекался (и не разбирался). Потому что этих существующих анализов достаточно, чтобы сформировать пояснение к вопросу в начале статьи и сделать выводы, не затрагивающие существующие бизнес-процессы. В конце концов, ваш лидер хочет только результатов. Если ваши предварительные знания могут удержать вашу работу, нет необходимости баловаться. В конце концов, применяйте то, чему вы научились, и в конечном итоге служите за эту зарплату. Если вы хотите получить более высокую зарплату, найти лучшую работу и не можете удержать свои способности, вы, естественно, возьмете на себя инициативу, чтобы дополнить себя. Ведь жизнь коротка,я использую питонРаботайте меньше сверхурочно, делайте больше того, что вам нравится, и проводите больше времени с важными людьми.
6. Ссылки
Сеть Golang: анализ реализации основного API