较暗的角落

2021-03-23 13:37:00

当我学习第一个时,我读了一些介绍书和语言规范,我已经了解了其他几种编程语言,毕竟它觉得我真的不太了解去做真实世界的工作。我觉得我没有足够深刻的了解,因为在去土地上,我可能需要落入许多陷阱,然后我能够对它有信心。

虽然简单性在于Go哲学的核心,但您将在进一步的文本中找到它,但它仍然可以在脚下射击自己的许多创造性方式。

从现在我已经使用了几年的生产应用程序,并且在我脚上的许多洞的帐户上,我以为我会把文本放在一起的伴侣的学生。

我的目标是在一个地方收集各种各样的事情,可能对新开发商感到令人惊讶,也许揭示了更不寻常的去的功能。我希望这会拯救读者大量的谷歌曲和调试时间,并且可能会阻止一些昂贵的错误。

我认为这篇文章对那些至少知道Go的语法最有用。如果您是已经了解其他编程语言并希望学习的中间或经验丰富的程序员,最好是。

如果您发现错误或者我没有包含您最喜欢的Go-Surfeise,请在[email protected]上告诉我

大谢谢和业力点指向Vytautas Shaltenis,以帮助提高这篇文章。

通过GOFMT工具将强制使用大部分代码格式。 GOFMT对源文件进行机械更改,例如排序导入声明和应用缩进。自切片面包以来,这是最好的事情,因为它拯救了人类的多年的开发人员争论几乎没有重要的事情。例如,它使用标签进行缩进,对齐空间 - 这就是该故事的结尾。您可以自由不使用GOFMT工具,但如果您无法将其配置为特定的格式化样式。该工具提供零代码格式选项,这是点。提供一个“足够好”的统一格式风格。它可能是没有人是最喜欢的风格,但去开发人员决定均匀性优于完美。

它将您从与OCD的同事中拯救出来,即开放支架应该去的地方或应该用于缩进。所有激情和能量都可以更加高效地使用。

代码更容易阅读:您不需要精神上解析别人的不熟悉的格式样式。

大多数流行的IDE具有用于自动运行GOFMT的插件,请保存源文件。

GoforMat等第三方工具允许自定义代码样式格式格式化。如果你真的必须有那个。

Gofmt不会试图为您分手长长的代码。有第三方工具,如狼犬,可以这样做。

必须将打开支架放置在路线的末端。有趣的是,这不是由GOFMT执行的,而是如何实现Loxical分析仪的副作用。有或没有GOFMT不能放置在新线上的开口支架。

package main //缺少函数bodyfunc main()//语法错误:意外的分号或换行符{{} //所有good!func main(){}

在初始化切片,阵列,映射或结构时,请在新行之前进行逗号。尾随逗号可以用多种语言允许,并在某些风格指南中鼓励。他们是强制性的。这种方式可以重新排列或添加新行而不修改无关线路。这意味着代码审查的噪音较少。

//所有这些都是OKA:= [] int {1,2} b:= [] int {1,2,} c:= [] int {1,2} d:= [] int {1,2 ,} //没有尾随致辞的语法错误:= [] int {1,//语法错误:意外的换行符,期待逗号或} 2}

