您当前的位置:首页 > 电脑百科 > 程序开发 > 语言 > Go语言

go语言中经常犯的错误

时间:2023-05-24 16:40:53  来源:  作者:干饭人小羽

0.1、索引

https://waterflow.link/articles/1664080524986

1、未知的枚举值

我们现在定义一个类型是unit32的Status,他可以作为枚举类型,我们定义了3种状态

type Status uint32

const (
	StatusOpen Status = iota
	StatusClosed
	StatusUnknown
)

其中我们使用了iota,相关的用法自行google。最终对应的状态就是:

0-开启状态,1-关闭状态,2-未知状态

现在我们假设有一个请求参数过来,数据结构如下:

{
  "Id": 1234,
  "Timestamp": 1563362390,
  "Status": 1
}

可以看到是一个json类型的字符串,其中就包含了Status状态,我们的请求是希望把状态修改为关闭状态。

然后我们在服务端创建一个结构体,方便把这些字段解析出来:

type Request struct {
	ID        int    `json:"Id"`
	Timestamp int    `json:"Timestamp"`
	Status    Status `json:"Status"`
}

好了,我们在main中执行下代码,看下解析是否正确:

package main

import (
	"encoding/json"
	"fmt"
)

type Status uint32

const (
	StatusOpen Status = iota
	StatusClosed
	StatusUnknown
)

type Request struct {
	ID        int    `json:"Id"`
	Timestamp int    `json:"Timestamp"`
	Status    Status `json:"Status"`
}

func main() {
	js := `{
		"Id": 1234,
		"Timestamp": 1563362390,
		"Status": 1
	  }`

	request := &Request{}
	err := json.Unmarshal([]byte(js), request)
	if err != nil {
		fmt.Println(err)
		return
	}
}

执行后的结果如下:

go run main.go
&{1234 1563362390 1}

可以看到解析是没问题的。

然而,让我们再提出一个未设置状态值的请求(无论出于何种原因):

{
  "Id": 1234,
  "Timestamp": 1563362390
}

在这种情况下,请求结构的状态字段将被初始化为其零值(对于 uint32 类型:0)。因此,StatusOpen 而不是 StatusUnknown。

最佳实践是将枚举的未知值设置为 0:

type Status uint32

const (
	StatusUnknown Status = iota
	StatusOpen
	StatusClosed
)

在这里,如果状态不是 JSON 请求的一部分,它将被初始化为 StatusUnknown,正如我们所期望的那样。

2、指针无处不在?

按值传递变量将创建此变量的副本。而通过指针传递它只会复制内存地址。

因此,传递指针总是会更快,对么?

如果你相信这一点,请看看这个例子。这是一个 0.3 KB 数据结构的基准测试,我们通过指针和值传递和接收。 0.3 KB 并不大,但这与我们每天看到的数据结构类型(对于我们大多数人来说)应该相差不远。

当我在本地环境中执行这些基准测试时,按值传递比按指针传递快 4 倍以上。这可能有点违反直觉,对吧?

这其实与 Go 中如何管理内存有关。我们都知道变量可以分配在堆上或栈上,也知道:

  • 栈包含给定 goroutine 的正在进行的变量。一旦函数返回,变量就会从堆栈中弹出。
  • 堆包含共享变量(全局变量等)。

让我们看下下面这个简单的例子:

type foo struct{}

func getFooValue() foo {
	var result foo
	// Do something
	return result
}

这里,一个结果变量由当前的 goroutine 创建。这个变量被压入当前堆栈。一旦函数返回,客户端将收到此变量的副本。变量本身从堆栈中弹出。它仍然存在于内存中,直到它被另一个变量擦除,但它不能再被访问。

我们现在修改下上面的例子,使用指针:

type foo struct{}

func getFooPointer() *foo {
	var result foo
	// Do something
	return &result
}

结果变量仍然由当前的 goroutine 创建,但客户端将收到一个指针(变量地址的副本)。如果结果变量从堆栈中弹出,则此函数的客户端无法再访问它。

在这种情况下,Go 编译器会将结果变量转移到可以共享变量的地方:堆。

但是,传递指针是另一种情况。例如:

type foo struct{}

func main()  {
	p := &foo{}
	f(p)
}

因为我们在同一个 goroutine 中调用 f,所以 p 变量不需要被转移。它只是被压入堆栈,子函数可以访问它。

比如在 io.Reader 的 Read 方法中接收切片而不是返回切片的直接结果,也不会转移到堆上。

