Go语言学习(一)

1、计算uint64中二进制1的个数

Go 复制代码
// 代码:https://gopl-zh.github.io/ch2/ch2-06.html
package main

import "fmt"

// pc[i] is the population count of i.
var pc [256]byte

// 将0-255数字中二进制的1个数计算出来
func init() {
	for i := range pc {
		// 为什么是除2呢,因为后面的数字中1的个数是它一半时1的个数加当前数字最后一个1的个数
		pc[i] = pc[i/2] + byte(i&1)
	}
}

// PopCount returns the population count (number of set bits) of x.
func PopCount(x uint64) int {
	// 把当前数字的八个字节分别拆开计算,算出每个字节1的个数,最后相加
	return int(pc[byte(x>>(0*8))] +
		pc[byte(x>>(1*8))] +
		pc[byte(x>>(2*8))] +
		pc[byte(x>>(3*8))] +
		pc[byte(x>>(4*8))] +
		pc[byte(x>>(5*8))] +
		pc[byte(x>>(6*8))] +
		pc[byte(x>>(7*8))])
}


func PopCountEach1(x uint64) int {
	var res int
	for x > 0 {
		if x&1 > 0 {
			res++
		}
		x = x>>1
	}
	return res
}

func PopCountEach8(x uint64) int {
    var res int
	for step := 7; step >= 0; step-- {
		res += int(pc[byte(x>>(8 * step))])
	}
	return res
}

func PopCount0(x uint64) int {
	var res int
	for x > 0 {
		x &= x-1
		res++
	}
	return res
}

func main() {
	for i := 0; i < 256; i++ {
		fmt.Printf("%d\n", pc[i])
	}
}

2、go mod tidy 命令

go mod tidy用于管理和整理项目的依赖项。具体来说,它会执行以下操作:

  1. 添加缺失的模块依赖项 :如果你的代码文件中引用了某些模块,但这些模块并未在 go.mod 文件中声明,go mod tidy 会自动将这些缺失的依赖项添加到 go.mod 文件中。

  2. 移除未使用的模块依赖项 :如果 go.mod 文件中声明了一些模块,但这些模块并未在代码中实际使用,go mod tidy 会将这些未使用的依赖项从 go.mod 文件中移除。

  3. 更新 go.sum 文件go mod tidy 还会更新 go.sum 文件,以确保其包含当前项目所需的所有模块版本的校验和信息。如果有不再需要的模块,相关的校验和也会被移除。

3、八股文

Go语言数组赋值和函数参数传递,都是以值传递的方式。

Go 语言字符串底层数据也是对应的字节数组,但是字符串的只读属性禁止了在程序中对底层字节数组的元素的修改。字符串赋值只是复制了数据地址和对应的长度,而不会导致底层数据的复制。

除了闭包函数以引用的方式对外部变量访问之外,其它赋值和函数传参数都是以传值的方式处理。

切片可以和 nil 进行比较,只有当切片底层数据指针为空时切片本身为 nil,这时候切片的长度和容量信息将是无效的。

如果有切片的底层数据指针为空,但是长度和容量不为 0 的情况,那么说明切片本身已经被损坏了(比如直接通过 reflect.SliceHeaderunsafe 包对切片作了不正确的修改)。

切片的类型和长度信息无关,只要是相同类型元素构成的切片,不管长度是否相同,均对应相同的切片类型。

如果切片中放置的是指针类型,删除切片中末尾元素,可能会导致指针所指的内存不能及时被回收。因为该指针仍然指向这块内存区域。保险的方式是在执行删除前,明确将其置为nil值。如果整个切片本身使用周期很短,它自身很快会被回收,那就无需考虑这个问题,切片被回收的时候,内部指针元素也会被回收。

接口对应的方法是运行时动态绑定的。

在Go语言中,不用担心堆和栈的问题,编译器和运行时会解决这些问题,并且指针的位置是可能会变化的,如果它指向的内容地址发生了变化,那么指针也会随着发生变化。

#cgo: C/C++ 遗留的问题,C 头文件检索目录可以是相对目录,但是库文件检索目录则需要绝对路径。在库文件的检索目录中可以通过 ${SRCDIR} 变量表示当前包目录的绝对路径:

// #cgo LDFLAGS: -L${SRCDIR}/libs -lfoo

