Go 1.19.4 命令调用、日志、包管理、反射-Day 17

news2024/11/21 1:44:12

1. 系统命令调用

所谓的命令调用,就是通过os,找到系统中编译好的可执行文件,然后加载到内存中,变成进程。

1.1 exec.LookPath(寻找命令)

作用:

        exec.LookPath 函数用于在系统的环境变量中搜索可执行文件的路径。这个函数属于 os/exec 包,通常用于查找并执行系统命令。

语法:

        func exec.LookPath(file string) (string, error)
参数:
        file string:要查找的可执行文件的名称。

返回值:

        string:该返回值是可执行文件的完整路径,如果找到了的话。

        error:该返回值是一个错误对象,如果搜索过程中出现错误,则返回非 nil 值。

package main

import (
	"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 main

import (
	"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 main

import (
	"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 main

import (
	"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 main

import (
	"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 main

import (
	"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的退出状态码为2
	log.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 main

import (
	"log"
	"os"
)

func main() {
	// stdout: 标准输出
	logger := log.New(os.Stdout, "", log.LstdFlags)

	logger.Println("这是自定义的标准日志输出")
}
======调试结果======
2024/09/29 13:56:22 这是自定义的标准日志输出

2.4.2 标准错误输出

package main

import (
	"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 main

import (
	"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 main

import (
	"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 main

import (
	"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 main

import (
	"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 main

import (
	"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 main

import (
	"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 main

import (
	"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的消息级别为debug
	log.Debug().Msg("log.Debug相当于是log.Print")

	// 这一条不会打印,因为log1的记录器日志级别为Info
	log1.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 main

import (
	"fmt"

	"github.com/rs/zerolog"
)

func main() {
	fmt.Println(zerolog.GlobalLevel())
}
========调试结果========
trace

可以看到,zerolog默认的级别为trace,也就是-1。

调整默认级别:

package main

import (
	"fmt"

	"github.com/rs/zerolog"
)

func main() {
	// 3 Error
	zerolog.SetGlobalLevel(zerolog.ErrorLevel)
	fmt.Println(zerolog.GlobalLevel())
}
=========调试结果=========
error

然后,我们结合之前的代码:

package main

import (
	"fmt"

	"github.com/rs/zerolog"
	"github.com/rs/zerolog/log"
)

func main() {
	// 3 Error
	zerolog.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 main

import (
	"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 main

import (
	"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 main

import (
	"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 main

import (
	"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 main

import (
	"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 main

import (
	"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 main

import (
	"github.com/rs/zerolog"
	"github.com/rs/zerolog/log"
)

func main() {
	zerolog.TimeFieldFormat = zerolog.TimeFormatUnix
	logger := log.With(). // With()返回基于全局Logger的子logger
				Str("School", "Magedu").
				Caller(). // 增加日志调用的位置信息字段
				Logger()  // 返回Logger
	logger.Info().Send() // {"level":"info","School":"Magedu","time":1223947070}
	log.Info().Send()    // {"level":"info","time":1223947070} 全局Logger
}
package main

import (
	"fmt"
	"os"

	"github.com/rs/zerolog"
	"github.com/rs/zerolog/log"
)

func main() {
	zerolog.TimeFieldFormat = zerolog.TimeFormatUnix
	logger := zerolog.New(os.Stdout). // 不基于全局Logger,重新构造了一个Logger
						With().Str("School", "Magedu").
						Caller().                 // 调用者信息:增加日志函数调用的位置信息字段
						Logger().                 // 返回Logger
						Level(zerolog.ErrorLevel) // 重新定义Logger级别为3 error,返回Logger
	fmt.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 main

import (
	"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 main

import (
	"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 = 200

func main() {

}

再在main.go文件同级目录中,创建一个x.go文件,并调用a和B变量:

package main

import "fmt"

// 这个test函数,也属于包内可见函数,如果首字母大写,那就是包外可见
func test() {
	fmt.Println(a, B)// 因为该文件也是属于main包的,所以可以调用aB变量
}

回到main.go文件中,调用test函数:

package main

// 定义一个小写全局变量,包内(main包内)可见
var a = 100

// 大写全局变量,包外(main包外)可见
var B = 200

func 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 main

import (
	// 注意这个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 main

import (
	// 注意这个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 calc

import "fmt"

func Multply(x, y int) int {
	fmt.Println("这是新的私有的calc包!!!")
	return x * y
}

5.4.2 在mian.go中使用新的这个calc包

先在main.go中定义导入的包,可以随便写一个包名:

package main

import (
	"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 main

import (
	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 main

import (
	"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需要线上运行时才可能发现,并造成严 重后果。
  • 反射性能很差,通常比正常代码慢一到两个数量级。如果性能要求高时,或反复调用的代码块里建 议不要使用反射。

反射主要应用场合就是写库或框架,一般用不到,再一个面试时候极低概率被问到。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/2213800.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

【每日刷题】Day139

51. N 皇后 - 力扣&#xff08;LeetCode&#xff09; //思路&#xff1a;DFS哈希。 //本题的难点就在于剪枝操作。 class Solution { public: int n; vector<vector<string>> ans; vector<string> path; bool cols[10], dig1[20], dig2[20]; void dfs(int ro…

【无标题】基于情境依赖因果影响的多智能体协作强化学习

、文章探讨了大型语言模型&#xff08;LLMs&#xff09;&#xff0c;例如GPT-4&#xff0c;是否以及在何种意义上拥有知识。作者认为&#xff0c;这些模型展现了一种称为“工具性知识”的能力&#xff0c;这种知识允许它们根据输入上下文推断任务结构&#xff0c;并在此基础上进…

appium启动hbuild打包的apk异常解决

目录 一、错误信息 二、问题解决 2.1 通过以下命令获取安装包名称&#xff1a; 2.2 这个launcher状态下的安装包名称和active&#xff0c;替换原先的安装包名称 一、错误信息 通过adb shell dumpsys activity | findstr "mResume" 命令获取的安装包信息&#xff…

2024年公路水运工程施工企业主要负责人证模拟考试题库及公路水运工程施工企业主要负责人理论考试试题

题库来源&#xff1a;安全生产模拟考试一点通公众号小程序 2024年公路水运工程施工企业主要负责人证模拟考试题库及公路水运工程施工企业主要负责人理论考试试题是由安全生产模拟考试一点通提供&#xff0c;公路水运工程施工企业主要负责人证模拟考试题库是根据公路水运工程施…

芯课堂 | 使用 SWM341 系列 MCU 环境配置

SWM341系列MCU调试环境配置 SWM341 是华芯微特的其中一款 MCU&#xff0c;341 和 341内置 SDRAM 的 34S 系列&#xff0c;其内核相同。 芯片使用安谋科技“星辰”STAR-MC1 内核,基于 Arm-V8 指令集架构&#xff0c;下载烧录选 M33&#xff0c;对应的工具需要升级; 1、使用 KE…

MySQL中库的操作

目录 前言 一、查看数据库 1.1、语法 二、创建数据库 2.1、语法 2.2、自定义⼀个数据库 2.2.1、创建一个名为“aokey”的数据库 三、字符集编码和校验(排序)规则 3.1、查看数据库支持的字符集编码 3.2、查看数据库支持的排序规则 3.3、不同的字串集与排序规则对数…

芯片供电引脚为什么要放一个104电容?

每个芯片的电源都要接一个104电容是为什么? 这颗电容叫高频旁路电容&#xff0c;作用就是把电源中的高频杂波对地短路防止高频污染&#xff0c;降低电源输入对芯片的影响。 接地滤波电容&#xff0c;如果没有&#xff0c;杂波干扰非常大&#xff0c;有了这个滤波电容&#x…

Json-Rpc框架(项目设计 —— 服务端客户端 模块功能划分简介)

阅读导航 引言一、理解项目功能⭕分布式RPC服务发现/注册/订阅/创建 结构图⭕项目三大核心功能 二、服务端模块划分简介三、客户端模块划分简介 引言 在上一篇文章中&#xff0c;我们深入探讨了Json-Rpc框架中不可或缺的序列化与反序列化利器——JsonCpp库。 在本篇文章中&am…

Python酷玩之旅_如何正确使用pipreqs完成依赖导出(解决UnicodeDecodeError)

导览 前言Q&#xff1a;如何正确使用pipreqs1. 安装库2. 命令详解2.1 options2.2 path 3. 实践与问题 结语系列回顾 前言 使用python开发的应用在迁移部署的时候&#xff0c;你一定会使用pipreqs&#xff08;当然也有其他选择&#xff09;吧&#xff1f;这是一款强大的依赖管理…

【STM32CubeMX开发】-2.1-TIM_生成一个定时器中断

目录 1 Tim定时器的时钟源 2 Tim定时器的配置 2.1 中断配置 3 生成代码 4 测试结果 结尾 1 Tim定时器的时钟源 TIM1的时钟来源自APB1 Timer clocks&#xff0c;时钟树上所有总线频率均设置为了STM32F0能达到的最高频率&#xff0c;此时APB1 Timer clocks 48MHz。 2 Tim…

pdf加密怎么弄?8款热门pdf加密工具大盘点,速来收藏!(2024)

在如今&#xff0c;分享pdf文件时确保敏感信息的安全显得尤为重要。如果没有适当的保护措施&#xff0c;这些文件可能会被不法人士访问&#xff0c;从而对您的数据和声誉造成风险。为了避免这种潜在的问题&#xff0c;在Windows操作系统上加密pdf文件就显得非常有必要。如果您不…

vue3 使用 Vue Router实现前端路由控制

vue3 使用 Vue Router实现前端路由控制 **在现代Web应用中&#xff0c;前端路由控制是非常重要的一部分。它可以帮助我们将不同的页面内容展示给用户&#xff0c;同时保持用户在浏览不同页面时的连贯性。本文将介绍如何使用Vue Router来实现前端路由控制。 首先&#xff0c;我…

在Oxygen编辑器中支持数学公式

在编写文档时&#xff0c;经常需要插入公式。虽然将公式作为图片插入到文档中是可以的&#xff0c;但这会使后续的修改变得非常不便。目前&#xff0c;MathML (Mathematical Markup Language) 和 LaTeX 是两种常用的数学公式描述语言&#xff0c;它们各自具有不同的特点和适用场…

Protobuf:消息更新

Protobuf&#xff1a;消息更新 更新字段保留字段未知字段option选项 在开发中&#xff0c;需要对产品进行版本迭代。迭代前后&#xff0c;类的成员可能就会有所改动&#xff0c;一旦类成员改动&#xff0c;那么老版本的对象&#xff0c;新版本可能就无法解析&#xff0c;此时就…

ubuntu中多cuda版本兼容问题

当ubuntu中已经有老版本的cuda时&#xff0c;按正常步骤直接下载新的cuda和cudnn&#xff0c;只需要注意在下载新的cuda版本时&#xff0c;出现“A symlink already exists at /usr/local/cuda. Update to this installation?”&#xff0c;选择“no”&#xff0c;之后按如下的…

【华为HCIP实战课程十二】OSPF网络中1类2类LSA SPF详解,网络工程师

一、OSPF 1类LSA详解 1、通告者(产生LSA的设备):任何一台设备都会产生1类LSA 2、通告的范围:区域内部 3、功能和内容:产生拓扑信息和路由信息 LSA是OSPF链路状态信息的载体 4、每台OSPF路由器使用一条Router-LSA描述本区域内的链路状态信息 Type :LSA类型,Router-L…

Java学习Day45:兰喜村(Redis)

1.redis概念 1.是什么 redis&#xff08;c语言开发的高并发键值对数据库&#xff09;是nosql的一种&#xff0c;是键值存储数据库&#xff1b; 其核心概念是三高&#xff1a;高并发&#xff0c;高可用性和高扩展性&#xff1b; 优点&#xff1a;快速存取高并发 缺点&#…

Echart自定义饼图

const chartOption computed(() > {return {//与容器边距// grid: {// left: 3%,// right: 4%,// bottom: 3%,// containLabel: true// },// 自定义鼠标悬浮显示内容tooltip: {trigger: item,formatter: function (params: any) {return ${params.value} 个},textS…

新手铲屎官提问,如何在双十一选到性价比高的宠物空气净化器

不知不觉就已经迎来了双十一&#xff0c;这一年即将到头了&#xff0c;意味着我养猫已经是第五个年头了。 当初养猫的时候&#xff0c;就看中了长毛类型的猫&#xff0c;因为感觉摸起来会更舒服&#xff0c;美型到舒服确实是舒服了&#xff0c;但是面临的挑战也不少。其中浮毛…

电脑显示d3dcompiler_47.dll缺失如何修复,马上教你6个修复方法

在用电脑的时候&#xff0c;很多人就遇到过一个叫“计算机缺失d3dcompiler47.dll”的错误提示。在详细解读计算机缺失d3dcompiler_47.dll问题时&#xff0c;我们首先需要了解这个文件的作用&#xff0c;以及缺失d3dcompiler_47.dll对系统的影响和解决方法。 一&#xff0c;d3dc…