但是返回一个切片(它是一个指针)会将其转移到堆中。

为什么堆栈那么快?主要原因有两个:

  • 堆栈不需要垃圾收集器。正如我们所说,一个变量在创建后被简单地压入,然后在函数返回时从堆栈中弹出。无需进行复杂的过程来回收未使用的变量等。
  • 堆栈属于一个 goroutine,因此与将变量存储在堆上相比,存储变量不需要同步。这也导致性能增益。

结论就是:

当我们创建一个函数时,我们的默认行为应该是使用值而不是指针。仅当我们想要共享变量时才应使用指针。

最后:

如果我们遇到性能问题,一种可能的优化可能是检查指针在某些特定情况下是否有帮助。使用以下命令可以知道编译器何时将变量转移到堆中:go build -gcflags "-m -m"。(内存逃逸)

3、中断 for/switch 或 for/select

我们看下下面的代码会发生什么:

package main

func f() bool {
	return true
}

func main() {
	for {
		switch f() {
		case true:
			break
		case false:
			// Do something
		}
	}
}

我们将调用 break 语句。但是,这会破坏 switch 语句,而不是 for 循环。

相同的情况还会出现在fo/select中,像下面这样:

package main

import (
	"context"
	"time"
)

func main() {
	ch := make(chan struct{})
	ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second)
	defer cancel()
	for {
		select {
		case <-ch:
		// Do something
		case <-ctx.Done():
			break
		}
	}
}

虽然调用了break,但是还是会陷入死循环。break 与 select 语句有关,与 for 循环无关。

打破 for/switch 或 for/select 的,一种方案是直接return结束整个函数,下面如果还有代码不会被执行。

package main

import (
	"context"
	"fmt"
	"time"
)

func main() {
	ch := make(chan struct{})
	ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second)
	defer cancel()
	for {
		select {
		case <-ch:
		// Do something
		case <-ctx.Done():
			return
		}
	}

  // 这里不会执行
	fmt.Println("done")
}

还有一种方案是使用中断标记

package main

import (
	"context"
	"fmt"
	"time"
)

func main() {
	ch := make(chan struct{})
	ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second)
	defer cancel()
loop:
	for {
		select {
		case <-ch:
		// Do something
		case <-ctx.Done():
			break loop
		}
	}

  // 会继续往下执行
	fmt.Println("done")
}

4、错误管理

一个错误应该只处理一次。记录错误就是处理错误。因此,应该记录或传播错误。

我们可能希望为错误添加一些上下文并具有某种形式的层次结构。

让我们看一个接口请求数据库的例子,我们分为接口层,service层和类库层。我们希望返回的层次结构像下面这样:

unable to serve HTTP POST request for id 1
 |_ unable to insert customer
     |_ unable to commit transaction

如果我们使用 pkg/errors,我们可以这样做:

package main

import (
	"fmt"

	"Github.com/pkg/errors"
)

func postHandler(id int) string {
	err := insert(id)
	if err != nil {
		fmt.Printf("unable to serve HTTP POST request for id %dn", id)
		return `{ok: false}`
	}
	return `{ok: true}`
}

func insert(id int) error {
	err := dbQuery(id)
	if err != nil {
		return errors.Wrapf(err, "unable to insert customer")
	}
	return nil
}

func dbQuery(id int) error {
	// Do something then fail
	return errors.New("unable to commit transaction")
}

func main() {
	res := postHandler(1)
	fmt.Println(res)
}

初始错误(如果不是由外部库返回)可以使用 errors.New 创建。service层 insert 通过向其添加更多上下文来包装此错误。然后,接口层通过记录错误来处理错误。每个级别都返回或处理错误。

例如,我们可能还想检查错误原因本身以实现重试。假设我们有一个来自处理数据库访问的外部库的 db 包。这个库可能会返回一个名为 db.DBError 的暂时(临时)错误。要确定是否需要重试,我们必须检查错误原因:

package main

import (
	"fmt"

	"github.com/pkg/errors"
)

type DbError struct {
	msg string
}

func (e *DbError) Error() string {
	return e.msg
}

func postHandler(id int) string {
	err := insert(id)
	if err != nil {
		errCause := errors.Cause(err)
		if _, ok := errCause.(*DbError); ok {
			fmt.Println("retry")
		} else {
			fmt.Printf("unable to serve HTTP POST request for id %dn", id)
			return `{ok: false}`
		}

	}
	return `{ok: true}`
}