4、下面是一个 brainfuck 程序,++++++++++[>++++++++++<-]>++++.+.

参考链接:gRPC入门 - Go语言高级编程

Brainfuck 是一种极简的编程语言,它只有八条指令。

Brainfuck 指令

  • `>`:指针右移

  • `<`:指针左移

  • `+`:当前指针位置的字节值加 1

  • `-`:当前指针位置的字节值减 1

  • `.`:输出当前指针位置的字节值(作为 ASCII 字符)

  • `,`:输入一个字节并存储到当前指针位置

  • `[`:如果当前指针位置的字节值为 0,则跳转到与之对应的 `]` 的下一条指令

  • `]`:如果当前指针位置的字节值不为 0,则跳转回与之对应的 `[` 的下一条指令

程序逐步解释

  1. `++++++++++`
  • 将当前指针位置的值增加 10。假设指针初始位置为 `ptr = 0`,内存状态为:

```

[10, 0, 0, 0, ...]

```

  1. `[`
  • 进入循环,如果当前指针位置的值不为 0,则执行循环内部的指令。
  1. `>++++++++++`
  • 指针右移一位,`ptr = 1`。

  • 将 `ptr = 1` 的值增加 10,内存状态为:

```

[10, 10, 0, 0, ...]

```

  1. `<-`
  • 指针左移一位,`ptr = 0`。

  • 将 `ptr = 0` 的值减 1,内存状态为:

```

[9, 10, 0, 0, ...]

```

  1. `]`
  • 如果 `ptr = 0` 的值不为 0,则跳回到对应的 `[`,继续循环。

循环过程:

  • 每次循环,`ptr = 0` 的值减 1,`ptr = 1` 的值增加 10。

  • 这个循环运行 10 次后,`ptr = 0` 的值变为 0,`ptr = 1` 的值变为 100。

内存状态为:

```

[0, 100, 0, 0, ...]

```

  1. `>++++`
  • 指针右移一位,`ptr = 1`。

  • 将 `ptr = 1` 的值增加 4,内存状态为:

```

[0, 104, 0, 0, ...]

```

  1. `.`
  • 输出 `ptr = 1` 的值(104 对应的 ASCII 字符是 `h`)。
  1. `+`
  • 将 `ptr = 1` 的值增加 1,内存状态为:

```

[0, 105, 0, 0, ...]

```

  1. `.`
  • 输出 `ptr = 1` 的值(105 对应的 ASCII 字符是 `i`)。

程序输出

通过逐步解释,该程序的输出是:

```

hi

```

总结:

  • `++++++++++` 将 `ptr = 0` 的值增加到 10。

  • `[>++++++++++<-]` 循环 10 次,将 `ptr = 1` 的值增加到 100。

  • `>++++` 将 `ptr = 1` 的值增加到 104。

  • `.` 输出字符 `h`。

  • `+` 将 `ptr = 1` 的值增加到 105。

  • `.` 输出字符 `i`。

最终结果是 `hi`。

Go 复制代码
package main

import (
	"bufio"
	"bytes"
	"crypto/md5"
	"crypto/sha256"
	"encoding/hex"
	"fmt"
	"io"
	"log"
	"net/http"
	"os"
)

// ReadSmallFile 函数读取指定文件的内容并打印到标准输出。
// 如果读取文件过程中发生错误,则返回相应的错误。
// 参数:
//
//	file:待读取文件的路径。
//
// 返回值:
//
//	error:如果读取文件过程中发生错误,则返回非nil的error。
func ReadSmallFile(file string) error {
	content, err := os.ReadFile(file)
	if err != nil {
		return err
	}
	fmt.Println(string(content))
	return nil
}

// ReadBigFile 函数用于读取大文件并输出到标准输出
// 参数:
//   - file:待读取文件的路径
//
// 返回值:
//   - error:如果打开文件或读取文件过程中发生错误,则返回相应错误信息;否则返回nil
func ReadBigFile(file string) error {
	f, err := os.Open(file)
	defer f.Close()
	if err != nil {
		return err
	}
	buf := make([]byte, 1024)
	for {
		n, err := f.Read(buf)
		if err != nil {
			break
		}
		content := buf[:n]
		os.Stdout.Write(content)
	}
	return nil
}

