Go имеет много преимуществ, таких как: простота, встроенная поддержка параллелизма и т. д., а хорошая переносимость также является одним из важных факторов, благодаря которым Go принимается большинством программистов. Но знаете ли вы, почему у Go отличная переносимость платформы? В духе «знать, что верно, но также знать, почему» в этой статье мы рассмотрим принципы, лежащие в основе хорошей переносимости Go.
1. Портативность Go
Когда дело доходит до переносимости языка программирования, мы обычно рассматриваем следующие два аспекта:
- Легкость, с которой сам язык можно портировать на разные платформы;
- Адаптивность прикладной программы, составленной этим языком, к платформе.
В Go 1.7 и более поздних версиях мы можем просмотреть список ОС и платформ, поддерживаемых Go, с помощью следующей команды:
$ go tool dist list
android/386
android/amd64
android/arm
android/arm64
darwin/386
darwin/amd64
darwin/arm
darwin/arm64
dragonfly/amd64
freebsd/386
freebsd/amd64
freebsd/arm
linux/386
linux/amd64
linux/arm
linux/arm64
linux/mips
linux/mips64
linux/mips64le
linux/mipsle
linux/ppc64
linux/ppc64le
linux/s390x
nacl/386
nacl/amd64p32
nacl/arm
netbsd/386
netbsd/amd64
netbsd/arm
openbsd/386
openbsd/amd64
openbsd/arm
plan9/386
plan9/amd64
plan9/arm
solaris/amd64
windows/386
windows/amd64
Из вышеприведенного списка мы видим, что:linux/arm64встроенных систем дляlinux/s390xСистемы мейнфреймов, такие как Windows, Linux и darwin (mac), а также основные процессорные системы, такие как amd64 и 386, Go поддерживает различные платформы и операционные системы.
Официальный представитель Go, похоже, не дает четкого руководства по переносу, а содержание переноса языка Go на другие платформы больше обсуждается в небольших кругах, таких как golang-dev. Но поскольку язык Go может поддерживать так много платформ за такое короткое время, перенос Go относительно прост. По моему личному пониманию Go, это отчасти связано с независимой реализацией среды выполнения в Go.
Среда выполнения — это основа, поддерживающая работу программы. Мы лучше всего знакомы с libc (среда выполнения C), которая является наиболее часто используемой средой выполнения в основных операционных системах, обычно в виде динамически подключаемых библиотек (например: /lib/x86_64-linux-gnu/libc.so. 6). С выходом системы ее функции примерно таковы:
- Обеспечьте основные вызовы библиотечных функций, такие как: strncpy;
- Инкапсулировать системный вызов (Примечание: системный вызов — это порт API, предоставляемый операционной системой, когда пользовательский уровень выполняет системный вызов, код будет перехватываться (перехватываться) на уровне ядра для выполнения) и обеспечивать вызовы библиотечных функций на том же языке. , такие как: malloc, fread и т. д.;
- Провайдер запускает функцию входа, например: __libc_start_main под linux.
Библиотеки времени выполнения C, такие как libc, были реализованы давно, и даже некоторые старые libcs являются однопоточными. У некоторых программистов, много лет занимающихся c/c разработкой, вероятно, в первые годы был такой опыт: то есть при линковке runtime-библиотеки приходится даже выбирать, линковать ли библиотеку, поддерживающую многопоточность, или библиотека, которая поддерживает только однопоточность. К тому же версия c runtime тоже неравномерна. Такая ситуация среды выполнения c не может удовлетворить потребности самого языка go, кроме того, одна из целей go — нативно поддерживать параллелизм и использовать модель goroutine, и среда выполнения c ничего не может с этим поделать, потому что среда выполнения c сама основана на модели потока. Сочетая вышеперечисленные факторы, Go реализует среду выполнения и инкапсулирует системный вызов для работы на разных платформах. Код пользовательского уровня предоставляет инкапсулированную и унифицированную стандартную библиотеку Go, в то же время среда выполнения Go реализует поддержку модели goroutine.
Независимо реализованный слой go runtime отделяет пользовательский код Go от системного вызова ОС.При портировании Go на новую платформу рантайм можно состыковать с системным вызовом новой платформы (разумеется, работа по переносу не только в этом) ; в то же время среда выполнения Реализация уровня в основном избавляет программу Go от зависимости от libc, так что статически скомпилированная программа Go имеет хорошую адаптируемость к платформе. Например: программа Go, скомпилированная для Linux amd64, может хорошо работать в различных дистрибутивах Linux (centos, ubuntu).
Следующая тестовая среда: darwin amd64 Go 1.8.
Во-вторых, программа Go со статической ссылкой по умолчанию.
Давайте сначала напишем две программы: hello.c и hello.go, которые выполняют схожие функции и выводят строку текста на стандартный вывод:
//hello.c
#include
int main() {
printf("%s\n", "hello, portable c!");
return 0;
}
//hello.go
package main
import "fmt"
func main() {
fmt.Println("hello, portable go!")
}
Мы используем метод «по умолчанию» для отдельной компиляции следующих двух программ:
$cc -o helloc hello.c
$go build -o hellogo hello.go
$ls -l
-rwxr-xr-x 1 tony staff 8496 6 27 14:18 helloc*
-rwxr-xr-x 1 tony staff 1628192 6 27 14:18 hellogo*
По размеру двух скомпилированных файлов, helloc и hellogo, видно, что hellogo — «гигант» по сравнению с helloc, а его размер почти в 200 раз больше, чем у helloc. Любой, кто немного изучил Go, знает, что это потому, что hellogo включает в себя необходимую среду выполнения Go. Давайте проверим зависимости двух файлов от внешних динамических библиотек с помощью инструмента otool (можно использовать ldd в linux):
$otool -L helloc
helloc:
/usr/lib/libSystem.B.dylib (compatibility version 1.0.0, current version 1197.1.1)
$otool -L hellogo
hellogo:
Из вывода otool мы видим, что hellogo не зависит ни от какой внешней библиотеки.Мы можем скопировать двоичный файл hellogo на любую платформу mac amd64, и он сможет работать. И helloc зависит от внешней динамической библиотеки: /usr/lib/libSystem.B.dylib, а динамическая библиотека libSystem.B.dylib имеет другие зависимости. Мы можем использовать инструмент nm, чтобы увидеть, какой функциональный символ helloc должен быть предоставлен внешней динамической библиотекой:
$nm helloc
0000000100000000 T __mh_execute_header
0000000100000f30 T _main
U _printf
U dyld_stub_binder
Видно, что символы _printf и dyld_stub_binder не определены (соответствующий символ префикса — U). Если вы используете nm с hellog, вы увидите много символов на выходе, но никаких неопределенных символов.
$nm hellogo
00000000010bb278 s $f64.3eb0000000000000
00000000010bb280 s $f64.3fd0000000000000
00000000010bb288 s $f64.3fe0000000000000
00000000010bb290 s $f64.3fee666666666666
00000000010bb298 s $f64.3ff0000000000000
00000000010bb2a0 s $f64.4014000000000000
00000000010bb2a8 s $f64.4024000000000000
00000000010bb2b0 s $f64.403a000000000000
00000000010bb2b8 s $f64.4059000000000000
00000000010bb2c0 s $f64.43e0000000000000
00000000010bb2c8 s $f64.8000000000000000
00000000010bb2d0 s $f64.bfe62e42fefa39ef
000000000110af40 b __cgo_init
000000000110af48 b __cgo_notify_runtime_init_done
000000000110af50 b __cgo_thread_start
000000000104d1e0 t __rt0_amd64_darwin
000000000104a0f0 t _callRet
000000000104b580 t _gosave
000000000104d200 T _main
00000000010bbb20 s _masks
000000000104d370 t _nanotime
000000000104b7a0 t _setg_gcc
00000000010bbc20 s _shifts
0000000001051840 t errors.(*errorString).Error
00000000010517a0 t errors.New
.... ...
0000000001065160 t type..hash.time.Time
0000000001064f70 t type..hash.time.zone
00000000010650a0 t type..hash.time.zoneTrans
0000000001051860 t unicode/utf8.DecodeRuneInString
0000000001051a80 t unicode/utf8.EncodeRune
0000000001051bd0 t unicode/utf8.RuneCount
0000000001051d10 t unicode/utf8.RuneCountInString
0000000001107080 s unicode/utf8.acceptRanges
00000000011079e0 s unicode/utf8.first
$nm hellogo|grep " U "
Go помещает весь код функции, необходимый для запуска, в hellogo, что называется «статической компоновкой». Правда ли, что во всех случаях Go не зависит от внешних динамических разделяемых библиотек? Давайте посмотрим на следующий код:
//server.go
package main
import (
"log"
"net/http"
"os"
)
func main() {
cwd, err := os.Getwd()
if err != nil {
log.Fatal(err)
}
srv := &http.Server{
Addr: ":8000", // Normally ":443"
Handler: http.FileServer(http.Dir(cwd)),
}
log.Fatal(srv.ListenAndServe())
}
Мы написали файловый сервер, используя пакет net/http стандартной библиотеки Go. Давайте соберем сервер и посмотрим, есть ли у него внешние зависимости и неопределенные символы:
$go build server.go
-rwxr-xr-x 1 tony staff 5943828 6 27 14:47 server*
$otool -L server
server:
/usr/lib/libSystem.B.dylib (compatibility version 0.0.0, current version 0.0.0)
/System/Library/Frameworks/CoreFoundation.framework/Versions/A/CoreFoundation (compatibility version 0.0.0, current version 0.0.0)
/System/Library/Frameworks/Security.framework/Versions/A/Security (compatibility version 0.0.0, current version 0.0.0)
/usr/lib/libSystem.B.dylib (compatibility version 0.0.0, current version 0.0.0)
/usr/lib/libSystem.B.dylib (compatibility version 0.0.0, current version 0.0.0)
$nm server |grep " U "
U _CFArrayGetCount
U _CFArrayGetValueAtIndex
U _CFDataAppendBytes
U _CFDataCreateMutable
U _CFDataGetBytePtr
U _CFDataGetLength
U _CFDictionaryGetValueIfPresent
U _CFEqual
U _CFNumberGetValue
U _CFRelease
U _CFStringCreateWithCString
U _SecCertificateCopyNormalizedIssuerContent
U _SecCertificateCopyNormalizedSubjectContent
U _SecKeychainItemExport
U _SecTrustCopyAnchorCertificates
U _SecTrustSettingsCopyCertificates
U _SecTrustSettingsCopyTrustSettings
U ___error
U ___stack_chk_fail
U ___stack_chk_guard
U ___stderrp
U _abort
U _fprintf
U _fputc
U _free
U _freeaddrinfo
U _fwrite
U _gai_strerror
U _getaddrinfo
U _getnameinfo
U _kCFAllocatorDefault
U _malloc
U _memcmp
U _nanosleep
U _pthread_attr_destroy
U _pthread_attr_getstacksize
U _pthread_attr_init
U _pthread_cond_broadcast
U _pthread_cond_wait
U _pthread_create
U _pthread_key_create
U _pthread_key_delete
U _pthread_mutex_lock
U _pthread_mutex_unlock
U _pthread_setspecific
U _pthread_sigmask
U _setenv
U _strerror
U _sysctlbyname
U _unsetenv
По результатам вывода otool и nm мы с удивлением увидели: как программы Go, использующие «статическую компоновку» по умолчанию, могут зависеть от внешних библиотек динамической компоновки, а также содержать множество «неопределенных» символов? Проблема с cgo.
3. Влияние cgo на переносимость
По умолчанию переменная среды выполнения Go CGO_ENABLED=1, которая запускает cgo по умолчанию, позволяет вам вызывать код C в коде Go, и в этом случае также компилируется файл .a предварительно скомпилированной стандартной библиотеки Go. В $GOROOT/pkg/darwin_amd64 мы просматриваем все предварительно скомпилированные файлы стандартной библиотеки .a и используем nm для вывода неопределенных символов каждого .a. Мы видим, что некоторые из следующих пакетов являются внешними зависимостями (динамическая компоновка):
=> crypto/x509.a
U _CFArrayGetCount
U _CFArrayGetValueAtIndex
U _CFDataAppendBytes
... ...
U _SecCertificateCopyNormalizedIssuerContent
U _SecCertificateCopyNormalizedSubjectContent
... ...
U ___stack_chk_fail
U ___stack_chk_guard
U __cgo_topofstack
U _kCFAllocatorDefault
U _memcmp
U _sysctlbyname
=> net.a
U ___error
U __cgo_topofstack
U _free
U _freeaddrinfo
U _gai_strerror
U _getaddrinfo
U _getnameinfo
U _malloc
=> os/user.a
U __cgo_topofstack
U _free
U _getgrgid_r
U _getgrnam_r
U _getgrouplist
U _getpwnam_r
U _getpwuid_r
U _malloc
U _realloc
U _sysconf
=> plugin.a
U __cgo_topofstack
U _dlerror
U _dlopen
U _dlsym
U _free
U _malloc
U _realpath$DARWIN_EXTSN
=> runtime/cgo.a
... ...
U _abort
U _fprintf
U _fputc
U _free
U _fwrite
U _malloc
U _nanosleep
U _pthread_attr_destroy
U _pthread_attr_getstacksize
... ...
U _setenv
U _strerror
U _unsetenv
=> runtime/race.a
U _OSSpinLockLock
U _OSSpinLockUnlock
U __NSGetArgv
U __NSGetEnviron
U __NSGetExecutablePath
U ___error
U ___fork
U ___mmap
U ___munmap
U ___stack_chk_fail
U ___stack_chk_guard
U __dyld_get_image_header
.... ...
Возьмем для примера os/user.При CGO_ENABLED=1, то есть cgo включен, функции серии lookupUserxxx в пакете os/user реализованы в версии c. Мы видим, что в $GOROOT/src/os/ user/lookup_unix Тег сборки в .go содержит build cgo. Таким образом, с CGO_ENABLED=1 файл будет скомпилирован и будет использован lookupUser, реализованный в версии c файла:
// build darwin dragonfly freebsd !android,linux netbsd openbsd solaris
// build cgo
package user
... ...
func lookupUser(username string) (*User, error) {
var pwd C.struct_passwd
var result *C.struct_passwd
nameC := C.CString(username)
defer C.free(unsafe.Pointer(nameC))
... ...
}
С этой точки зрения все исполняемые файлы, скомпилированные кодом Go, которые зависят от вышеуказанных пакетов, должны иметь внешние зависимости. Однако мы все еще можем компилировать чистые статические программы Go, отключив CGO_ENABLED:
$CGO_ENABLED=0 go build -o server_cgo_disabled server.go
$otool -L server_cgo_disabled
server_cgo_disabled:
$nm server_cgo_disabled |grep " U "
Если вы используете опцию сборки «-x -v», вы увидите, что компилятор go перекомпилирует статические версии зависимых пакетов, включая net, mime/multipart, crypto/tls и т. д., и преобразует скомпилированные файлы . a (начиная с пакетов) во временный рабочий каталог компилятора ($WORK), а затем статически связать эти версии.
4. Внутренние ссылки и внешние ссылки
Возникает вопрос: можно ли добиться чистого статического соединения со значением по умолчанию CGO_ENABLED=1? Ответ положительный. В $GOROOT/cmd/cgo/doc.go документация представляет два режима работы cmd/link: внутреннее связывание и внешнее связывание.
1. Внутренняя перелинковка
Общий смысл внутренней компоновки заключается в том, что если пользовательский код использует только зависимые от cgo пакеты в нескольких стандартных библиотеках, таких как net, os/user и т. д., cmd/link по умолчанию использует внутреннюю компоновку без запуска внешнего внешнего компоновщика (например: gcc, clang и т. д.), но из-за ограниченной функциональности cmd/link в окончательный бинарный файл записываются только .o и .a из предварительно скомпилированной стандартной библиотеки. Таким образом, если стандартная библиотека скомпилирована с параметром CGO_ENABLED=1, окончательный скомпилированный бинарный файл по-прежнему будет динамически компоноваться, даже если в go build будет передан параметр -ldflags. 'extldflags "-static"' также бесполезен, потому что внешний компоновщик вообще не используется:
$go build -o server-fake-static-link -ldflags \'-extldflags "-static"\' server.go
$otool -L server-fake-static-link
server-fake-static-link:
/usr/lib/libSystem.B.dylib (compatibility version 0.0.0, current version 0.0.0)
/System/Library/Frameworks/CoreFoundation.framework/Versions/A/CoreFoundation (compatibility version 0.0.0, current version 0.0.0)
/System/Library/Frameworks/Security.framework/Versions/A/Security (compatibility version 0.0.0, current version 0.0.0)
/usr/lib/libSystem.B.dylib (compatibility version 0.0.0, current version 0.0.0)
/usr/lib/libSystem.B.dylib (compatibility version 0.0.0, current version 0.0.0)
2. Внешние ссылки
Механизм внешней компоновки заключается в том, что cmd/link вводит все сгенерированные .o в файл .o, а затем передает его внешнему компоновщику, такому как gcc или clang, для окончательной обработки ссылки. Если в это время мы передаем -ldflags 'extldflags "-static"' в параметрах cmd/link, тогда gcc/clang выполнит статическое связывание и заменит неопределенные символы в .o реальным кодом. Мы можем заставить cmd/link использовать внешний компоновщик через -linkmode=external или взять в качестве примера компиляцию server.go:
$go build -o server-static-link -ldflags \'-linkmode "external" -extldflags "-static"\' server.go
# command-line-arguments
/Users/tony/.bin/go18/pkg/tool/darwin_amd64/link: running clang failed: exit status 1
ld: library not found for -lcrt0.o
clang: error: linker command failed with exit code 1 (use -v to see invocation)
Видно, что clang, вызванный cmd/link, пытается статически связать файл .a libc, но, поскольку мой Mac имеет только dylib libc, а не .a, статическое связывание не удается. Я нашел среду Ubuntu 16.04: повторите приведенную выше команду сборки:
# go build -o server-static-link -ldflags \'-linkmode "external" -extldflags "-static"\' server.go
# ldd server-static-link
not a dynamic executable
# nm server-static-link|grep " U "
В этой среде libc.a и libpthread.a расположены в следующих двух местах:
/usr/lib/x86_64-linux-gnu/libc.a
/usr/lib/x86_64-linux-gnu/libpthread.a
Таким же образом мы также скомпилировали и сконструировали чистую статически скомпонованную программу Go с CGO_ENABLED=1.
Если ваш код использует код C и полагается на cgo для вызова кода C в go, тогда cmd/link автоматически выберет механизм внешней компоновки:
//testcgo.go
package main
//#include
// void foo(char *s) {
// printf("%s\n", s);
// }
// void bar(void *p) {
// int *q = (int*)p;
// printf("%d\n", *q);
// }
import "C"
import (
"fmt"
"unsafe"
)
func main() {
var s = "hello"
C.foo(C.CString(s))
var i int = 5
C.bar(unsafe.Pointer(&i))
var i32 int32 = 7
var p *uint32 = (*uint32)(unsafe.Pointer(&i32))
fmt.Println(*p)
}
Скомпилируйте testcgo.go:
# go build -o testcgo-static-link -ldflags \'-extldflags "-static"\' testcgo.go
# ldd testcgo-static-link
not a dynamic executable
vs.
# go build -o testcgo testcgo.go
# ldd ./testcgo
linux-vdso.so.1 => (0x00007ffe7fb8d000)
libpthread.so.0 => /lib/x86_64-linux-gnu/libpthread.so.0 (0x00007fc361000000)
libc.so.6 => /lib/x86_64-linux-gnu/libc.so.6 (0x00007fc360c36000)
/lib64/ld-linux-x86-64.so.2 (0x000055bd26d4d000)
V. Резюме
В этой статье исследуется переносимость Go и факторы, влияющие на переносимость программ, скомпилированных с помощью Go:
- Какие пакеты стандартных библиотек использует ваша программа? Если это обычный пакет, отличный от net, os/user и т. д., то ваша программа по умолчанию будет чисто статической и не будет зависеть от какой-либо внешней библиотеки динамической компоновки, такой как c lib;
- Если вы используете стандартный библиотечный пакет, такой как net, который содержит код cgo, то значение CGO_ENABLED повлияет на свойства вашей программы после компиляции: будет ли она статической или динамически скомпонованной;
- В случае CGO_ENABLED=0 Go использует чистую статическую компиляцию;
- Если CGO_ENABLED=1, но все же для принудительной статической компиляции, передайте -linkmode=external в cmd/link.
Ссылка на ссылку:
Тони Пендулум.com/27/06/2017/…