func insert(id int) error {
	err := dbQuery(id)
	if err != nil {
		return errors.Wrapf(err, "unable to insert customer")
	}
	return nil
}

func dbQuery(id int) error {
	// Do something then fail
	return &DbError{"unable to commit transaction"}
}

func main() {
	res := postHandler(1)
	fmt.Println(res)
}

这是使用errors.Cause完成的,它也来自pkg/errors。(可以通过errors.Cause检查。 errors.Cause 将递归检索没有实现causer 的最顶层错误,这被认为是原始原因。)

有时候也会有人这么用。例如,检查错误是这样完成的:

package main

import (
	"fmt"

	"github.com/pkg/errors"
)

type DbError struct {
	msg string
}

func (e *DbError) Error() string {
	return e.msg
}

func postHandler(id int) string {
	err := insert(id)
	if err != nil {
		switch err.(type) {
		default:
			fmt.Printf("unable to serve HTTP POST request for id %dn", id)
			return `{ok: false}`
		case *DbError:
			fmt.Println("retry")

		}
	}
	return `{ok: true}`
}

func insert(id int) error {
	err := dbQuery(id)
	if err != nil {
		return errors.Wrapf(err, "unable to insert customer")
	}
	return nil
}

func dbQuery(id int) error {
	// Do something then fail
	return &DbError{"unable to commit transaction"}
}

func main() {
	res := postHandler(1)
	fmt.Println(res)
}

如果 DBError 被包装,它永远不会触发重试。

5、切片初始化

有时,我们知道切片的最终长度是多少。例如,假设我们要将 Foo 的切片转换为 Bar 的切片,这意味着这两个切片将具有相同的长度。

我们有时候经常会这样初始化切片:

var bars []Bar
bars := make([]Bar, 0)

我们都知道切片的底层是数组。如果没有更多可用空间,它会实施增长战略。在这种情况下,会自动创建一个新数组(容量更大)并复制所有元素。

现在,假设我们需要多次重复这个增长操作,因为我们的 []Foo 包含数千个元素?插入的摊销时间复杂度(平均值)将保持为 O(1),但在实践中,它会对性能产生影响。

因此,如果我们知道最终长度,我们可以:

  • 使用预定义的长度对其进行初始化:
  • func convert(foos []Foo) []Bar { bars := make([]Bar, len(foos)) for i, foo := range foos { bars[i] = fooToBar(foo) } return bars }
  • 或者使用 0 长度和预定义容量对其进行初始化:
  • func convert(foos []Foo) []Bar { bars := make([]Bar, 0, len(foos)) for _, foo := range foos { bars = Append(bars, fooToBar(foo)) } return bars }

选哪个更好呢?第一个稍微快一点。然而,你可能更喜欢第二个,因为无论我们是否知道初始大小,在切片末尾添加一个元素都是使用 append 完成的。

6、上下文管理

context.Context对我们来说非常好用,他可以在协程之间传递数据、可以控制协程的生命周期等等。但是这也造成了它的滥用。

go官方文档是这么定义的:

一个 Context 携带一个截止日期、一个取消信号和其他跨 API 边界的值。

这个描述很宽泛,足以让一些人对为什么以及如何使用它感到困惑。

让我们试着详细说明一下。上下文可以携带:

  • 一个截止时间。它意味着一个持续时间(例如 250 毫秒)或日期时间(例如 2022-01-08 01:00:00),我们认为如果达到,我们必须取消正在进行的活动(I/O 请求,等待通道输入等)。
  • 取消信号(基本上是 <-chan struct{})。 在这里,行为是相似的。 一旦我们收到信号,我们必须停止正在进行的活动。 例如,假设我们收到两个请求。 一个插入一些数据,另一个取消第一个请求(因为它不再需要)。 这可以通过在第一次调用中使用可取消上下文来实现,一旦我们收到第二个请求,该上下文将被取消。
  • 键/值列表(均基于 interface{} 类型)。

另外需要说明的是。

首先,上下文是可组合的。因此,我们可以有一个包含截止日期和键/值列表的上下文。

此外,多个 goroutine 可以共享相同的上下文,因此取消信号可能会停止多个活动。

我们可以看下一个具体的错误例子

一个 Go 应用程序是基于 urfave/cli 的(如果你不知道,那是一个在 Go 中创建命令行应用程序的好库)。一旦开始,开发人员就会继承某种应用程序上下文。这意味着当应用程序停止时,库将使用此上下文发送取消信号。