// 通过IO简化操作
func ReadBigFile2(file string) error {
	f, err := os.Open(file)
	if err != nil {
		return err
	}
	defer f.Close()
	io.Copy(os.Stdout, f)
	return nil
}

// ReadFileByLine 逐行读取文件内容并打印
//
// 参数:
//
//	file string - 文件路径
//
// 返回值:
//
//	error - 读取文件过程中遇到的错误,如果读取成功则返回nil
func ReadFileByLine(file string) error {
	f, err := os.Open(file)
	if err != nil {
		return err
	}
	defer f.Close()
	scanner := bufio.NewScanner(f)
	for scanner.Scan() {
		line := scanner.Text()
		fmt.Println(line)
	}
	return nil
}

// 初始化/dev/zero
type DevZeroReader struct{}

func (d DevZeroReader) Read(p []byte) (n int, err error) {
	for i := range p {
		p[i] = 0
	}
	return len(p), nil
}

// 创建一个1M大小的零字节的文件
func CreateFile() error {
	f, err := os.Create("1M.bin")
	defer f.Close()
	if err != nil {
		return err
	}
	io.Copy(f, &io.LimitedReader{
		R: new(DevZeroReader),
		N: 1 << 20,
	})
	return nil
}

// hash值计算,一次性计算,流式计算
func CalHash() {
	content := []byte("hello world")
	var sum [md5.Size]byte
	sum = md5.Sum(content)
	fmt.Println(sum)

	h := md5.New()
	r := bytes.NewBuffer(content)
	io.Copy(h, r)
	sumArray := h.Sum(nil)[0:md5.Size]
	fmt.Println(sumArray)
}

// 同时下载文件,同时计算hash值
func DownloadFile() {
	// 设置要下载的文件的URL
	url := "https://chai2010.cn/advanced-go-programming-book/ch4-rpc/ch4-01-rpc-intro.html"
	// 设置下载文件保存的路径
	filepath := "download.html"

	// 创建一个新的sha256哈希器
	hasher := sha256.New()

	// 创建一个新文件,用于保存下载的内容
	file, err := os.Create(filepath)
	if err != nil {
		log.Fatal("create file error: ", err)
	}
	// 延迟关闭文件句柄
	defer file.Close()

	// 创建一个多写器,将文件内容和哈希值同时写入
	multiWriter := io.MultiWriter(file, hasher)

	// 发起HTTP GET请求,获取文件内容
	resp, err := http.Get(url)
	if err != nil {
		log.Fatal("download error: ", err)
	}
	// 延迟关闭HTTP响应的Body
	defer resp.Body.Close()

	// 将HTTP响应的Body内容复制到多写器中
	if _, err := io.Copy(multiWriter, resp.Body); err != nil {
		log.Fatal("copy error: ", err)
	}
	// 打印文件的哈希值
	fmt.Println("hash: ", hex.EncodeToString(hasher.Sum(nil)))
}

func main() {
	// ReadSmallFile("test.txt")
	// ReadBigFile("test.txt")
	// ReadBigFile2("test.txt")
	// ReadFileByLine("test.txt")
	// CreateFile()
	// CalHash()
	DownloadFile()
}

5、为客户端和服务的生成公钥和私钥

bash 复制代码
$ openssl genrsa -out server.key 2048
$ openssl req -new -x509 -days 3650 \
    -subj "/C=GB/L=China/O=grpc-server/CN=server.grpc.io" \
    -key server.key -out server.crt

$ openssl genrsa -out client.key 2048
$ openssl req -new -x509 -days 3650 \
    -subj "/C=GB/L=China/O=grpc-client/CN=client.grpc.io" \
    -key client.key -out client.crt

6、go范型

package main

import "fmt"

func printGenericType[T any](t T) {
	fmt.Println(t)
}

type MyInt int

func main() {
	printGenericType(1)
	printGenericType(MyInt(1))
	printGenericType([]int{1, 2, 3})
	printGenericType(map[string]int{"a": 1, "b": 2, "c": 3})
	printGenericType([]string{"a", "b", "c"})
	printGenericType(map[string]string{"a": "1", "b": "2", "c": "3"})
	printGenericType(uint32(1))
	printGenericType(uint64(1))
	printGenericType(int32(1))
	printGenericType("hello")
	printGenericType(map[int]int{1: 2, 3: 4})
	printGenericType(make(chan struct{}))
	printGenericType(struct {
		A int
		B string
	}{A: 1, B: "hello"})
}