struct s struct {一个int两个int} f:= s {一:1,//语法错误:意外的换行符,期待逗号或}二:2}

使用未使用导入的程序无法编译。这是语言的故意特征,因为导入包慢下来编译器。在大型计划中,未使用的进口可能对编译时间产生重大影响。

要使编译器快乐,同时开发您可以以某种方式引用包装:

package mainimport(" fmt"" math")//引用未使用packagevar _ = math.round func main(){fmt.println(" hello")}

更好的解决方案是使用GoImports工具。 GoImports删除了对您的未引用的进口。什么可以更好地尝试自动查找和添加缺失的。

包裹MainImport"数学" //进口并未使用:" math" func main(){fmt.println(" hello")//未定义:fmt}

下划线导入仅针对其副作用引用包。这意味着它创建了包级变量并运行包init函数:

软件包包1func package1function()int {fmt.println("包1副作用")return 1} var globalvariable = package1function()func init(){fmt.println("包1 init侧效果")}

多次导入包(例如,在主包中以及主要引用的其他包中)只会运行Init函数一次。

下划线导入用于Go运行时库。例如,导入net / http / pprof调用其init函数,公开可以提供有关您应用程序的调试信息的HTTP端点:

如果应该从语言中完全删除DOT导入,则存在开放辩论。除了测试包中的任何位置,Go Team不建议使用它们:

它使程序更难阅读,因为目前尚不清楚QUUX的名称是否是当前包中的顶级标识符或导入的包中。

https://golang.org/doc/faq.

此外,如果使用Go-Lint工具,当在测试文件之外使用DOT导入时,它将显示警告,并且您无法轻松关闭。

Go Team建议的一个用例是在测试中,由于循环依赖项,无法成为被测试的包的一部分:

// foo_test package for foo packagepackage foo_timport("酒吧/ restutil" //也进口" foo"。" foo")

此测试文件不能成为foo包的一部分,因为它引用了栏/ restutil,其又转接foo和它会产生循环依赖性。

在这种情况下首先要考虑的是,如果可能有更好的方法来构建将避免循环依赖性的包。移动或可能无意义,可以从foo到第三个包装中移动到第三个包装,以至于foo和bar / testutil可以导入。然后,这将允许测试在Foo包中正常写入。

如果重组没有有意义并且测试被移动到单独的包,则使用点导入foo_test包,至少可以假装成为foo包的一部分。注意事项,无法访问Foo Package的未被实施的类型和功能。

可以说,在域特定语言中的DOT导入有一个很好用例。例如,GOA Framework将其用于配置。如果没有DOT导入,它看起来不会很棒:

封装DesignImport。 " goa.design/goa/v3/dsl" // api描述了api server.var _ = api的全局属性(" calc" func(){title(&# 34;计算器服务")描述("添加数字的HTTP服务,GOA预告值")服务器(" calc" func(){host(" localhost&# 34;,func(){URI(" http:// localhost:8088")})}))

未使用变量的存在可能表示错误[...]拒绝编制具有未使用变量或导入的程序,以便为长期构建速度和节目清晰度交易短期方便。

https://golang.org/doc/faq.

package mainvar未使用global int //这是okfunc f1(未使用的函数参数也是OK //错误:声明但未使用的a,b:= 1,2 // b此处使用,但是a是只分配到,不计入“使用”a = b}

package mainv1:= 1 //错误:非声明语句外部函数bodyvar v2 = 2 //这是Okfunc main(){v3:= 3 //这是OK FMT.Println(v3)}

包维护MyStruct struct {field int} func main(){var s mystruct //错误:左侧的非名称s.field:= s.field,newvar:= 1,2 var newvar int s.field, newvar = 1,2 //这实际上是可以的}

允许可悲的变色阴影。它是不断注意的,因为它可能导致难以发现的问题。这发生了,因为如果至少一个变量是新的,则使用速记变量声明,允许使用速记变量声明:

Package MainImport" fmt" func main(){v1:= 1 // v1实际上没有重新录制,只得到一个新的值设置v1,v2:= 2,3 fmt.println(v1,v2) //打印2,3}

但是,如果声明是另一个代码块的内部,它将声明一个新变量,可能导致严重的错误:

包裹MainImport" fmt" func main(){v1:= 1如果v1 == 1 {v1,v2:= 2,3 fmt.println(v1,v2)//打印2,3} fmt。 println(v1)//打印1!}

Package MainImport(""" fmt")func func1()错误{return nil} func errfun1()(int,错误){return 1,错误.new("重要的错误")} func returnserr()错误{err:= func1()如果err == nil {v1,err:= errfunc1()如果err!= nil {fmt.println(v1,err)// prints :1重要错误}} return err //这个returns nil!func main(){fmt.println(returnserr())// prints nil}

对此的一个解决方案是不使用嵌套代码块内的速记声明:

func returnserr()错误{err:= func1()var v1 int如果err == nil {v1,err = errfunc1()如果err!= nil {fmt.println(v1,err)// print:1重要错误}返回err //返回"重要的错误"}

Func returnserr()错误{err:= func1()如果err!= nil {returner} v1,err:= errfunc1()如果err!= nil {fmt.println(v1,err)// print:1重要错误return err} return nil}

还有工具可以提供帮助。 GO VET工具中有一个实验遮蔽的变量检测。它后来被删除了。在撰写本文时,您如何安装和运行该工具:

优先级操作员5* /%<< >> & & ^ 4 + - | ^ 3 ==!=< < => > = 2&& 1 ||

优先操作员10 *,/,%9 +,-8<> 7>>> = 6 ==,!= 5& 4 ^ 3 | 2&amp ;& 1 ||

在Go:1<< 1 + 1 //(1< 1)+1 = 3℃:1< 1 + 1 // 1<<(1 + 1)= 4

var i int ++ i //语法错误:意外++,期待} - i //语法错误:意外 - ,期待}

虽然Go确实有这些运营商的Postfix版本,但在表达式中不允许: 没有,不寻找它。 您必须使用else。 去语言设计师决定这个运营商经常会导致丑陋的代码和它'最好不要拥有它。 在Go XOR运算符中,用作一元而不是运算符而不是〜〜符号,如许多其他语言。 IOTA开始持续编号。 由于有人可能期望,它并不意味着“从零开始”。 它是当前Const块中的常数索引: const(Zero = IOTA // 0一个// 1两= IOTA // 2) 在Go切片和阵列提供类似的目的。 它们几乎相同的方式宣布: 包裹MainImport" fmt" func main(){slice:= [] int {1,2,3} array:= [3] int {1,2,3} //让编译器锻炼阵列 长度//这将是[3] int array2:= [...] int {1,2,3} fmt.println(slice,array,array2)}

切片感觉像顶部有用功能的阵列。他们在内部使用指针到实施。然而,切片是如此方便,阵列很少直接使用。

阵列是固定大小的键入的内存集。不同长度的阵列被认为是不同的不兼容类型。与C中的不同,当创建数组时,数组元素初始化为零值,因此无需明确执行此操作。同样与C的不同,Array是一个值类型。它不是对存储块的第一个元素的指针。如果将数组传递成函数,则将复制整个数组。您仍然可以将指针传递到数组以不复制。

切片是数组段的描述符。这是一个非常有用的数据结构,但也许略有异常。有几种方法可以在脚下射击自己,如果你知道切片在内部工作,那么所有这些都可以避免。这是Go源代码中切片的实际定义:

这具有有趣的影响。切片本身是一个值类型,但它引用了它使用指针的数组。与数组不同,如果您将切片传递给函数,则会获得数组指针,LEN和CAP属性的副本(上面的图像中的第一个块),但阵列本身的数据不会被复制切片的两个副本都将指向相同的阵列。当你“切片”切片时,会发生同样的事情。切片创建一个新的切片,它仍然指向相同的数组:

Package MainImport" fmt" func f1(s [] int){//切片切片创建一个新切片//但不复制阵列数据s = s [2:4] //修改子-slice //在主函数中更改切片数组,同时为i:=范围s {s [i] + = 10} fmt.println(" f1",len(s),帽子(s))} func main(){s:= [] int {1,2,3,4,5} //通过切片作为参数//使得切片属性的副本(指针,LEN和CAP )//但副本共享相同的数组f1 fmt.println(" main",len(s),帽子)}

如果您没有意识到切片,您可能会假设它是一个值类型,并且感到惊讶,F1“损坏”在主要的切片中的数据。

要使用其数据获取切片的副本,您需要做一些工作。您可以手动将元素复制到新切片或使用副本或附加:

Package MainImport" fmt" func f1(s [] int){s = s [2:4] s2:= make([] int,len(s))副本(s2,s)//或如果您更喜欢效率,但更简洁的版本:// s2:= i for i:=范围s2 {s2 [i] + 10} fmt .println(" f1",s2,len(s2),帽(s2))} func main(){s:= [] int {1,2,3,4,5} f1(s )fmt.println(" main",s,len(s),帽子)}

因此,将切片弄乱了指向LEN和CAP属性的事情,并且切片的所有副本都共享相同的数组。直到他们没有。切片的最有用的属性是它管理为您生长阵列。当需要增长现有阵列的容量时,需要分配一个完全新的数组。如果您期望两份切片的两份分享阵列,这也可以是一个陷阱:

Package MainImport" fmt" func main(){//使得长度为3和容量4 s:= make([] int,3,4)//初始化为1,2,3 s [ 0] = 1 s [1] = 2 s [2] = 3 //阵列的容量为4 //在初始阵列S2中添加一个更多数字:= Append(S,4)//修改元素array // s和s2仍然共享相同的阵列I:=范围S2 {S2 [i] + = 10} FMT.PrintLn(s,len(s),帽(s)// [11 12 13] 3 4 fmt.println(S2,Len(S2),帽(S2))// [11 12 13 14] 4 4 //该附加增长阵列超过其容量//必须为S3 S3分配新数组:= Append(S2,5)//修改数组的元素,以查看I:=范围S3 {S3 [i] + 10} FMT.Println(s,len(s),帽)/ /仍然是旧阵列[11 12 13] 3 4 fmt.println(s2,len(s2),帽(s2))//旧阵列[11 12 13 14] 4 4 //数组在最后一个附加上复制[ 21 22 23 24 15] 5 8 8 fmt.println(S3,Len(S3),帽(S3))}

不必检查切片的数量,并不需要初始化。 LEN,CAP和Append等功能在零切片上工作正常:

Package MainImport" fmt" func main(){var s [] int // nil slice fmt.println(s,len(s),cap(s)// [] 0 0 0 s = Append( s,1)fmt.println(s,len(s),帽子)// [1] 1 1 1}

Package MainImport" fmt" func main(){var s [] int //这是一个nil slice s2:= [] int {} //这是一个空的切片//看起来在这里同样的事情:fmt.println(s,len(s),帽子)// [] 0 0 0 fmt.println(S2,LEN(S2),帽(S2))// [] 0 0 //但S2是实际上分配了一些地方fmt.printf("%p%p%p" s,s2)// 0x0 0x65ca90}

如果您非常关心性能和内存使用情况,并且初始化空切片可能不如使用NIL切片的理想。

要创建新的切片,可以使用切片类型和切片的初始长度和容量来使用make。容量参数是可选的:

Package MainImport(" fmt")func main(){s:= make([] int,3)s =附加(s,1)s =附加(s,2)s =附加(s, 3)fmt.println(s)}

不,这永远不会发生在我身上,我知道将切片的第二个论点是长度,而不是容量,我听到你说的话......

因为切片数组创建一个新切片,但共享底层数组,因此可以在内存中保持更多的数据,然后您可能想要或期望。这是一个愚蠢的例子:

package mainimport("字节"" fmt"" io / ioutil"" os")func getexecutableFormat()[]字节{//阅读我们的自己可执行文件进入内存字节,错误:= ioutil.readfile(OS.ARGS [0])如果ERR!= nil {PANIC(ERR)}返回字节[:4]} FUNC MAIN(){格式:= getExecutableFormat()如果bytes.hasprefix(格式,[]字节(" elf")){fmt.println(" linux可执行文件")}如果bytes.hasprefix(format,[] byte(& #34; mz")){fmt.println(" windows可执行文件")}}

在上面的代码中,只要该格式变量处于范围并且不能收集垃圾,整个可执行文件(可能很少有兆字节的数据)都必须保持在内存中。要修复它,请制作实际需要的字节的副本。

此时没有这样的事情。可能有一天,但目前你需要手动使用单个维度切片作为多维切片,通过求解元素索引或使用“锯齿状”切片(锯齿状切片是一片切片):

Package MainImport" fmt" func main(){x:= 2 y:= 3 s:= make([] [] int,y)for i:= srount s {s [i] = make( [] int,x)} fmt.println(s)}

String本身是一个值类型,它具有指向字节数组和固定长度的指针。字符串中的零字节不会标记在C中的字符串的末尾。字符串中可以存在任何数据。通常,该数据被编码为UTF-8字符串,但它不一定是。

字符串从来没有零。字符串的默认值是空字符串,而不是nil:

Package Mainimport" fmt" func main(){var s string fmt.println(s =="")// true s = nil //错误:不能使用nil作为类型字符串在作业}

package mainfunc main(){str:=" Darkercorners" str [0] =' D' //错误:无法分配给str [0]}

不可变数据更容易推理,从而创造较少的塔拉斯。缺点是每次要从字符串中添加或删除某些东西,都必须分配全新的字符串。如果您真的很愿意,可以通过不安全的包来修改字符串,但如果您发现自己发生这种方式,您可能太聪明了。当您可能想要担心分配时,最常见的情况是当需要加入许多字符串时。有一个字符串。用于此目的的Builder类型。 strings.Builder以批处理分配内存,而不是每次添加字符串时:

Package MainImport(" Strconv""字符串""测试" test#34;)func基准(b * testing.b){var str string for i:= 0;我< B.N; i ++ {str + = strconv.itoa(i)}} func benchmarkstringbuilder(b * testing.b){var str strings.builder for i:= 0;我< B.N; i ++ {str.writeString(strconv.itoa(i))}}

BenchmarkString-8 401053 147346 NS / OP 1108686 B / OP 2 Allocs / OpbenchmarkStringBuilder-8 29307392 44.9 NS / OP 52 B / OP 0 Allocs / Op 在此示例中使用strings.Builder比简单地添加字符串(每次分配新内存)快3000倍。 在字符串转换为字节的范围子句中:对于i,v:=范围[]字节(str){...} Go编译器的新版本可能会增加更多优化,因此如果性能是 ......