Go 1.19.4 命令调用、日志、包管理、反射-Day 17
1. 系统命令调用
所谓的命令调用,就是通过os,找到系统中编译好的可执行文件,然后加载到内存中,变成进程。
1.1 exec.LookPath(寻找命令)
作用:
exec.LookPath
函数用于在系统的环境变量中搜索可执行文件的路径。这个函数属于os/exec
包,通常用于查找并执行系统命令。
语法:func exec.LookPath(file string) (string, error)
参数:
file string:要查找的可执行文件的名称。返回值:
string:该返回值是可执行文件的完整路径,如果找到了的话。
error:该返回值是一个错误对象,如果搜索过程中出现错误,则返回非
nil
值。
package mainimport ("fmt""os/exec"
)func main() {// 通过LookPath,在系统PATH中找命令s, err := exec.LookPath("git")if err != nil {panic(err)} else {fmt.Println("命令在path中找到!")}
}
==========调试结果==========
命令在path中找到!
1.2 exec.Command(构造要执行的命令)
作用:
exec.Command
函数用于创建一个要执行的命令的*exec.Cmd
结构体(但并不执行)。
语法:func exec.Command(name string, arg ...string) *exec.Cmd
参数:name string:要执行的可执行文件的名称。如果该文件不在系统的 PATH 环境变量中,你需要提供完整的路径。
arg ...string:一个字符串切片,表示传递给命令的参数。第一个参数通常是子命令或命令的主要用途,后续参数是该命令的其他选项或参数。
返回值:*exec.Cmd:返回一个 *exec.Cmd 结构体,它表示要执行的命令。
package mainimport ("fmt""os/exec"
)func main() {// 通过LookPath,在系统PATH中找命令s, err := exec.LookPath("git")if err != nil {panic(err)}// 构造要执行的命令// c := exec.Command(s, "-v") // git -v// 或者还可以这样c := exec.Command(s, []string{"-v"}...)fmt.Println(c) // 打印执行的命令fmt.Println(c.Args) // 打印命令的参数列表
}
========调试结果========
D:\软件安装\Git\Git\cmd\git.exe -v
[D:\软件安装\Git\Git\cmd\git.exe -v]
1.3 Output(执行命令,并输出结果)
作用:
Output
方法用于执行命令并捕获其标准输出。这个方法会启动命令,等待命令完成,并返回命令的输出。
语法:func (*exec.Cmd).Output() ([]byte, error)
返回值:[]byte:输出结果。
error:如果执行过程中遇到错误,会返回错误信息。
package mainimport ("fmt""os/exec"
)func main() {// 通过LookPath,在系统PATH中找命令s, err := exec.LookPath("git")if err != nil {panic(err)}// 构造要执行的命令c := exec.Command(s, "-v") // git -v// 或者还可以这样// c := exec.Command(s, []string{"-v"}...)fmt.Println(c) // 打印执行的命令fmt.Println(c.Args) // 打印命令的参数列表// 执行命令,并输出结果b, err2 := c.Output()if err2 != nil {panic(err2)}fmt.Println(b)fmt.Printf("b=%v\nstring(b)=%v", b, string(b))
}
===========调试结果===========
D:\软件安装\Git\Git\cmd\git.exe -v
[D:\软件安装\Git\Git\cmd\git.exe -v]
[103 105 116 32 118 101 114 115 105 111 110 32 50 46 52 52 46 48 46 119 105 110 100 111 119 115 46 49 10]
b=[103 105 116 32 118 101 114 115 105 111 110 32 50 46 52 52 46 48 46 119 105 110 100 111 119 115 46 49 10]
string(b)=git version 2.44.0.windows.1
2. 日志
2.1 log包
Go标准库中有log包,提供了简单的日志功能。
日志输出需要使用日志记录器Logger。
2.1.1 log.Print
作用:
输出日志信息。
语法:func log.Print(v ...any)
参数:v ...any:表示接受任意数量的参数,并且每个参数可以是任何类型,多参数用逗号分隔。
package mainimport ("fmt""log"
)func main() {log.Print("xxxxx")fmt.Println("xxxx")
}
这里在ide看的比较明显,log.Print默认打印出来的是含年月日,时分秒的,且是红色字体,这是因为它用的是stderr,标准错误输出。
而fmt.Print是标准输出stdout,所以是蓝色字体。
但是颜色仅仅在ide有显示,编译后执行或linux系统中是无颜色的。
2.1.2 log.Fatal
作用:
用于输出日志信息,并且在输出日志信息后,会调用
os.Exit(1)
来终止程序的运行。
语法:func log.Fatal(v ...any)
参数:v ...any:任意数量,任意类型的参数,多个参数用逗号分隔。
2.1.3 log.Panic
作用:
用于输出日志信息,并在输出后触发一个 panic,这会导致程序立即停止运行,并开始 panic 恢复机制。
语法:func log.Panic(v ...any)
参数:同上。
package mainimport ("log"
)func main() {log.Panic("log.Panic test!!!")
}
2.2 log.Print系列源码讲解
2.2.1 日志记录器(Logger)
var std = New(os.Stderr, "", LstdFlags),注意这个std虽然是个全局变量,但小写包外不可见的。
上面我们用的log.Print系列,都是调用的这个std对象,但该std既不是标准输出也不是标准错误输出,而是一个标准logger。
因为日志的输出,是需要使用日志记录器(Logger)的,通过Logger才能输出日志,并不是直接print就行了。
Logger日志记录器也叫缺省日志记录器。
var std = New(os.Stderr, "", LstdFlags)含义:
(1)New:这是 log
包中的一个函数,用于创建一个新的 log.Logger
对象,logger实际是一个结构体。
(2)os.Stderr:这是标准库 os
包中的一个变量,表示标准错误输出(stderr)。日志信息会被输出到这个标准错误流。
(3)"":这是 New
函数的第二个参数,表示日志消息前缀。这里传入空字符串,意味着日志消息前不会有额外的前缀。
(4)LstdFlags:这是 log
包中定义的一组日志标志位,用于控制日志输出的格式。LstdFlags
通常包括时间戳、日志级别等信息。
这里注意LstdFlags,点击它看下源码:
const (// 将日志的日期设置为当地时间,格式为 2009/01/23。这是通过左移操作符(<<)和 iota 关键字实现Ldate = 1 << iota // the date in the local time zone: 2009/01/23// 将日志的时间设置为当地时间,格式为 01:23:23。Ltime // the time in the local time zone: 01:23:23// 在时间中包含微秒,格式为 01:23:23.123123,这个选项假设 Ltime 已经被设置。Lmicroseconds // microsecond resolution: 01:23:23.123123. assumes Ltime.// 在日志中包含完整的文件名和行号,例如 /a/b/c/d.go:23。Llongfile // full file name and line number: /a/b/c/d.go:23// 在日志中包含文件名的最后一个元素和行号,例如 d.go:23。如果设置了这个选项,它会覆盖 Llongfile。Lshortfile // final file name element and line number: d.go:23. overrides Llongfile// 如果设置了 Ldate 或 Ltime,使用协调世界时(UTC)而不是本地时区。LUTC // if Ldate or Ltime is set, use UTC rather than the local time zone// 将日志前缀从行的开始移动到消息之前。Lmsgprefix // move the "prefix" from the beginning of the line to before the message// 是一个组合标志,设置了日志的初始值,这里组合了 Ldate 和 Ltime。LstdFlags = Ldate | Ltime // initial values for the standard logger
)
上表列出的方法底层都使用std.Output输出日志内容。而std本质上是使用了标准错误输出、无前缀、 LstdFlags标准标记的记录器Logger实例。
2.3 std(标准输出)使用
package mainimport ("log"
)func main() {log.Print("log.Print")log.Printf("log.Printf")log.Println("log.Println")// 等价于log.Print("log.Fatal");os.Exit(1)log.Fatal("log.Fatal")// 等价于log.Println("xxx"); panic(),panic的退出状态码为2log.Panicln("log.Panicln")
}
=======调试结果=======
2024/09/29 11:44:14 log.Print
2024/09/29 11:44:14 log.Printf
2024/09/29 11:44:14 log.Println
2024/09/29 11:44:14 log.Fatal
2.4 自定义Logger
2.4.1 标准输出
package mainimport ("log""os"
)func main() {// stdout: 标准输出logger := log.New(os.Stdout, "", log.LstdFlags)logger.Println("这是自定义的标准日志输出")
}
======调试结果======
2024/09/29 13:56:22 这是自定义的标准日志输出
2.4.2 标准错误输出
package mainimport ("log""os"
)func main() {// stderr: 标准错误输出// log.LstdFlags: 年月日时分秒// log.Lshortfile:在日志中包含完整的文件名和行号logger1 := log.New(os.Stderr, "日志前缀:", log.LstdFlags|log.Lshortfile)// Fatalln: Println+os.Exit(1)logger1.Fatalln("自定义的标准错误输出。")
}
======调试结果======
日志前缀:2024/09/29 14:03:21 main.go:15: 自定义的标准错误输出。
Process 3312 has exited with status 1
这里注意两个地方:
(1)日志前缀:2024/09/29 14:03:21 main.go:15: 自定义的标准错误输出。
这里明确的显示了错误的地方在main.go:15。
(2)status 1
退出状态码为1。
然后还可以配置flag,调整日志前缀的位置。
package mainimport ("log""os"
)func main() {// stderr: 标准错误输出// log.LstdFlags: 年月日时分秒// log.Lshortfile:在日志中包含完整的文件名和行号logger1 := log.New(os.Stderr, "日志前缀:", log.LstdFlags|log.Lshortfile)// Fatalln: Println+os.Exit(1)logger1.Println("自定义的标准错误输出。")logger2 := log.New(os.Stderr, "日志前缀:", log.LstdFlags|log.Lshortfile|log.Lmsgprefix)logger2.Fatalln("自定义的标准错误输出。")
}
=========调试结果=========
日志前缀:2024/09/29 14:23:42 main.go:15: 自定义的标准错误输出。
2024/09/29 14:23:42 main.go:18: 日志前缀:自定义的标准错误输出。
2.5 日志持久化
package mainimport ("log""os"
)func main() {logfile := "D:/个人/学习/Go/文件与目录操作/test.log"f, err := os.OpenFile(logfile, os.O_APPEND|os.O_WRONLY|os.O_CREATE, os.ModePerm)if err != nil {log.Panic(err)}defer f.Close()l := log.New(f, "", log.LstdFlags|log.Lshortfile)l.Println("日志持久化文件测试!")
}
3. zerolog
官网:https://zerolog.io/
上面介绍的log模块太简陋了,实际使用并不方便。
常用的三方日志模块如下:
(1)logrus有日志级别、Hook机制、日志格式输出,很好用。
(2)zap是Uber的开源高性能日志库。
(3)zerolog更注重开发体验,高性能、有日志级别、链式API,json格式日志记录,号称0内存分配。
3.1 下载
go get -u github.com/rs/zerolog/log
3.2 简单日志示例
对于简单的日志记录,导入全局记录器包github.com/rs/zerolog/log
package main// 注意这个包的自动引入的话,需要先zerolog,等import "github.com/rs/zerolog出来后,再自动补充后面的/log
import "github.com/rs/zerolog/log"func main() {log.Print("zerolog/log Print test")log.Fatal().Msg("zerolog/log Fatal test")// 这个也是 os.Exit(1)
}
=====调试结果=====
{"level":"debug","time":"2024-09-29T17:28:09+08:00","message":"zerolog/log Print test"}
{"level":"fatal","time":"2024-09-29T17:28:09+08:00","message":"zerolog/log Fatal test"}
3.3 日志级别
zerolog提供以下级别(从高到底)
- panic (zerolog.PanicLevel, 5)
- fatal (zerolog.FatalLevel, 4)
- error (zerolog.ErrorLevel, 3)
- warn (zerolog.WarnLevel, 2)
- info (zerolog.InfoLevel, 1)
- debug (zerolog.DebugLevel, 0)
- trace (zerolog.TraceLevel, -1)
且级别还分为:
- gLevel全局级别。
zerolog.SetGlobalLevel(级别数字或常量) 来设置全局级别。
zerolog.GlobalLevel() 获取当前全局级别。- 每个Logger的级别(日志记录器级别)。
- 消息的级别(Msg)。
package mainimport ("github.com/rs/zerolog/log"
)func main() {// debug级别的消息,相当于log.Debug().Msg()log.Print("zerolog/log Print test")// fatal级别的消息,结合了os.Exit(1)log.Fatal().Msg("zerolog/log Fatal test")// Panic级别的消息log.Panic().Msg("zerolog/log Panic test")
}
3.3.1 尝试先理解“级别”
首先是我们使用的Zeerolog包中的log.xxx这种,它其实是一个缺省(默认的)的logger,比如log.Print,点这个Print看下源码:
func Print(v ...interface{}) {Logger.Debug().CallerSkipFrame(1).Msg(fmt.Sprint(v...))
}
然后再点击Logger,跳转到下一层源码:
var Logger = zerolog.New(os.Stderr).With().Timestamp().Logger()
可以看到,这里定义了一个全局的且包外可见的Logger,我们之前使用的zerrolog包中的log.xxx,实际调用的都是这个缺省Logger。
我们可以尝试着调用一下这个Logger:
package mainimport ("fmt""github.com/rs/zerolog/log"
)func main() {fmt.Println(log.Logger)
}
======调试结果======
{{0xc00000a020} -1 <nil> [123] [{}] false <nil>}
可以看到,log.Logger(缺省logger)的默认级别为 -1 trace。
那可以尝试自定义一下这个默认的Logger(日志记录器)级别:
package mainimport ("fmt""github.com/rs/zerolog""github.com/rs/zerolog/log"
)func main() {fmt.Println(log.Logger)// 自定义Logger级别(记录器级别)log1 := log.Logger.Level(zerolog.InfoLevel)fmt.Println(log1)}
========调试结果========
{{0xc00000a020} -1 <nil> [123] [{}] false <nil>}
{{0xc00000a020} 1 <nil> [123] [{}] false <nil>}
可以看到,上面自定义的级别,已经从原来的-1变成1了。
接下来试试,-1和1在一起打印。
package mainimport ("fmt""github.com/rs/zerolog""github.com/rs/zerolog/log"
)func main() {fmt.Println(log.Logger)// 自定义Logger级别(记录器级别)log1 := log.Logger.Level(zerolog.InfoLevel)fmt.Println(log1)log.Debug().Msg("log.Debug相当于是log.Print") // 缺省logger// 这一条不会打印// 该消息是debug级别,通过log1这个日志记录器输出log1.Debug().Msg("自定义日志级别的logger")
}
=======调试结果=======
{{0xc00000a020} -1 <nil> [123] [{}] false <nil>}
{{0xc00000a020} 1 <nil> [123] [{}] false <nil>}
{"level":"debug","time":"2024-09-30T15:10:15+08:00","message":"log.Debug相当于是log.Print"}
为什么log1.Debug().Msg("自定义日志级别的logger"),没有打印?
这里就涉及到另一个级别了,“消息级别”。
级别,分为“记录器级别”和“消息级别(Logger)”,我们使用log.xxx,输出的都是“消息级别”,那为啥上面的log1没有打印出来呢?
输出成功的前提:消息级别必须 >= 日志记录器级别。
我们在log1中自定义的级别是记录器级别,记录器级别为info,但log1的消息级别为debug,所以输出不了。
所以这里尝设置log1的消息级别为Info,这样消息级别和记录器级别就相等了。
package mainimport ("fmt""github.com/rs/zerolog""github.com/rs/zerolog/log"
)func main() {fmt.Println(log.Logger)// 自定义记录器日志级别log1 := log.Logger.Level(zerolog.InfoLevel)fmt.Println(log1)// 缺省logger的消息级别为debuglog.Debug().Msg("log.Debug相当于是log.Print")// 这一条不会打印,因为log1的记录器日志级别为Infolog1.Debug().Msg("自定义日志级别的logger")log1.Info().Msg("调整log1的消息级别为Info")
}
========调试结果========
{{0xc00000a020} -1 <nil> [123] [{}] false <nil>}
{{0xc00000a020} 1 <nil> [123] [{}] false <nil>}
{"level":"debug","time":"2024-09-30T15:41:36+08:00","message":"log.Debug相当于是log.Print"}
{"level":"info","time":"2024-09-30T15:41:36+08:00","message":"调整log1的消息级别为Info"}
可以看到,调整log1消息级别为Info后,终于输出了。
3.3.2 zerolog
package mainimport ("fmt""github.com/rs/zerolog"
)func main() {fmt.Println(zerolog.GlobalLevel())
}
========调试结果========
trace
可以看到,zerolog默认的级别为trace,也就是-1。
调整默认级别:
package mainimport ("fmt""github.com/rs/zerolog"
)func main() {// 3 Errorzerolog.SetGlobalLevel(zerolog.ErrorLevel)fmt.Println(zerolog.GlobalLevel())
}
=========调试结果=========
error
然后,我们结合之前的代码:
package mainimport ("fmt""github.com/rs/zerolog""github.com/rs/zerolog/log"
)func main() {// 3 Errorzerolog.SetGlobalLevel(zerolog.ErrorLevel)fmt.Println(zerolog.GlobalLevel())fmt.Println(log.Logger)log1 := log.Logger.Level(zerolog.InfoLevel)fmt.Println(log1)log.Debug().Msg("log.Debug相当于是log.Print")log1.Debug().Msg("自定义日志级别的logger")log1.Info().Msg("调整log1的消息级别为Info")log1.Error().Msg("调整log1的消息级别为Error")
}
======调试结果======
error
{{0xc00000a020} -1 <nil> [123] [{}] false <nil>}
{{0xc00000a020} 1 <nil> [123] [{}] false <nil>}
{"level":"error","time":"2024-09-30T17:02:21+08:00","message":"调整log1的消息级别为Error"}
根据上述代码发现,zerolog.SetGlobalLevel(zerolog.ErrorLevel)后,原来log和log1的debug和info级别的消息都不打印了,只有log1的error级别打印。
这说明SetGlobalLevel,控制所有Logger的输出级别,只有>=GlobalLevel的消息级别,才能输出。
那也就是说,消息级别 >= MAX(Logger(记录器), GlobalLevel),才能最终输出。
3.3.3 消息与日志记录器
首先消息需要通过日志记录器才能输出,不管是输出到屏幕还是文件。
如:log1.Info().Msg()。
消息(Msg()),是Info级别(Info())的,通过log1这个日志记录器输出。
但消息输出的前提:消息级别 >= MAX(Logger(记录器,此处是log1自己), GlobalLevel),才能最终输出。
3.4 上下文
zerolog是以Json对象格式输出的,还可以自定义一些键值对字段增加到上下文中以输出。
3.4.1 自定义字段
package mainimport ("github.com/rs/zerolog""github.com/rs/zerolog/log"
)func main() {zerolog.SetGlobalLevel(zerolog.InfoLevel)log.Info().Msg("")
}
============调试结果============
{"level":"info","time":"2024-10-08T11:45:25+08:00"}
从上面的输出可以看到,Msg为空,实际打印出来的消息体中也没有mssage。
那我们尝试着自定义一下:
package mainimport ("github.com/rs/zerolog""github.com/rs/zerolog/log"
)func main() {zerolog.SetGlobalLevel(zerolog.InfoLevel)log.Info().Str("School", "magedu.com").Msg("")log.Info().Str("School", "magedu.com").Bool("Address", true).Msg("")
}
============调试结果============
{"level":"info","School":"magedu.com","time":"2024-10-08T14:31:34+08:00"}
{"level":"info","School":"magedu.com","Address":true,"time":"2024-10-08T14:31:34+08:00"}
可以看到,可以通过Str来自定义内容,且Str可以多个(还可以使用其他数据类型)。
3.5 错误日志
package mainimport ("errors""github.com/rs/zerolog""github.com/rs/zerolog/log"
)func main() {zerolog.SetGlobalLevel(zerolog.InfoLevel)err := errors.New("test error")log.Error().Msg(err.Error())log.Error().Err(err).Msg("")log.Error().Err(err).Send()log.Fatal().Err(err).Send()log.Panic().Err(err).Send()
}
======调试结果======
{"level":"error","time":"2024-10-09T10:29:59+08:00","message":"test error"}
{"level":"error","error":"test error","time":"2024-10-09T10:29:59+08:00"}
{"level":"error","error":"test error","time":"2024-10-09T10:29:59+08:00"}
{"level":"fatal","error":"test error","time":"2024-10-09T10:29:59+08:00"}
package mainimport ("errors""github.com/rs/zerolog""github.com/rs/zerolog/log" // 全局logger
)func main() {zerolog.TimeFieldFormat = zerolog.TimeFormatUnix // 自定义time字段时间的格式,TimeFormatUnix时间戳// zerolog.ErrorFieldName = "err" // 修改日志Json中的缺省字段名error// 错误日志err := errors.New("自定义的错误")log.Error(). // 错误级别消息Err(err). // err字段,错误消息内容Send() // 有错误消息了,message可以省略log.Fatal(). // fatal级别Err(err).Send()
}
3.6 自定义全局logger
3.6.1 全局logger
// Logger is the global logger.
var Logger = zerolog.New(os.Stderr).With().Timestamp().Logger()
默认使用是这样的:
package mainimport ("github.com/rs/zerolog/log"
)func main() {log.Debug().Msg("debug")log.Error().Msg("Error")
}
============调试结果============
{"level":"debug","time":"2024-10-09T10:44:33+08:00","message":"debug"}
{"level":"error","time":"2024-10-09T10:44:33+08:00","message":"Error"}
3.6.2自定义全局Logger
不建议直接修改默认的全局logger,如果有需求,最好自定义一个。
package mainimport ("github.com/rs/zerolog/log"
)func main() {log.Logger = log.With().Str("School", "haha").Logger()log.Debug().Msg("debug")log.Error().Msg("Error")
}
========调试结果========
{"level":"debug","School":"haha","time":"2024-10-09T10:46:11+08:00","message":"debug"}
{"level":"error","School":"haha","time":"2024-10-09T10:46:11+08:00","message":"Error"}
package mainimport ("github.com/rs/zerolog""github.com/rs/zerolog/log"
)func main() {zerolog.TimeFieldFormat = zerolog.TimeFormatUnixlogger := log.With(). // With()返回基于全局Logger的子loggerStr("School", "Magedu").Caller(). // 增加日志调用的位置信息字段Logger() // 返回Loggerlogger.Info().Send() // {"level":"info","School":"Magedu","time":1223947070}log.Info().Send() // {"level":"info","time":1223947070} 全局Logger
}
package mainimport ("fmt""os""github.com/rs/zerolog""github.com/rs/zerolog/log"
)func main() {zerolog.TimeFieldFormat = zerolog.TimeFormatUnixlogger := zerolog.New(os.Stdout). // 不基于全局Logger,重新构造了一个LoggerWith().Str("School", "Magedu").Caller(). // 调用者信息:增加日志函数调用的位置信息字段Logger(). // 返回LoggerLevel(zerolog.ErrorLevel) // 重新定义Logger级别为3 error,返回Loggerfmt.Println(logger.GetLevel())logger.Info().Send() // {"level":"info","School":"Magedu","time":1223947070}看颜色区别logger.Error().Send()log.Info().Send() // {"level":"info","time":1223947070} 全局Logger
}
3.7 写入日志文件
3.7.1 只写入文件
package mainimport ("fmt""os""github.com/rs/zerolog""github.com/rs/zerolog/log"
)func main() {f, err := os.OpenFile("D:/个人/学习/Go/文件与目录操作/test.log", os.O_WRONLY|os.O_APPEND|os.O_CREATE, os.ModePerm)if err != nil {log.Panic().Err(err).Send()}defer f.Close()log1 := zerolog.New(f)log1.Info().Msg("测试写入")
}
3.7.2 写入文件,并输出到控制台
package mainimport ("os""github.com/rs/zerolog""github.com/rs/zerolog/log"
)func main() {f, err := os.OpenFile("D:/个人/学习/Go/文件与目录操作/test.log", os.O_WRONLY|os.O_APPEND|os.O_CREATE, os.ModePerm)if err != nil {log.Panic().Err(err).Send()}defer f.Close()// log1 := zerolog.New(f)// log1.Info().Msg("测试写入")// 一分为二,一边写文件,一边控制台输出lw := zerolog.MultiLevelWriter(f, os.Stdout)log := zerolog.New(lw).With().Timestamp().Str("school", "haha").Logger().Level(zerolog.InfoLevel)log.Info().Send()
}
=======调试结果=======
{"level":"info","school":"haha","time":"2024-10-09T17:46:17+08:00"}
4. 包管理
用任何语言来开发,如果软件规模扩大,会编写大量的函数、结构体、接口等代码,这些代码不可能写在一个文件中,这就会产生大量的文件。
如果这些文件杂乱无章,就会造成名称冲突、重复定义、难以检索、无法引用、共享不便、版本管理等一系列问题。
如:有一些功能模块如何复用,如何共享方便其他项目使用。
所以,一定要有模块化(包)管理,解决以上诸多问题。
4.1 包
- 包由多个文件和目录组成。
- 使用 package <包名> 来定义包名。
- 包名一般都采用小写,符合标识符要求。
- 当前目录名和 package <包名> 中的包名不需要一致,但最好保持一致。
- 同级文件归属一个包,就是说每个包目录的当前目录中,只能统一使用同一个package的包名,否则编译出错。
一般来说,开发项目时,可以把相关功能的代码集中放在某个包里面,例如在main包目录中新建一个calc包,将所有计算函数都放在其中,以供别的代码调用。
同一个目录就是同一个包,该包内go文件里的变量、函数、结构体互相可见,可以直接使用。
跨目录就是跨包,使用时需要导入别的包,导入需要指定该包的路径。
4.1.1 包内可见演示
先在main.go文件中定义2个全局变量:
package main// 定义一个小写全局变量,包内(main包内)可见
var a = 100// 大写全局变量,包外(main包外)可见
var B = 200func main() {}
再在main.go文件同级目录中,创建一个x.go文件,并调用a和B变量:
package mainimport "fmt"// 这个test函数,也属于包内可见函数,如果首字母大写,那就是包外可见
func test() {fmt.Println(a, B)// 因为该文件也是属于main包的,所以可以调用aB变量
}
回到main.go文件中,调用test函数:
package main// 定义一个小写全局变量,包内(main包内)可见
var a = 100// 大写全局变量,包外(main包外)可见
var B = 200func main() {test()
}
=====调试结果=====
100 200
4.2 包管理的发展历程
4.2.1 GOPATH
Go 1.11版本之前,项目依赖包存于GOPATH。GOPATH是一个环境变量,指向一个目录,其中存放项 目依赖包的源码。
GOPATH默认值是 家目录/go 。
开发的代码放在 GOPATH/src 目录中,编译这个目录的代码,生成的二进制文件放到 GOPATH/bin 目录 下。
这会有以下问题:
- GOPATH不区分项目,代码中任何import的路径均从GOPATH作为根目录开始。如果有多个项目,不同项目依赖不同库的不同版本,这就很难解决了。
- 所有项目的依赖都放在GOPATH中,很难知道当前项目的依赖项是哪些。
4.2.2 GOPATH + vendor机制
Go1.5引入vendor机制。
vendor:将项目依赖包复制到项目下的vendor目录,在编译时使用项目下的vendor目录的包进行编译。
但依然不能解决不同项目依赖不同包版本问题。
该方式下的包搜索顺序
在当前包vendor目录查找向上级目录查找,直到GOPATH/src/vendor目录
在GOPATH目录查找
在GOROOT目录查找标准库
4.2.3 Go Modules(官方解决方案)
Go Modules是从Go 1.11版本引入,到1.13版本之后已经成熟,Go Modules成为官方的依赖包管理解决方案。
优势:
- 不受GOPATH限制,代码可放在任意目录。
- 自动管理和下载依赖,且可以控制使用版本。
- 不允许使用相对导入。
4.2.3.1 go modules 初始化
之前也演示过,我们最开始写好代码后,需要执行一段命令,来初始化我们的代码:
go mod init 自定义模块名
执行完后,会在当前目录下生成一个go.mod文件,且里面会包含我们使用的第三方库。这样每个目录都有自己的go.mod,就不用担心冲突啥的。
如果想改模块名,可以手动修改,然后当前目录下所有文件都属于该模块。
5. Module模式
5.1 go mod命令
在Go1.11开始引入,可以在任何目录使用go.mod创建项目。
- go mod init name 命令,在当前文件夹下初始化一个新的module, 创建go.mod文件。
- go mod tidy 命令自动分析依赖,下载缺失的模块,移除未使用的模块,并更新go.mod文件。
5.1.1 go mod tidy演示
比如我下面这个代码,引入了一些包:
在go.mod中就是这样的:
包括还有go.sum中也会生成内容:
那如果我们不需要这些东西了,该怎么办呢?可以如下操作。
5.1.1.1 清理无用依赖
执行go mod tidy
5.1.1.2 恢复依赖
上面的报错是说,这个包还在,但是go.mod中没有,所以没有办法使用。
解决办法如下:
上面没有//indirect的是直接依赖,有//indirect的是间接依赖,就是直接依赖需要的依赖包。
5.2 导入子包
5.2.1 创建子目录和文件
package calc // 这个包名可以自定义,但是建议和目录名一致。import "fmt"// 注意首字母大写,才能包外可见
func Add(x, y int) int {fmt.Printf("这是calc.go里面打印的!")return x + y
}
这里注意,同一级目录中的.go文件,package名必须一致。
5.2.2 在main包中调用calc.go中的Add函数
package mainimport (// 注意这个test,一定要是go.mod中的module name"test/calc"
)func main() {calc.Add(1, 2)
}
===========调试结果===========
这是calc.go里面打印的!
5.2.3 在子包中创建子包
在子目录中创建子目录和代码文件
5.2.4 调用子包中的子包
5.3 import导入包
5.3.1 绝对导入
就是下面这种,包都是绝对路径的。
package mainimport (// 注意这个test,一定要是go.mod中的module name"test/calc""test/calc/minus"
)
5.3.2 别名导入
如果有两个导入的包冲突时,可以重命名包来避免冲突
import m "magedu.com/tools/calc/minus"// 使用举例
m.Minus()
5.3.3 相对导入
不建议使用
import "./calc"
5.3.4 匿名导入
import _ "magedu.com/tools/calc/minus"
使用下划线作为别名,就意味着无法使用了,那其目的何在?
这种情况下,只能执行导入的包内的所有init函数了。主要作用是做包的初始化用。
5.4 导入本地私有包到其它项目
就是自己写的包,想在其他项目用,可以按照如下步骤进行导入。
5.4.1 创建模拟用的本地私有包
比如说我在这个目录下创建了新的代码文件:
然后写了新的代码
package calcimport "fmt"func Multply(x, y int) int {fmt.Println("这是新的私有的calc包!!!")return x * y
}
5.4.2 在mian.go中使用新的这个calc包
先在main.go中定义导入的包,可以随便写一个包名:
package mainimport ("tools/abcd" // 随便写
)
修改go.mod
但此时注意,module test这里报错了:
意思是在新的这个包路径中,找不到go.mod,所以需要初始化一下。
PS D:\个人\学习\Go\私有包\calc> go mod init calc
go: creating new go.mod: module calc
go: to add module requirements and sums:go mod tidy
在main.go中使用这个新的私有包
package mainimport (c "tools/abcd" // c相当于是包的别名
)func main() {c.Multply(2, 3)
}
========调试结果========
这是新的私有的calc包!!!
5.5 导入第三方包
这个不做演示了,官放文档都有,直接go get -u下载就行了。前面的文章也有演示,百度也大把。
6. init函数
6.1 init函数的作用
init函数主要是配合匿名导入使用,目的不是为了让你使用包内的资源,而是运行该包所有的init。
所有的包中,都可以定义init函数。
注意:
- init函数在本包内可以有n多个,但是每个.go文件中只能有1个。
- 同一个包内的多个init函数,执行顺序是没有办法保证的。
- 不同包的init函数的执行顺序由导入顺序决定
- init函数,无参无返回值,不能被其他函数调用。
- 包中的init函数将在main函数之前自动执行。
6.2 代码演示
6.2.1 在子包中创建init函数
6.2.1.1 minus.go
6.2.1.2 calc.go
6.2.2 在main.go中调用
package mainimport ("fmt"_ "test/calc"_ "test/calc/minus"
)func main() {fmt.Println("这是main.go中的init函数测试!!!")
}
=========调试结果=========
这是calc/calc.go中的init函数!!!
这是calc/minus/minus.go中的init函数!!!
这是main.go中的init函数测试!!!
7. 反射
反射有很大的弊端,这里略过了。
反射的弊端:
- 代码难以阅读,难以维护。
- 编译期间不能发现类型错误,覆盖测试难度很大,有些Bug需要线上运行时才可能发现,并造成严 重后果。
- 反射性能很差,通常比正常代码慢一到两个数量级。如果性能要求高时,或反复调用的代码块里建 议不要使用反射。
反射主要应用场合就是写库或框架,一般用不到,再一个面试时候极低概率被问到。
相关文章:

Go 1.19.4 命令调用、日志、包管理、反射-Day 17
1. 系统命令调用 所谓的命令调用,就是通过os,找到系统中编译好的可执行文件,然后加载到内存中,变成进程。 1.1 exec.LookPath(寻找命令) 作用: exec.LookPath 函数用于在系统的环境变量中搜索可…...

Unity 2d UI 实时跟随场景3d物体
2d UI 实时跟随场景3d物体位置,显示 3d 物体头顶信息,看起来像是场景中的3dUI,实质是2d UIusing System.Collections; using System.Collections.Generic; using UnityEngine; using DG.Tweening; using UnityEngine.UI; /// <summary>…...

全方面熟悉Maven项目管理工具(二)坐标、pom.xml文件的解读!
1. 坐标(核心概念) 1.1 数学中的坐标 使用 x、y、z 三个向量作为空间的坐标系,可以在空间中唯一的定位到一个点 1.2 Maven 中的坐标 1.2.1 向量说明: 使用三个向量在 Maven的仓库 中唯一的定位到一个 jar 包 groupId…...
php常用设计模式之单例模式
设计模式是我们日常开发中最常用的编程模式之一,也是面试中最高频的考点之一。通过合理运用设计模式,可以使代码结构更加清晰、易于维护。通过这篇文章 我也讲一下设计模式中的单例模式,了解下它的原理和适用场景。 单例模式 单例模式&…...
一文搞懂Android主题和样式
1. 概念与作用 1.1 定义与组成 Android Theme是用于定义应用程序或其部分的视觉和界面风格的一种资源。主题在Android中扮演着重要的角色,它们允许开发者统一和自定义应用的外观和感觉。一个主题定义了一组属性集合,这些属性可以是颜色、字体、控件样式…...

360与重庆科技大学战略携手,为数字中国建设输送实战人才
近日,360数字安全集团与重庆科技大学正式签订了战略合作协议,双方将围绕创新人才培养、科研技术攻关、专业实验室共建、车联网安全以及社会服务等多个维度展开深度合作,共同打造数字安全人才培养新高地,为数字重庆的建设与发展注入…...

基于异常合成的图像异常检测方法
基于异常合成的图像异常检测方法 基于异常合成思路实现图像异常检测的方法,它们的核心思路是: 试图通过合成异常样本穷尽所有可能出现的异常类型,从而将无监督的异常检测(one class classification)建模为一个全监督…...
机器学习方向在算法优化上有哪些创新点?
以下是机器学习算法优化方面的一些创新点: 一、优化算法自身的改进 随机梯度下降(SGD)的变体 Adagrad 传统的随机梯度下降算法使用固定的学习率,而Adagrad根据每个参数的历史梯度信息自适应地调整学习率。对于稀疏数据,它可以为不同的参数分配不同的学习率,使得频繁出现…...

基于yolov8的道路交通事故检测系统python源码+onnx模型+评估指标曲线+精美GUI界面
【算法介绍】 基于YOLOV8的道路交通事故检测系统是一种先进的智能交通监控解决方案,它利用YOLOV8这一前沿的目标检测算法,实现对交通事故的高效、实时检测。 该系统结合了自定义的道路交通事故数据集,对YOLOV8模型进行了针对性的训练与优化…...
HttpUtils 详解
一、详解 1.1 介绍 现如今的 Web 项目,由服务端向外发起网络请求的场景,基本上随处可见! 传统情况下,在服务端代码里访问 http 服务时,一般会使用 JDK 的 HttpURLConnection 或者 Apache 的 HttpClient,不…...

云计算第四阶段: cloud二周目 07-08
cloud 07 一、k8s服务管理 创建服务 # 资源清单文件 [rootmaster ~]# kubectl create service clusterip websvc --tcp80:80 --dry-runclient -o yaml [rootmaster ~]# vim websvc.yaml --- kind: Service apiVersion: v1 metadata:name: websvc spec:type: ClusterIPselector…...
智能合约开发工具Remix
不久前,我在B站、视频号和 YouTube 都陆续发布了新的一套免费视频课程《智能合约开发工具Remix》,总共分为了 9 个小节的视频,以下分别是这 9 个小节在 B 站的视频链接,喜欢直接看视频的伙伴可以去 B 站观看: 概况文件…...

YYF桌面 1.2 | 个性化桌面体验,清爽美观。
YYF桌面是一款经过定制的安卓桌面启动器,此版本基于1.2版本进行了一系列个性化修改。主界面快捷图标进行了美化处理,并采用了清爽半透明的图标背景。取消了底部快捷键,并重新制作了“全部应用”按钮,保留了动态效果。修改了右上角…...

【人工智能/计算机工程/大数据】第五届人工智能与计算工程国际学术会议(ICAICE 2024,2024年11月8-10日)
The 5th International Conference on Artificial Intelligence and Computer Engineering 第五届人工智能与计算工程国际学术会议(ICAICE 2024) 会议官网:www.event-icaice.org The 5th International Conference on Artificial Intellige…...
uni-app录音功能
纯纯干货,cv即用 <template><!-- 录音页面 --><view class"page"><view class"tape_box"><view class"upload_box1"><view class"upload_top"><view class"upload_img_title…...

C语言【调试】(个人笔记版)
调试 前言一、Bug二、调试工具1.DeBug2.Release 三、调试快捷键1、断点 四、调试时查看程序的当前信息1、查看临时变量2、查看内存3、查看调用堆栈、汇编、寄存器 总结 前言 这篇文章大都是我的个人笔记: 调试在日常程序设计中是很重要的。调试说白了就是为了解决代…...

连锁收银系统
商淘云连锁管理系统助力连锁企业实现“人货账”全方位数字化管理,它依托连锁品牌进销存管理实现门店订货、线下收银、线上商城、会员营销等一体化管理。 门店订货补货支持连锁直营、加盟 不同门店不同进货价、不同门店不同商品、不同门店在线或者账期支付、门店PC或…...
5.2024.10.21
2024.10.21 学习和复习 学习QT的流程控制、函数、内联函数复习C语言printf练习 问题及解决方案 C学到函数之后有些吃力代码逻辑能力不大行,需要巩固一下C语言基础再挤多点时间去学习嵌入式写代码前先把大概思路写出来 碎碎念 最近作业比较多,有点静不下…...
前端Socket互动小游戏开发体验分享
随着实时网络通信技术的不断发展,基于WebSocket的前端互动小游戏成为了一种非常流行的选择。WebSocket允许客户端和服务器之间进行双向通信,为游戏互动带来了更快的响应时间和更流畅的体验。本文将通过一个简单的互动小游戏来探讨前端如何利用WebSocket技…...
react项目,通过自定义 separator 属性来修改日期选择器中间的分隔符:
1. 引入必要的依赖 确保你已经引入了 DatePicker 组件和 moment 库。 import React, { Component } from react; import { DatePicker } from antd; import moment from moment; const { RangePicker } DatePicker; const dateFormat "YYYY/MM/DD"; 2. 定义父组…...
[2025CVPR]DeepVideo-R1:基于难度感知回归GRPO的视频强化微调框架详解
突破视频大语言模型推理瓶颈,在多个视频基准上实现SOTA性能 一、核心问题与创新亮点 1.1 GRPO在视频任务中的两大挑战 安全措施依赖问题 GRPO使用min和clip函数限制策略更新幅度,导致: 梯度抑制:当新旧策略差异过大时梯度消失收敛困难:策略无法充分优化# 传统GRPO的梯…...

边缘计算医疗风险自查APP开发方案
核心目标:在便携设备(智能手表/家用检测仪)部署轻量化疾病预测模型,实现低延迟、隐私安全的实时健康风险评估。 一、技术架构设计 #mermaid-svg-iuNaeeLK2YoFKfao {font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}#mermaid-svg…...
深入浅出:JavaScript 中的 `window.crypto.getRandomValues()` 方法
深入浅出:JavaScript 中的 window.crypto.getRandomValues() 方法 在现代 Web 开发中,随机数的生成看似简单,却隐藏着许多玄机。无论是生成密码、加密密钥,还是创建安全令牌,随机数的质量直接关系到系统的安全性。Jav…...
聊一聊接口测试的意义有哪些?
目录 一、隔离性 & 早期测试 二、保障系统集成质量 三、验证业务逻辑的核心层 四、提升测试效率与覆盖度 五、系统稳定性的守护者 六、驱动团队协作与契约管理 七、性能与扩展性的前置评估 八、持续交付的核心支撑 接口测试的意义可以从四个维度展开,首…...

学习STC51单片机32(芯片为STC89C52RCRC)OLED显示屏2
每日一言 今天的每一份坚持,都是在为未来积攒底气。 案例:OLED显示一个A 这边观察到一个点,怎么雪花了就是都是乱七八糟的占满了屏幕。。 解释 : 如果代码里信号切换太快(比如 SDA 刚变,SCL 立刻变&#…...
DeepSeek 技术赋能无人农场协同作业:用 AI 重构农田管理 “神经网”
目录 一、引言二、DeepSeek 技术大揭秘2.1 核心架构解析2.2 关键技术剖析 三、智能农业无人农场协同作业现状3.1 发展现状概述3.2 协同作业模式介绍 四、DeepSeek 的 “农场奇妙游”4.1 数据处理与分析4.2 作物生长监测与预测4.3 病虫害防治4.4 农机协同作业调度 五、实际案例大…...
Mobile ALOHA全身模仿学习
一、题目 Mobile ALOHA:通过低成本全身远程操作学习双手移动操作 传统模仿学习(Imitation Learning)缺点:聚焦与桌面操作,缺乏通用任务所需的移动性和灵活性 本论文优点:(1)在ALOHA…...

Mysql中select查询语句的执行过程
目录 1、介绍 1.1、组件介绍 1.2、Sql执行顺序 2、执行流程 2.1. 连接与认证 2.2. 查询缓存 2.3. 语法解析(Parser) 2.4、执行sql 1. 预处理(Preprocessor) 2. 查询优化器(Optimizer) 3. 执行器…...

springboot整合VUE之在线教育管理系统简介
可以学习到的技能 学会常用技术栈的使用 独立开发项目 学会前端的开发流程 学会后端的开发流程 学会数据库的设计 学会前后端接口调用方式 学会多模块之间的关联 学会数据的处理 适用人群 在校学生,小白用户,想学习知识的 有点基础,想要通过项…...

mac 安装homebrew (nvm 及git)
mac 安装nvm 及git 万恶之源 mac 安装这些东西离不开Xcode。及homebrew 一、先说安装git步骤 通用: 方法一:使用 Homebrew 安装 Git(推荐) 步骤如下:打开终端(Terminal.app) 1.安装 Homebrew…...