输出结果:
1
1
[1 2 3]
map[a:1 b:2 c:3]
[a b c]
map[a:1 b:2 c:3]
1
1
1
hello
map[1:2 3:4]
0x14000102060
{1 hello}

7、模拟Context被动cancel

Go 复制代码
package main

import (
	"context"
	"log"
	"net/http"
	"strconv"
	"time"
)

func main(){
   http.HandleFunc("/timeout", handler)
   err:=http.ListenAndServe("127.0.0.1:8084",nil)
   log.Println("server exit:",err)
}

func handler(writer http.ResponseWriter, request *http.Request) {
   sleep,_:=strconv.Atoi(request.URL.Query().Get("sleep"))
   data,err:=doRPC(request.Context(),time.Duration(sleep)*time.Second)
   log.Println("data=",data,"err=",err)
   writer.Write([]byte(data))
}

func doRPC(ctx context.Context,sleep time.Duration)(string,error){
   select {
   case <-ctx.Done():
      return "ctx.Done",ctx.Err()
   case <-time.After(sleep):// 模拟 RPC 调用耗时
      return "success",nil 
   }
}

终端测试:

Go 复制代码
❯ curl 'http://127.0.0.1:8084/timeout'
success%                                                                                                                                                      
❯ curl 'http://127.0.0.1:8084/timeout?sleep=5' -m 3
curl: (28) Operation timed out after 3006 milliseconds with 0 bytes received

服务端端输出:

bash 复制代码
2024/09/02 11:15:37 data= success err= <nil>
2024/09/02 11:16:11 data= ctx.Done err= context canceled

8、模拟Context主动cancel

Go 复制代码
package main

import (
	"context"
	"fmt"
	"sync"
	"testing"
	"time"
)

func longRunningTask(ctx context.Context, wg *sync.WaitGroup, id int) {
	defer wg.Done()
	select {
	case <-ctx.Done():
		fmt.Printf("Task %d canceled\n", id)
	case <-time.After(2*time.Second):
		fmt.Printf("Task %d completed\n", id)
	}
}

func TestContextCancel(t *testing.T) {
	var wg sync.WaitGroup

	// 创建一个可以被取消的 Context
	ctx, cacel := context.WithCancel(context.Background())

	// 启动任务
	wg.Add(1)
	go longRunningTask(ctx, &wg, 1)
	time.Sleep(1 * time.Second)
	// 取消任务
	cacel()
	// 确保 goroutine 完成
	wg.Wait()
	
	// 另一个对比实验:不取消 Context
	wg.Add(1)
	go longRunningTask(context.Background(), &wg, 2) // 注意这里使用的是 context.Background(),它永远不会被取消

	// 等待足够长的时间让任务完成
	time.Sleep(3 * time.Second)

	wg.Wait()
}

func main() {
	// 这里不写代码,使用测试的方式运行即可
}

运行结果:

bash 复制代码
❯ go test -v cancel1_test.go
=== RUN   TestContextCancel
Task 1 canceled
Task 2 completed
--- PASS: TestContextCancel (4.00s)
PASS
ok      command-line-arguments  4.639s

9、Go使用option选项模式来传递参数(函数方式)

Go 复制代码
package main

import "fmt"

// Logger 代表一个日志记录器
type Logger struct {
	Verbose   bool
	UseColors bool
	Level     int
}

// Option 用于配置 Logger 的函数类型
type Option func(*Logger)

// WithVerbose 设置日志记录器为详细模式
func WithVerbose(verbose bool) Option {
	return func(l *Logger) {
		l.Verbose = verbose
	}
}

// WithColors 设置日志记录器是否使用颜色
func WithColors(useColors bool) Option {
	return func(l *Logger) {
		l.UseColors = useColors
	}
}

// WithLevel 设置日志记录器的日志级别
func WithLevel(level int) Option {
	return func(l *Logger) {
		l.Level = level
	}
}

