























In the automation path of OpenIM, it involves more and more comprehensive automated design and testing. In the process, I encountered a problem, so I completed a full set of experience from go language type detection to integrated local and CI.
The problem is this issue: https://github.com/openimsdk/open-im-server/issues/1807
Our Go code encountered an integer overflow issue when running on a 32-bit system (linux/386). This problem occurs because the int type in Go has different sizes depending on the architecture: on 32-bit systems it is equivalent to int32, and on 64-bit systems it is equivalent to int64.
It happened to run normally on 64-bit machines, but overflow problems would occur on 32-bit machines, so I thought about making a set of detection tools to solve the type detection of each platform.
Before we dive into the code, let’s review some basic concepts of the Go language, specifically package management, concurrent programming, and the type system. These concepts are fundamental to understanding and programming effectively with the Go language.
import statement to import other packages.go keyword to start a new goroutine.int, float, bool), composite types (such as struct, slice ), and user-defined types.In the Go language, type declarations are the way to define new types. Go supports a variety of types, including primitive types (such as int, float64, bool), composite types (such as array, slice, map, struct), and interface types. Type declarations allow you to create custom types, which is important for writing clear, easy-to-maintain code.
A basic type declaration refers to defining a new type based on an existing type. For example, you can create a new type called Seconds, which is based on the int type.
Here, Seconds is a new type that has all the features of int.
Structure (struct) is a very important composite type in Go language. It allows you to combine different types of data together.
type Person struct {
Name string
Age int
}
In this example, we define a Person type with two fields: Name and Age.
After you create custom types, you can use them like any other type.
func main() {
varsSeconds = 10
fmt.Println(s) // Output: 10
var p Person
p.Name = "Alice"
p.Age = 30
fmt.Println(p) // Output: {Alice 30}
}
Let’s use a small example to show how to define and use custom types and structures.
package main
import "fmt"
//Define a custom type based on int
type Counter int
//Define a structure type
type Rectangle struct {
Length, Width int
}
// Define a method for the Rectangle type
func (r Rectangle) Area() int {
return r.Length * r.Width
}
func main() {
// use custom type
var c Counter = 5
fmt.Println("Counter:", c)
//Use custom structure
rect := Rectangle{Length: 10, Width: 5}
fmt.Println("Rectangle:", rect)
fmt.Println("Area:", rect.Area())
}
In this example, we define a Counter type and a Rectangle structure. For Rectangle, we also define a method Area, which returns the area of the rectangle. Then in the main function we create and use instances of these types.
This example shows how to define and use custom types and structs in Go, and how to define methods for structs. This way you can create more complex and feature-rich data structures.
In Go, an interface is a type that specifies a set of method signatures. When a type implements these methods, it is said to implement the interface. Interfaces are a very powerful feature because they provide a way to define the behavior of objects rather than their concrete implementations. This abstraction is the basis for polymorphic and flexible design.
Interfaces are declared in Go via the interface keyword. An interface can contain multiple methods. An empty interface (interface{}) contains no methods, so all types implement the empty interface by default.
typeShape interface {
Area() float64
Perimeter() float64
}
A Shape interface is defined here, containing two methods Area and Perimeter. Any type that defines these two methods implements the Shape interface.
In Go, we don’t need to explicitly declare that a type implements an interface. If a type has all the methods of an interface, then it implements the interface.
type Rectangle struct {
Length, Width float64
}
// The Rectangle type implements the Shape interface
func (r Rectangle) Area() float64 {
return r.Length * r.Width
}
func (r Rectangle) Perimeter() float64 {
return 2 * (r.Length + r.Width)
}
Interfaces can be used to create functions that can accept many different types, as long as those types implement the interface.
// Calculate the total area of the shape
func TotalArea(shapes ...Shape) float64 {
var area float64
for _, s := range shapes {
area += s.Area()
}
return area
}
The following example shows how to define an interface, implement it, and use it in a function.
package main
import (
"fmt"
"math"
)
// Shape interface
typeShape interface {
Area() float64
Perimeter() float64
}
// Rectangle type
type Rectangle struct {
Length, Width float64
}
// Rectangle implements the Shape interface
func (r Rectangle) Area() float64 {
return r.Length * r.Width
}
func (r Rectangle) Perimeter() float64 {
return 2 * (r.Length + r.Width)
}
// Circle type
type Circle struct {
Radius float64
}
// Circle implements the Shape interface
func (c Circle) Area() float64 {
return math.Pi * c.Radius * c.Radius
}
func (c Circle) Perimeter() float64 {
return 2 * math.Pi * c.Radius
}
//The TotalArea function accepts a series of shapes that implement the Shape interface
func TotalArea(shapes ...Shape) float64 {
var area float64
for _, shape := range shapes {
area += shape.Area()
}
return area
}
func main() {
r := Rectangle{Length: 10, Width: 5}
c := Circle{Radius: 12}
fmt.Println("Total Area:", TotalArea(r, c))
}
In this example, we define the Shape interface, the Rectangle and Circle types, and then let Rectangle and Circle implement the Shape interface. The TotalArea function accepts any type array that implements the Shape interface and calculates their total area. This way, you can pass to TotalArea any shape that implements the Shape interface.
This example demonstrates how polymorphism can be achieved through interfaces, allowing you to write more flexible and extensible code.
Type assertion and reflection are two important concepts for dealing with types and values in the Go language. These two mechanisms provide the ability to inspect and manipulate values of interface types.
Type assertions are used to check the dynamic type of an interface value, or to convert an interface value to a more specific type. The syntax for type assertion is x.(T), where x is a variable of interface type and T is the type you wish to assert.
If the type assertion succeeds, it returns the value’s concrete type and a boolean value true; if it fails, it returns a zero value and false.
var i interface{} = "hello"
s, ok := i.(string)
if ok {
fmt.Println(s) // Output: hello
} else {
fmt.Println("Not a string")
}
Reflection is a powerful feature of the Go language that allows programs to inspect the types and values of objects and modify them at runtime. Go’s reflection mechanism is built on two important types: reflect.Type and reflect.Value, which represent types and values from interface values respectively.
To use reflection, you first need to import the reflect package.
You can use the reflect.TypeOf() and reflect.ValueOf() functions to get the type and value of any object.
var x float64 = 3.4
t := reflect.TypeOf(x) // Get the type of x
fmt.Println("Type:", t) // Output: Type: float64
v := reflect.ValueOf(x) // Get the value of x
fmt.Println("Value:", v) // Output: Value: 3.4
You can also modify values through reflection. To do this, you need to make sure you are using an addressable reflect.Value for the value, and then call the Set method of reflect.Value.
var y float64 = 3.4
v := reflect.ValueOf(&y) // Note: we passed a pointer to y
v.Elem().SetFloat(7.1)
fmt.Println(y) // Output: 7.1
The following examples show how to use type assertions and reflection in Go language.
package main
import (
"fmt"
"reflect"
)
func main() {
// type assertion
var i interface{} = "Hello, world!"
s, ok := i.(string)
if ok {
fmt.Println("Value:", s) // Output: Value: Hello, world!
} else {
fmt.Println("i is not a string")
}
//reflection
var x float64 = 3.4
fmt.Println("Type:", reflect.TypeOf(x)) // Output: Type: float64
fmt.Println("Value:", reflect.ValueOf(x)) // Output: Value: 3.4
//Reflection modified value
var y float64 = 3.4
v := reflect.ValueOf(&y)
v.Elem().SetFloat(7.1)
fmt.Println("New Value of y:", y) // Output: New Value of y: 7.1
}
In this example, we first demonstrate how to use type assertions to inspect and access the underlying type of an interface value. We then used reflection to check the type and value of variables and demonstrated how to modify a variable’s value. These techniques are an important part of advanced Go programming and allow programs to handle types and values more flexibly.
Now let’s dig into the Go code you provided. This code is a tool for quick type checking of OpenIM code, supporting cross-platform builds. We will analyze the main parts of this code block by block to better understand its structure and functionality.
package main
import (
//A series of imported packages
)
main package.fmt, log, os) and third-party libraries (golang.org/x/tools/go/packages).var (
//A series of global variables
)
verbose, cross, platforms, etc.).newConfig functionfunc newConfig(platform string) *packages.Config {
platSplit := strings.Split(platform, "/")
goos, goarch := platSplit[0], platSplit[1]
mode := packages.NeedName | packages.NeedFiles | packages.NeedTypes | packages.NeedSyntax | packages.NeedDeps | packages.NeedImports | packages.NeedModule
if *defuses {
mode = mode | packages.NeedTypesInfo
}
env := append(os.Environ(),
"CGO_ENABLED=1",
fmt.Sprintf("GOOS=%s", goos),
fmt.Sprintf("GOARCH=%s", goarch))
tagstr := "selinux"
if *tags != "" {
tagstr = tagstr + "," + *tags
}
flags := []string{"-tags", tagstr}
return &packages.Config{
Mode: mode,
Env: env,
BuildFlags: flags,
Tests: !(*skipTest),
}
}
newConfig function creates a new packages.Config object based on the specified platform.Decomposition of platform parameters:
platform parameter is a string in the format "GOOS/GOARCH". For example: “linux/amd64” or “darwin/arm64”.strings.Split(platform, "/") is used to split the string into two parts: operating system (goos) and architecture (goarch) .Set loading mode:
mode variable defines what information needs to be collected when loading the package. For example, packages.NeedName indicates that the name of the package is needed, packages.NeedTypes indicates that type information is needed, etc.defuses flag is true, also add packages.NeedTypesInfo to collect type information.Environment variable settings:
env is to create a new environment variable slice, based on the current system environment variables, and add CGO_ENABLED (allow CGo), GOOS and GOARCH (target platform).Build label settings:
tagstr is initially set to "selinux". If additional build tags are provided (via the tags global variable), they are added to tagstr.Build flags:
flags Slice containing build-time command line flags. Here, only the tags flag is set, with the value tagstr.Return to configuration:
packages.Config instance that contains all these settings.collector structure and related methodscollector structuretype collector struct {
dirs[]string
ignoreDirs[]string
}
collector The structure has two fields, both of which are string slices.dirs is used to store the collected directory paths.ignoreDirs is a set of directory paths that need to be ignored.newCollector functionfunc newCollector(ignoreDirs string) collector {
c := collector{
ignoreDirs: append([]string(nil), standardIgnoreDirs...),
}
if ignoreDirs != "" {
c.ignoreDirs = append(c.ignoreDirs, strings.Split(ignoreDirs, ",")...)
}
return c
}
collector instance.ignoreDirs field, first containing a standard set of ignore directories (standardIgnoreDirs), which may be defined elsewhere in the code.ignoreDirs string is provided (passed as a parameter), this string is comma-split and the result is added to the ignoreDirs slice.collector instance.walk methodfunc (c *collector) walk(roots []string) error {
for _, root := range roots {
err := filepath.Walk(root, c.handlePath)
if err != nil {
return err
}
}
sort.Strings(c.dirs)
return nil
}
walk is a method of collector that walks through a set of root directories (roots) and collects directory paths.filepath.Walk function to recursively walk each root directory. filepath.Walk requires a callback function, which is c.handlePath (not yet defined in your code snippet).walk method will return the error immediately.dirs**Sort to ensure that the order of the directory listing is consistent.verify methodfunc (c *collector) verify(plat string) ([]string, error) {
errors := []packages.Error{}
start := time.Now()
config := newConfig(plat)
rootPkgs, err := packages.Load(config, c.dirs...)
if err != nil {
return nil, err
}
// Recursively import all deps and flatten to one list.
allMap := map[string]*packages.Package{}
for _, pkg := range rootPkgs {
if *verbose {
serialFprintf(os.Stdout, "pkg %q has %d GoFiles\\n", pkg.PkgPath, len(pkg.GoFiles))
}
allMap[pkg.PkgPath] = pkg
if len(pkg.Imports) > 0 {
for _, imp := range pkg.Imports {
if *verbose {
serialFprintf(os.Stdout, "pkg %q imports %q\\n", pkg.PkgPath, imp.PkgPath)
}
allMap[imp.PkgPath] = imp
}
}
}
keys := make([]string, 0, len(allMap))
for k := range allMap {
keys = append(keys, k)
}
sort.Strings(keys)
allList := make([]*packages.Package, 0, len(keys))
for _, k := range keys {
allList = append(allList, allMap[k])
}
for _, pkg := range allList {
if len(pkg.GoFiles) > 0 {
if len(pkg.Errors) > 0 && (pkg.PkgPath == "main" || strings.Contains(pkg.PkgPath, ".")) {
errors = append(errors, pkg.Errors...)
}
}
if *defuses {
for id, obj := range pkg.TypesInfo.Defs {
serialFprintf(os.Stdout, "%s: %q defines %v\\n",
pkg.Fset.Position(id.Pos()), id.Name, obj)
}
for id, obj := range pkg.TypesInfo.Uses {
serialFprintf(os.Stdout, "%s: %q uses %v\\n",
pkg.Fset.Position(id.Pos()), id.Name, obj)
}
}
}
if *timings {
serialFprintf(os.Stdout, "%s took %.1fs\\n", plat, time.Since(start).Seconds())
}
return dedup(errors), nil
}
Initialize error list and timing:
errors of type packages.Error to store errors found during type checking.start, used to calculate the total time spent on type checking.Load configuration and packages:
config by calling the newConfig function (analyzed previously).packages.Load function to load the directory specified in c.dirs, that is, the collected Go code package.Process packages and dependencies:
allMap to store all loaded packages and their dependencies.rootPkgs and add them and their imported packages to allMap.verbose flag), print package information.Organize and traverse all packages:
keys slice containing the paths to all packages in the allMap.keys and use the keys to create an ordered list of packages allList.Check error and type information:
allList and check each package.defuses flag), print out this information.Timing and return:
timings flag), print out the elapsed time of the type check.main functionfunc main() {
flag.Parse()
args := flag.Args()
if *verbose {
*serial = true // to avoid confusing interleaved logs
}
if len(args) == 0 {
args = append(args, ".")
}
c := newCollector(*ignoreDirs)
if err := c.walk(args); err != nil {
log.Fatalf("Error walking: %v", err)
}
platforms := crossPlatforms[:]
if *platforms != "" {
platforms = strings.Split(*platforms, ",")
} else if !*cross {
plats = plats[:1]
}
var wg sync.WaitGroup
var failMu sync.Mutex
failed := false
if *serial {
*parallel = 1
} else if *parallel == 0 {
*parallel = len(plats)
}
throttle := make(chan int, *parallel)
for _, plat := range plats {
wg.Add(1)
go func(plat string) {
// block until there's room for this task
throttle <- 1
defer func() {
// indicate this task is done
<-throttle
}()
f := false
serialFprintf(os.Stdout, "type-checking %s\\n", plat)
errors, err := c.verify(plat)
if err != nil {
serialFprintf(os.Stderr, "ERROR(%s): failed to verify: %v\\n", plat, err)
f = true
} else if len(errors) > 0 {
for _, e := range errors {
// Special case CGo errors which may depend on headers we
// don't have.
if !strings.HasSuffix(e, "could not import C (no metadata for C)") {
f = true
serialFprintf(os.Stderr, "ERROR(%s): %s\\n", plat, e)
}
}
}
failMu.Lock()
failed = failed || f
failMu.Unlock()
wg.Done()
}(plat)
}
wg.Wait()
if failed {
os.Exit(1)
}
}
Parse command line parameters:
flag.Parse() Parse command line parameters.flag.Args() Get non-flag command line arguments.Set verbose mode:
verbose) is enabled, set serial to true to avoid interleaving information in the log.Process input parameters:
args is empty), the current directory "." is used as the default argument.Initialize the directory collector:
newCollector to create a new collector Example for collecting directories.Traverse the directory:
c.walk method to traverse the root directory specified by the command line parameters and collect directory paths.log.Fatalf to print the error message and exit the program.Set platform list:
crossPlatforms.platforms argument is provided, the list of platforms specified by that argument is used.cross is false), only the first platform in the list is used.Concurrency control initialization:
sync.WaitGroup is used to wait for all goroutines to complete.failMu to protect shared variables failed.serial or parallel parameters.Concurrent execution of type checking:
throttle channel to limit the number of goroutines running simultaneously.c.verify to perform type checking and updates the failed status based on the check results.Wait for all goroutines to complete:
wg.Wait() blocks until all goroutines call the Done method.Check for failures:
failed is true), exit the program with a non-zero status.main function implements a concurrent type checking tool that can handle multiple platforms at the same time.sync.WaitGroup and sync.Mutex) to control concurrent execution and synchronization.sync.WaitGroup and sync.Mutex are used in the code to control concurrency.Type checking is a mechanism used in programming languages to verify the types of variables and expressions to ensure the consistency and correctness of data. In Go language, type checking is mainly done at compile time, but in some cases it can also be done at runtime. The following are several key aspects of the Go language type checking mechanism:
Static type system:
Type inference:
:= syntax.Strong type checking:
Interface type assertion:
Reflection:
In the code you provided, a key application of type checking is through the packages package. This package allows programs to load and analyze Go code at runtime for type checking. Here are some of its uses:
Load the code package:
packages.Load function to load code packages and get detailed information about the package, including type information.Analyze dependencies:
Error reporting:
packages can report various type errors, such as unresolved references or type mismatches.packages package for type checkingHere is a simple example showing how to use packages packages for type checking:
package main
import (
"fmt"
"golang.org/x/tools/go/packages"
)
func main() {
cfg := &packages.Config{Mode: packages.NeedTypes | packages.NeedSyntax}
pkgs, err := packages.Load(cfg, "path/to/your/package")
if err != nil {
fmt.Println("Error:", err)
return
}
for _, pkg := range pkgs {
for _, err := range pkg.Errors {
fmt.Println("Type Error:", err)
}
}
}
In this example, we use the packages.Load function to load the package from the specified path and print out any type errors. This is a way to type-check your code at runtime, especially useful for building code analysis tools or compiler plugins.
Cross-platform building is the ability to build a program from one platform (such as Windows) to run on another platform (such as Linux or macOS). In the Go language, cross-platform building is a built-in feature and is very easy to implement. Here are some key points to achieve cross-platform builds:
GOOS and GOARCH environment variables. For example, GOOS=linux and GOARCH=amd64 will build the program for Linux AMD64.xxx_windows.go will only be included when building the Windows version of the program.// +build linux, and such files will only be included when building the Linux version.Here is a simple example showing how to cross-compile a Go program for Windows AMD64 architecture on a Linux system.
GOOS=windows GOARCH=amd64 go build -o myapp.exe myapp.go
In this command, we specify the target platform by setting the GOOS and GOARCH environment variables, and then execute the go build command to generate an executable for Windows AMD64 document.
By mastering these concepts and techniques, you can ensure that your Go applications run seamlessly on multiple platforms, thereby expanding your application’s usability and audience reach.
**In fact, OpenIM itself has implemented this part, especially in the Makefile system and CICD system: **
Among them, multi-architecture compilation:
make multiarch PLATFORMS="linux_s390x linux_mips64 linux_mips64le darwin_amd64 windows_amd64 linux_amd64 linux_arm64"
Build the specified binary:
make build BINS="openim-api openim-cmdutils"
In the Go language project you provided, concurrency is used to perform type checking on different platforms at the same time, thereby improving efficiency. The Go language provides powerful concurrent programming features, mainly through goroutines (lightweight threads) and channels (pipes for communication between goroutines). Here are the key practices and concepts of concurrent programming in your projects:
go keyword before the function call. In your project, this is used to enable type checking for multiple platforms simultaneously.sync.WaitGroup in your project to wait for a group of goroutines to complete. WaitGroup has three main methods: Add (increase the count), Done (decrement the count), and Wait (wait for the count to reach zero).WaitGroup is incremented. When each type check is completed, the Done method is called. The main goroutine blocks on the Wait method until all type checks are completed.sync.Mutex) to avoid race conditions.Mutex locking and unbundling when updating shared variables (such as error flags or shared logs)Lock.Here is a simplified example showing how to implement similar functionality in concurrent programming in Go.
package main
import (
"fmt"
"sync"
)
func performCheck(wg *sync.WaitGroup, platform string) {
defer wg.Done()
// Simulate type checking operation
fmt.Println("Checking platform:", platform)
//Type checking logic is performed here
}
func main() {
var wg sync.WaitGroup
platforms := []string{"linux/amd64", "darwin/amd64", "windows/amd64"}
for _, platform := range platforms {
wg.Add(1)
go performCheck(&wg, platform)
}
wg.Wait()
fmt.Println("All platform checks completed.")
}
In this example, we start a new goroutine for each platform to execute the performCheck function. sync.WaitGroup is used to wait for all checks to complete. This approach shows how to use the concurrency features of the Go language to perform tasks on multiple platforms simultaneously.
Practical exercises are key to consolidating and improving programming skills. For the Go language project you provided, we can design some practical exercises to deepen your understanding of code structure, concurrent programming, cross-platform construction and type checking mechanisms. Here are a few suggested exercises:
Added new command line parameters:
Support more platforms:
linux/arm or android/amd64.Performance optimization:
pprof, to help locate performance bottlenecks.Improved error handling:
unit test:
-Write unit tests for key functions and methods in your code to ensure they run correctly under various expected circumstances.
- Use Go’s testing package to write and run tests.
Integration Testing:
-Write integration tests to verify that the program as a whole works as expected. - Different environments and parameter combinations can be set up to test different parts of the program.
Added logging function:
log package from the standard library or a more advanced logging tool (such as zap or logrus).cobra to improve the command line interface and add functions such as help commands and command auto-completion.-Write documentation: - Write detailed documentation and usage instructions for the program. - Add clear comments to your code, especially for complex logic or parts that are not obvious.
// Copyright © 2023 OpenIM. All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
// do a fast type check of openim code, for all platforms.
package main
import (
"flag"
"fmt"
"io"
"log"
"os"
"path/filepath"
"sort"
"strings"
"sync"
"time"
"golang.org/x/tools/go/packages"
)
var (
verbose = flag.Bool("verbose", false, "print more information")
cross = flag.Bool("cross", true, "build for all platforms")
platforms = flag.String("platform", "", "comma-separated list of platforms to typecheck")
timings = flag.Bool("time", false, "output times taken for each phase")
defuses = flag.Bool("defuse", false, "output defs/uses")
serial = flag.Bool("serial", false, "don't type check platforms in parallel (equivalent to --parallel=1)")
parallel = flag.Int("parallel", 2, "limits how many platforms can be checked in parallel. 0 means no limit.")
skipTest = flag.Bool("skip-test", false, "don't type check test code")
tags = flag.String("tags", "", "comma-separated list of build tags to apply in addition to go's defaults")
ignoreDirs = flag.String("ignore-dirs", "", "comma-separated list of directories to ignore in addition to the default hardcoded list including staging, vendor, and hidden dirs")
// When processed in order, windows and darwin are early to make
// interesting OS-based errors happen earlier.
crossPlatforms = []string{
"linux/amd64", "windows/386",
"darwin/amd64", "darwin/arm64",
"linux/386", "linux/arm",
"windows/amd64", "linux/arm64",
"linux/ppc64le", "linux/s390x",
"windows/arm64",
}
// directories we always ignore
standardIgnoreDirs = []string{
// Staging code is symlinked from vendor/k8s.io, and uses import
// paths as if it were inside of vendor/. It fails typechecking
// inside of staging/, but works when typechecked as part of vendor/.
"staging",
"components",
"logs",
// OS-specific vendor code tends to be imported by OS-specific
// packages. We recursively typecheck imported vendored packages for
// each OS, but don't typecheck everything for every OS.
"vendor",
"test",
"_output",
"*/mw/rpc_server_interceptor.go",
// Tools we use for maintaining the code base but not necessarily
// ship as part of the release
"sopenim::golang::setup_env:tools/yamlfmt/yamlfmt.go:tools",
}
)
func newConfig(platform string) *packages.Config {
platSplit := strings.Split(platform, "/")
goos, goarch := platSplit[0], platSplit[1]
mode := packages.NeedName | packages.NeedFiles | packages.NeedTypes | packages.NeedSyntax | packages.NeedDeps | packages.NeedImports | packages.NeedModule
if *defuses {
mode = mode | packages.NeedTypesInfo
}
env := append(os.Environ(),
"CGO_ENABLED=1",
fmt.Sprintf("GOOS=%s", goos),
fmt.Sprintf("GOARCH=%s", goarch))
tagstr := "selinux"
if *tags != "" {
tagstr = tagstr + "," + *tags
}
flags := []string{"-tags", tagstr}
return &packages.Config{
Mode: mode,
Env: env,
BuildFlags: flags,
Tests: !(*skipTest),
}
}
type collector struct {
dirs[]string
ignoreDirs[]string
}
func newCollector(ignoreDirs string) collector {
c := collector{
ignoreDirs: append([]string(nil), standardIgnoreDirs...),
}
if ignoreDirs != "" {
c.ignoreDirs = append(c.ignoreDirs, strings.Split(ignoreDirs, ",")...)
}
return c
}
func (c *collector) walk(roots []string) error {
for _, root := range roots {
err := filepath.Walk(root, c.handlePath)
if err != nil {
return err
}
}
sort.Strings(c.dirs)
return nil
}
// handlePath walks the filesystem recursively, collecting directories,
// ignoring some unneeded directories (hidden/vendored) that are handled
// specially later.
func (c *collector) handlePath(path string, info os.FileInfo, err error) error {
if err != nil {
return err
}
if info.IsDir() {
name := info.Name()
// Ignore hidden directories (.git, .cache, etc)
if (len(name) > 1 && (name[0] == '.' || name[0] == '_')) || name == "testdata" {
if *verbose {
fmt.Printf("DBG: skipping dir %s\n", path)
}
return filepath.SkipDir
}
for _, dir := range c.ignoreDirs {
if path == dir {
if *verbose {
fmt.Printf("DBG: ignoring dir %s\n", path)
}
return filepath.SkipDir
}
}
// Make dirs into relative pkg names.
// NOTE: can't use filepath.Join because it elides the leading "./"
pkg := path
if !strings.HasPrefix(pkg, "./") {
pkg = "./" + pkg
}
c.dirs = append(c.dirs, pkg)
if *verbose {
fmt.Printf("DBG: added dir %s\n", path)
}
}
return nil
}
func (c *collector) verify(plat string) ([]string, error) {
errors := []packages.Error{}
start := time.Now()
config := newConfig(plat)
rootPkgs, err := packages.Load(config, c.dirs...)
if err != nil {
return nil, err
}
// Recursively import all deps and flatten to one list.
allMap := map[string]*packages.Package{}
for _, pkg := range rootPkgs {
if *verbose {
serialFprintf(os.Stdout, "pkg %q has %d GoFiles\n", pkg.PkgPath, len(pkg.GoFiles))
}
allMap[pkg.PkgPath] = pkg
if len(pkg.Imports) > 0 {
for _, imp := range pkg.Imports {
if *verbose {
serialFprintf(os.Stdout, "pkg %q imports %q\n", pkg.PkgPath, imp.PkgPath)
}
allMap[imp.PkgPath] = imp
}
}
}
keys := make([]string, 0, len(allMap))
for k := range allMap {
keys = append(keys, k)
}
sort.Strings(keys)
allList := make([]*packages.Package, 0, len(keys))
for _, k := range keys {
allList = append(allList, allMap[k])
}
for _, pkg := range allList {
if len(pkg.GoFiles) > 0 {
if len(pkg.Errors) > 0 && (pkg.PkgPath == "main" || strings.Contains(pkg.PkgPath, ".")) {
errors = append(errors, pkg.Errors...)
}
}
if *defuses {
for id, obj := range pkg.TypesInfo.Defs {
serialFprintf(os.Stdout, "%s: %q defines %v\n",
pkg.Fset.Position(id.Pos()), id.Name, obj)
}
for id, obj := range pkg.TypesInfo.Uses {
serialFprintf(os.Stdout, "%s: %q uses %v\n",
pkg.Fset.Position(id.Pos()), id.Name, obj)
}
}
}
if *timings {
serialFprintf(os.Stdout, "%s took %.1fs\n", plat, time.Since(start).Seconds())
}
return dedup(errors), nil
}
func dedup(errors []packages.Error) []string {
ret := []string{}
m := map[string]bool{}
for _, e := range errors {
es := e.Error()
if !m[es] {
ret = append(ret, es)
m[es] = true
}
}
return ret
}
var outMu sync.Mutex
func serialFprintf(w io.Writer, format string, a ...any) (n int, err error) {
outMu.Lock()
defer outMu.Unlock()
return fmt.Fprintf(w, format, a...)
}
func main() {
flag.Parse()
args := flag.Args()
if *verbose {
*serial = true // to avoid confusing interleaved logs
}
if len(args) == 0 {
args = append(args, ".")
}
c := newCollector(*ignoreDirs)
if err := c.walk(args); err != nil {
log.Fatalf("Error walking: %v", err)
}
platforms := crossPlatforms[:]
if *platforms != "" {
platforms = strings.Split(*platforms, ",")
} else if !*cross {
plats = plats[:1]
}
var wg sync.WaitGroup
var failMu sync.Mutex
failed := false
if *serial {
*parallel = 1
} else if *parallel == 0 {
*parallel = len(plats)
}
throttle := make(chan int, *parallel)
for _, plat := range plats {
wg.Add(1)
go func(plat string) {
// block until there's room for this task
throttle <- 1
defer func() {
// indicate this task is done
<-throttle
}()
f := false
serialFprintf(os.Stdout, "type-checking %s\n", plat)
errors, err := c.verify(plat)
if err != nil {
serialFprintf(os.Stderr, "ERROR(%s): failed to verify: %v\n", plat, err)
f = true
} else if len(errors) > 0 {
for _, e := range errors {
// Special case CGo errors which may depend on headers we
// don't have.
if !strings.HasSuffix(e, "could not import C (no metadata for C)") {
f = true
serialFprintf(os.Stderr, "ERROR(%s): %s\n", plat, e)
}
}
}
failMu.Lock()
failed = failed || f
failMu.Unlock()
wg.Done()
}(plat)
}
wg.Wait()
if failed {
os.Exit(1)
}
}
此内容由惯性聚合(RSS阅读器)自动聚合整理,仅供阅读参考。 原文来自 — 版权归原作者所有。