我了解的是,这个上下文是在调用 gRPC 端点时直接传递的。这不是我们想要做的。

相反,我们想向 gRPC 库传递:请在应用程序停止时或在 100 毫秒后取消请求。

为此,我们可以简单地创建一个组合上下文。如果 parent 是应用程序上下文的名称(由 urfave/cli 创建),那么我们可以简单地这样做:

package main

import (
	"context"
	"fmt"
	"log"
	"os"
	"time"

	"github.com/urfave/cli/v2"
)

func main() {

	app := &cli.App{
		Name:  "boom",
		Usage: "make an explosive entrance",
		Action: func(parent *cli.Context) error {
      // 父上下文传进来,给个超时时间
			ctx, cancel := context.WithTimeout(parent.Context, 10*time.Second)
			defer cancel()
			grpcClientSend(ctx)

			return nil
		},
	}

	if err := app.Run(os.Args); err != nil {
		log.Fatal(err)
	}
}

func grpcClientSend(ctx context.Context) {
	for {
		select {
		case <-ctx.Done(): // 达到超时时间就结束
			fmt.Println("cancel!")
			return
		default:
			time.Sleep(2 * time.Second)
			fmt.Println("do something!")
		}
	}
}

7、使用文件名作为函数输入?

假设我们必须实现一个函数来计算文件中的空行数。一般我们是这样实现的:

package main

import (
	"bufio"
	"fmt"
	"os"

	"github.com/pkg/errors"
)

func main() {

	cou, err := count("a.txt")
	if err != nil {
		fmt.Println(err)
		return
	}
	fmt.Println(cou)
}

func count(filename string) (int, error) {
	file, err := os.Open(filename)
	if err != nil {
		return 0, errors.Wrapf(err, "unable to open %s", filename)
	}
	defer file.Close()

	scanner := bufio.NewScanner(file)
	count := 0
	for scanner.Scan() {
		if scanner.Text() == "" {
			count++
		}
	}
	return count, nil
}

文件名作为输入给出,所以我们打开它然后我们实现我们的逻辑,对吧?

现在,假设我们要在此函数之上实现单元测试,以测试普通文件、空文件、具有不同编码类型的文件等。这很容易变得非常难以管理。

此外,如果我们想要对http body实现相同的逻辑,我们将不得不为此创建另一个函数。

Go 带有两个很棒的抽象:io.Reader 和 io.Writer。我们可以简单地传递一个 io.Reader 来抽象数据源,而不是传递文件名。

是文件吗? HTTP body?字节缓冲区?这并不重要,因为我们仍将使用相同的 Read 方法。

在我们的例子中,我们甚至可以缓冲输入以逐行读取。因此,我们可以使用 bufio.Reader 及其 ReadLine 方法:

我们把读取文件的部分放到函数外面

package main

import (
	"bufio"
	"fmt"
	"io"
	"os"

	"github.com/pkg/errors"
)

func main() {

	filename := "a.txt"
	file, err := os.Open(filename)
	if err != nil {
		fmt.Println(err, "unable to open ", filename)
		return
	}
	defer file.Close()
	count, err := count(bufio.NewReader(file))
	if err != nil {
		fmt.Println(err)
		return
	}
	fmt.Println(count)
}

func count(reader *bufio.Reader) (int, error) {
	count := 0
	for {
		line, _, err := reader.ReadLine()
		if err != nil {
			switch err {
			default:
				return 0, errors.Wrapf(err, "unable to read")
			case io.EOF:
				return count, nil
			}
		}
		if len(line) == 0 {
			count++
		}
	}
}

使用第二种实现,无论实际数据源如何,都可以调用该函数。同时,这将有助于我们的单元测试,因为我们可以简单地从字符串创建一个 bufio.Reader:

package main

import (
	"bufio"
	"fmt"
	"io"
	"strings"

	"github.com/pkg/errors"
)

func main() {

	count, err := count(bufio.NewReader(strings.NewReader("inputnn")))

	if err != nil {
		fmt.Println(err)
		return
	}
	fmt.Println(count)
}

func count(reader *bufio.Reader) (int, error) {
	count := 0
	for {
		line, _, err := reader.ReadLine()
		if err != nil {
			switch err {
			default:
				return 0, errors.Wrapf(err, "unable to read")
			case io.EOF:
				return count, nil
			}
		}
		if len(line) == 0 {
			count++
		}
	}
}