// NewLogger 使用给定的选项来创建和配置一个 Logger
func NewLogger(options ...Option) *Logger {
	l := &Logger{
		// 可以设置默认配置
		Verbose:   false,
		UseColors: false,
		Level:     0,
	}
	// options中的值相当于各个字段赋值时,等号右边的值
	// 下面调用opt(l)相当于对l进行赋值操作
	// 应用所有的选项
	for _, opt := range options {
		opt(l)
	}

	return l
}

func main() {
	logger := NewLogger(
		WithVerbose(true),
		WithColors(true),
		WithLevel(3),
	)

	fmt.Printf("%+v\n", logger)
}

输出结果:

bash 复制代码
&{Verbose:true UseColors:true Level:3}

10、Go使用option选项模式来传递参数(接口方式)

Go 复制代码
package main

import "fmt"

// Logger 代表一个日志记录器
type Logger struct {
	Verbose   bool
	UseColors bool
	Level     int
}

// Option 用于配置 Logger 的函数类型
// 如果Option不定义为函数,而定义为接口类型
type Option interface {
	apply(*Logger)
}

// 定义一个具体的Option类型,用于设置日志级别
type LogLeverOption struct {
	Level int
}

// LogLeverOption实现Option接口
func (l LogLeverOption) apply(lg *Logger) {
	lg.Level = l.Level
}

// WithVerbose 设置日志记录器为详细模式
type WithVerboseOption struct {
	Verbose bool
}

// WithVerboseOption实现Option接口
func (w WithVerboseOption) apply(lg *Logger) {
	lg.Verbose = w.Verbose
}

// WithColors 设置日志记录器是否使用颜色
type WithColorsOption struct {
	UseColors bool
}

// WithColorsOption实现Option接口
func (w WithColorsOption) apply(lg *Logger) {
	lg.UseColors = w.UseColors
}

// NewLogger 使用给定的选项来创建和配置一个 Logger
func NewLogger(options ...Option) *Logger {
	l := &Logger{
		// 可以设置默认配置
		Verbose:   false,
		UseColors: false,
		Level:     0,
	}
	// options中的值相当于各个字段赋值时,等号右边的值
	// 下面调用opt(l)相当于对l进行赋值操作
	// 应用所有的选项
	for _, opt := range options {
		opt.apply(l)
	}

	return l
}

func main() {
	logger := NewLogger(
		WithVerboseOption{Verbose: true},
		WithColorsOption{UseColors: true},
		LogLeverOption{Level: 3},
	)

	fmt.Printf("%+v\n", logger)
}

11、使用自动化工具生成代码

(1)生成表格驱动测试

假设一个简单的函数

Go 复制代码
// add.go
package main

// Add 计算两个整数的和
func Add(a, b int) int {
	return a + b
}

执行如下命令:

Go 复制代码
gotests -all -w add.go

生成如下代码:

Go 复制代码
// add.go
package main

import "testing"

func TestAdd(t *testing.T) {
	type args struct {
		a int
		b int
	}
	tests := []struct {
		name string
		args args
		want int
	}{
		// TODO: Add test cases.
		
	}
	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			if got := Add(tt.args.a, tt.args.b); got != tt.want {
				t.Errorf("Add() = %v, want %v", got, tt.want)
			}
		})
	}
}

在// TODO: Add test cases.地方手动添加测试代码:

Go 复制代码
// add.go
package main

import "testing"

func TestAdd(t *testing.T) {
	type args struct {
		a int
		b int
	}
	tests := []struct {
		name string
		args args
		want int
	}{
		// TODO: Add test cases.
		{
			name: "test",
			args: args{
				a: 1,
				b: 2,
			},
			want: 3,
		},
		{
			name: "test2",
			args: args{
				a: 2,
				b: 3,
			},
			want: 5,
		},
	}
	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			if got := Add(tt.args.a, tt.args.b); got != tt.want {
				t.Errorf("Add() = %v, want %v", got, tt.want)
			}
		})
	}
}
(2)生成接口测试代码(mock)

安装 GoMock 和其命令行工具 mockgen

bash 复制代码
go get github.com/golang/mock/gomock
go install github.com/golang/mock/mockgen@latest

假设服务接口 Mailer 如下代码:

Go 复制代码
package mypackage

type Mailer interface {
	SendMail(to, subject, body string) error
}

使用 mockgen 工具为 Mailer 接口生成 mock 类:

bash 复制代码
mockgen -source=mypackage/mailer.go -destination=mypackage/mock_mailer/mock_mailer.go -package=mock_mailer

mypackage/mock_mailer 目录下创建一个包含 mock 实现的 mock_mailer.go 文件。

创建一个测试文件 mailer_test.go

Go 复制代码
package mypackage

import (
	"GoLearn/mypackage/mock_mailer"
	"testing"

	"github.com/golang/mock/gomock"
)

func TestSendEmail(t *testing.T) {
	mockCtrl := gomock.NewController(t)
	defer mockCtrl.Finish()

	mockMailer := mock_mailer.NewMockMailer(mockCtrl)
	mockMailer.EXPECT().SendMail("example@example.com", "Subject", "Hello").Return(nil).Times(1)

	// 测试的函数,它使用 Mailer
	err := sendWelcomeEmail(mockMailer)
	if err != nil {
		t.Errorf("sendWelcomeEmail returned an error: %v", err)
	}
}

func sendWelcomeEmail(m Mailer) error {
	return m.SendMail("example@example.com", "Subject", "Hello")
}

1、创建了一个 gomock.Controller 和一个 MockMailer 的实例。

2、使用 EXPECT() 方法为 SendMail 方法设置了期望的行为,期望它被正确的参数调用一次,并返回 nil。

3、调用实际的函数 sendWelcomeEmail,它接受 Mailer 接口作为参数。

12、使用打桩框架GoStub

GoStub 是一个在 Go 语言中用于测试时替换变量和函数的库。它可以用来在测试中临时替换常量或变量的值,或者改变函数的行为,这对于控制测试环境和测试代码路径非常有用。

安装:

Go 复制代码
go get github.com/prashantv/gostub

假设你有一个全局变量 MaxSize,你想在测试中替换它的值:

Go 复制代码
// example/package.go
package example

// MaxSize 是默认的最大尺寸限制。
var MaxSize = 100

// Calculate 根据给定的尺寸返回一个处理后的尺寸值。
// 如果给定尺寸大于MaxSize,则返回MaxSize。
func Calculate(size int) int {
	if size > MaxSize {
		return MaxSize // 如果输入值超过限制,返回最大限制值
	}
	return size // 否则返回实际大小
}

使用 GoStub 在测试中替换 MaxSize

Go 复制代码
// example/package_test.go
package example

import (
	"testing"

	"github.com/prashantv/gostub" // 引入gostub库
)

// TestCalculate 测试 Calculate 函数的行为。
func TestCalculate(t *testing.T) {
	// 使用gostub.Stub替换MaxSize的值为10,这是测试的一部分
	stubs := gostub.Stub(&MaxSize, 10)
	defer stubs.Reset() // 测试完成后,确保原始值被恢复

	// 调用Calculate函数,期望因为MaxSize被替换为10,输入20应返回10
	result := Calculate(20)
	if result != 10 {
		t.Errorf("Expected 10, got %d", result) // 如果结果不是预期的,报错
	}
}
相关推荐
Charles Ray20 分钟前
C++学习笔记 —— 内存分配 new
c++·笔记·学习
重生之我在20年代敲代码20 分钟前
strncpy函数的使用和模拟实现
c语言·开发语言·c++·经验分享·笔记
爱上语文22 分钟前
Springboot的三层架构
java·开发语言·spring boot·后端·spring
我要吐泡泡了哦1 小时前
GAMES104:15 游戏引擎的玩法系统基础-学习笔记
笔记·学习·游戏引擎
骑鱼过海的猫1231 小时前
【tomcat】tomcat学习笔记
笔记·学习·tomcat
编程零零七2 小时前
Python数据分析工具(三):pymssql的用法
开发语言·前端·数据库·python·oracle·数据分析·pymssql
贾saisai3 小时前
Xilinx系FPGA学习笔记(九)DDR3学习
笔记·学习·fpga开发
北岛寒沫3 小时前
JavaScript(JS)学习笔记 1(简单介绍 注释和输入输出语句 变量 数据类型 运算符 流程控制 数组)
javascript·笔记·学习
2401_858286113 小时前
52.【C语言】 字符函数和字符串函数(strcat函数)
c语言·开发语言
铁松溜达py3 小时前
编译器/工具链环境:GCC vs LLVM/Clang,MSVCRT vs UCRT
开发语言·网络