8、Goroutines 和循环变量

我看到一个常见错误是使用带有循环变量的 goroutines。

以下示例的输出是什么?

package main

import (
	"fmt"
	"time"
)

func main() {

	ints := []int{1, 2, 3}
	for _, i := range ints {
		go func() {
			fmt.Printf("%vn", i)
		}()
	}

	time.Sleep(time.Second)
}

在这个例子中,每个 goroutine 共享相同的变量实例,所以它会产生 3 3 3。而不是我们认为的1 2 3

有两种解决方案可以解决这个问题。第一个是将 i 变量的值传递给闭包(内部函数):

package main

import (
	"fmt"
	"time"
)

func main() {

	ints := []int{1, 2, 3}
	for _, i := range ints {
		go func(i int) {
			fmt.Printf("%vn", i)
		}(i)
	}

	time.Sleep(time.Second)
}

第二个是在 for 循环范围内创建另一个变量:

package main

import (
	"fmt"
	"time"
)

func main() {

	ints := []int{1, 2, 3}
	for _, i := range ints {
		i := i
		go func() {
			fmt.Printf("%vn", i)
		}()
	}

	time.Sleep(time.Second)
}

调用 i := i 可能看起来有点奇怪,但它完全有效。处于循环中意味着处于另一个范围内。所以 i := i 创建了另一个名为 i 的变量实例。当然,为了便于阅读,我们可能想用不同的名称来称呼它。



Tags:go语言   点击:()  评论:()
声明:本站部分内容及图片来自互联网,转载是出于传递更多信息之目的,内容观点仅代表作者本人,不构成投资建议。投资者据此操作,风险自担。如有任何标注错误或版权侵犯请与我们联系(Email:2595517585@qq.com),我们将及时更正、删除。
▌相关推荐
0.1、索引https://waterflow.link/articles/16640805249861、未知的枚举值我们现在定义一个类型是unit32的Status,他可以作为枚举类型,我们定义了3种状态type Status uint32co...【详细内容】
2023-05-24  Tags: go语言  点击:(0)  评论:(0)  加入收藏
众所周知,自媒体写作是一项需要不断创新的工作,而其中最常见的问题就是“原创难”。为了应对这一问题,人们开始寻找各种“伪原创”方法。而在这些方法中,go语言无疑是最受欢迎的...【详细内容】
2023-05-07  Tags: go语言  点击:(22)  评论:(0)  加入收藏
unsafe是Golang中的一个包,提供了一些不安全的操作,例如指针操作和类型转换,这些操作可以绕过Go语言的类型安全检查和垃圾回收机制,可以用于编写低层次的系统代码或进行一些高性...【详细内容】
2023-05-05  Tags: go语言  点击:(9)  评论:(0)  加入收藏
背景我们在使用kubernetes的客户端k8s.io/client-go 进行开发的时候,比如写个CRD的operator, 经常会用到队列这种数据结构。并且很多时候,我们在做服务器端后台开发的时候,需要...【详细内容】
2023-03-28  Tags: go语言  点击:(40)  评论:(0)  加入收藏
1、定义结点package mainimport ( "fmt")// 定义结点type BinaryTreeNode struct { Data int Left *BinaryTreeNode Right *BinaryTreeNode}2、创建结点// 创...【详细内容】
2023-03-16  Tags: go语言  点击:(64)  评论:(0)  加入收藏
背景在并发处理中,资源争用是一个常见的问题。为了避免资源争用,需要进行优化。以下是一些可以优化并发处理中的资源争用问题的建议: 避免锁竞争:锁竞争是一种常见的资源争用问...【详细内容】
2023-03-10  Tags: go语言  点击:(44)  评论:(0)  加入收藏
在很多情况下,并发的效果比并行好,因为操作系统和硬件的总资源一般很少,但能支持系统同时做很多事情。这种“使用较少的资源做更多的事情”的哲学,也是指导 Go语言设计的哲学。...【详细内容】
2023-02-10  Tags: go语言  点击:(85)  评论:(0)  加入收藏
背景在实际项目中,我们经常需要异步处理事件与数据。比如MVC模型中处理请求的Filter链,又如在nginx中或是linux的iptables中,都会有一个处理链条,来一步步的顺序处理一个请求。...【详细内容】
2023-01-19  Tags: go语言  点击:(99)  评论:(0)  加入收藏
你好,我是陈皓,网名左耳朵耗子。之前,作为 Go 语言的三位创始人之一,Unix 老牌黑客罗勃&middot;派克(Rob Pike)在文章“Go: Ten years and climbing”中,回顾了 Go 语言的发展历程...【详细内容】
2022-12-08  Tags: go语言  点击:(68)  评论:(0)  加入收藏
一、工具介绍Golang免杀马生成工具,在重复造轮子的基础上尽可能多一点自己的东西,最重要的loader部分参考其他作者。相较其他免杀工具具备以下优势:1、使用fyne的GUI界面,不算难...【详细内容】
2022-11-27  Tags: go语言  点击:(122)  评论:(0)  加入收藏
▌简易百科推荐
0.1、索引https://waterflow.link/articles/16640805249861、未知的枚举值我们现在定义一个类型是unit32的Status,他可以作为枚举类型,我们定义了3种状态type Status uint32co...【详细内容】
2023-05-24  干饭人小羽    Tags:go语言   点击:(0)  评论:(0)  加入收藏
1. 引言反射是现代编程语言中非常重要的一个特性,尤其是在面向对象编程语言中此前的文章中,我们看到 golang 如何实现面向对象的封装: 通过 GoLang 实现面向对象思想如果能够...【详细内容】
2023-05-24  干饭人小羽  今日头条  Tags:golang   点击:(1)  评论:(0)  加入收藏
原型设计模式是面向对象编程中的一种设计模式,它的主要目的是通过复制现有对象来创建新对象,从而避免昂贵的对象创建过程,提高程序的性能和效率。原型设计模式包括深拷贝和浅拷...【详细内容】
2023-05-22  阿琪说    Tags:Go   点击:(3)  评论:(0)  加入收藏
工厂模式是面向对象编程中的一种设计模式,它的主要目的是将对象的创建与使用分离开来,从而提高程序的可维护性和可扩展性。工厂模式包括工厂方法模式、抽象工厂模式和简单工厂...【详细内容】
2023-05-20  阿琪说    Tags:工厂模式   点击:(4)  评论:(0)  加入收藏
在遍历 Map 时,并不是固定地从 0 号 Bucket 开始遍历,每次都是从一个随机值序号的 Bucket 开始遍历,并且是从这个 Bucket 的一个随机序号的 Cell 开始遍历。Go 语言中的 map 是...【详细内容】
2023-05-19     AlwaysBeta  Tags:Go 语言   点击:(13)  评论:(0)  加入收藏
在生产环境中,如mysql数据库服务、rabbit-mq消息队列服务、redis缓存服务等。为了安全,这些服务的通常端口都是不对外网开放的。有时候,我们需要本地访问这些服务,要如何设置呢?...【详细内容】
2023-05-16  互联网解决方案服务  今日头条  Tags:golang   点击:(23)  评论:(0)  加入收藏
Go 语言中的 map 是一个非常常用的数据结构,它允许我们快速地存储和检索键值对。然而,在并发场景下使用 map 时,还是有一些问题需要注意的。本文将探讨 Go 语言中的 map 是否...【详细内容】
2023-05-15  AlwaysBeta  微信公众号  Tags:Go 语言   点击:(16)  评论:(0)  加入收藏
Golang中的sync包实现了两种锁:互斥锁(Mutex)和读写锁(RWMutex)。互斥锁(sync.Mutex) 使用Lock方法加锁,使用Unlock方法解锁,Golang从1.18新增了TryLock方法,用于尝试获取锁,返回成功或...【详细内容】
2023-05-15  路多辛  今日头条  Tags:Golang   点击:(21)  评论:(0)  加入收藏
众所周知,自媒体写作是一项需要不断创新的工作,而其中最常见的问题就是“原创难”。为了应对这一问题,人们开始寻找各种“伪原创”方法。而在这些方法中,go语言无疑是最受欢迎的...【详细内容】
2023-05-07  京京爱美食    Tags:Go语言   点击:(22)  评论:(0)  加入收藏
unsafe是Golang中的一个包,提供了一些不安全的操作,例如指针操作和类型转换,这些操作可以绕过Go语言的类型安全检查和垃圾回收机制,可以用于编写低层次的系统代码或进行一些高性...【详细内容】
2023-05-05  大厂背锅侠  今日头条  Tags:Go语言   点击:(9)  评论:(0)  加入收藏
站内最新
站内热门
站内头条