沧州三亚菏泽经济预测自然
投稿投诉
自然科学
知识物理
化学生物
地理解释
预测理解
本质社会
人类现象
行为研究
经济政治
心理结构
关系指导
人文遗产
菏泽德阳
山西湖州
宝鸡上海
茂名内江
三亚信阳
长春北海
西安安徽
黄石烟台
沧州湛江
肇庆鹤壁
六安韶关
成都钦州

Go语言自学系列高效golang开发

4月8日 霸王亭投稿
  视频来源:B站《golang入门到项目实战〔2021最新Go语言教程,没有废话,纯干货!持续更新中。。。〕》
  一边学习一边整理老师的课程内容及试验笔记,并与大家分享,侵权即删,谢谢支持!
  附上汇总贴:Go语言自学系列汇总COCOgsta的博客CSDN博客
  介绍
  Go是一种新语言。虽然它借鉴了现有语言的思想,但它具有不同寻常的特性,使有效的Go程序在特性上不同于用它的亲戚编写的程序。将C或Java程序直接翻译成Go不太可能产生令人满意的结果Java程序是用Java编写的,而不是Go。另一方面,从Go的角度思考问题可能会产生一个成功但完全不同的程序。换句话说,要写好Go,重要的是要了解它的属性和习语。了解Go编程的既定约定也很重要,例如命名、格式化、程序构造等,以便您编写的程序易于其他Go程序员理解。
  本文档提供了编写清晰、惯用的Go代码的技巧。它扩充了语言规范、Go之旅和如何编写Go代码,您应该首先阅读所有这些内容。格式化
  格式问题是最有争议但最不重要的问题。人们可以适应不同的格式风格,但如果他们没有必要,那就更好了,如果每个人都坚持相同的风格,那么花在该主题上的时间就会更少。问题是如何在没有长篇规范风格指南的情况下接近这个乌托邦。
  在Go中,我们采用了一种不同寻常的方法,让机器处理大多数格式问题。该gofmt程序(也可用作gofmt,它在包级别而不是源文件级别运行)读取Go程序并以标准的缩进和垂直对齐方式发出源代码,保留并在必要时重新格式化注释。如果你想知道如何处理一些新的布局情况,运行如果答案似乎不正确,请重新安排您的程序(或提交关于的错误gofmt),不要解决它。
  例如,无需花时间排列结构字段的注释。Gofmt会为你做的。鉴于声明typeTstruct{namestringnameoftheobjectvalueintitsvalue}
  gofmt将排列列:typeTstruct{namestringnameoftheobjectvalueintitsvalue}
  标准包中的所有Go代码都已格式化为gofmt。
  一些格式细节仍然存在。非常简短:缩进我们使用制表符进行缩进并gofmt默认发出它们。仅在必须时才使用空格。长度限制Go没有行长度限制。不用担心打孔卡溢出。如果一行感觉太长,请将其包裹起来并用额外的标签缩进。括弧Go需要的括号比C和Java少:控制结构(if,for,switch)的语法中没有括号。此外,运算符优先级层次结构更短更清晰,因此x8y16表示间距的含义,与其他语言不同。注释
  Go提供了C风格的块注释和C风格的行注释。行注释是常态;块注释主要作为包注释出现,但在表达式中或禁用大量代码时很有用。
  该程序和Web服务器godoc处理Go源文件以提取有关包内容的文档。出现在顶级声明之前的注释,中间没有换行符,与声明一起被提取出来作为项目的解释性文本。这些注释的性质和风格决定了文档godoc生成的质量。
  每个包都应该有一个包注释,包子句之前的块注释。对于多文件包,包注释只需要出现在一个文件中,任何一个都可以。包注释应介绍包并提供与整个包相关的信息。它将首先出现在godoc页面上,并应设置后面的详细文档。Packageregexpimplementsasimplelibraryforregularexpressions。Thesyntaxoftheregularexpressionsacceptedis:regexp:concatenation{concatenation}concatenation:{closure}closure:term〔?〕term:39;。character〔〔〕characterranges〕(regexp)packageregexp
  如果包装简单,包装注释可以简短。Packagepathimplementsutilityroutinesformanipulatingslashseparatedfilenamepaths。
  注释不需要额外的格式,例如星星横幅。生成的输出甚至可能不会以固定宽度的字体呈现,所以不要依赖间距来对齐godoc像一样gofmt,会照顾到这一点。注释是未解释的纯文本,因此HTML和其他注释(例如)this将逐字复制,不应使用。一种调整godoc确实是以固定宽度的字体显示缩进文本,适用于程序片段。对于包注释fmt包使用此效果良好。
  根据上下文,godoc甚至可能不会重新格式化注释,因此请确保它们直接看起来不错:使用正确的拼写、标点符号和句子结构,折叠长行等。
  在包内,紧接在顶级声明之前的任何注释都用作该声明的文档注释。程序中每个导出(大写)的名称都应该有一个文档注释。
  文档注释作为完整的句子效果最好,它允许各种自动演示。第一句话应该是一个以声明的名字开头的一句话总结。Compileparsesaregularexpressionandreturns,ifsuccessful,aRegexpthatcanbeusedtomatchagainsttext。funcCompile(strstring)(Regexp,error){
  如果每个doc注释都以其描述的项目名称开头,则可以使用go工具的doc子命令并通过。想象一下,你不记得名字编译,但正在寻找正则表达式的解析函数,所以你运行了命令,grepgodocallregexpgrepiparse
  如果包中的所有文档注释都以此函数。。。开头,grep将无法帮助您记住名称。但是因为包以名称开始每个文档注释,所以您会看到类似这样的内容,它会回忆起您正在寻找的单词。godocallregexpgrepiparseCompileparsesaregularexpressionandreturns,ifsuccessful,aRegexpMustCompileislikeCompilebutpanicsiftheexpressioncannotbeparsed。parsed。Itsimplifiessafeinitializationofglobalvariablesholding
  Go的声明语法允许对声明进行分组。单个文档注释可以引入一组相关的常量或变量。由于提出了整个声明,这样的注释通常是敷衍的。Errorcodesreturnedbyfailurestoparseanexpression。var(ErrInternalerrors。New(regexp:internalerror)ErrUnmatchedLparerrors。New(regexp:unmatched()ErrUnmatchedRparerrors。New(regexp:unmatched))。。。)
  分组还可以指示项目之间的关系,例如一组变量受互斥锁保护的事实。var(countLocksync。MutexinputCountuint32outputCountuint32errorCountuint32)命名
  名称在Go中与在任何其他语言中一样重要。它们甚至具有语义效果:包外名称的可见性取决于其第一个字符是否为大写。因此,值得花一点时间讨论Go程序中的命名约定。
  包名
  导入包时,包名称成为内容的访问器。后importbytes
  导入包可以谈bytes。Buffer。如果每个使用包的人都可以使用相同的名称来引用其内容,这将很有帮助,这意味着包名称应该是好的:简短、简洁、令人回味。按照惯例,包使用小写的单字名称;应该不需要下划线或混合大写字母。简而言之,因为使用您的包的每个人都会输入该名称。并且不要担心先验的碰撞。包名只是导入的默认名称;它不需要在所有源代码中都是唯一的,并且在极少数发生冲突的情况下,导入包可以选择不同的名称以在本地使用。在任何情况下,混淆都很少见,因为导入中的文件名决定了正在使用哪个包。
  另一个约定是包名是其源目录的基本名称;中的包srcencodingbase64被导入为encodingbase64但具有namebase64,notencodingbase64和notencodingBase64。
  包的导入器将使用名称来引用其内容,因此包中的导出名称可以使用该事实来避免重复。(不要使用import。符号,它可以简化必须在他们正在测试的包之外运行的测试,否则应该避免。)例如,包中的缓冲读取器类型bufio被称为Reader,而不是BufReader,因为用户将其视为bufio。Reader,这是一个清晰简洁的名称。此外,由于导入的实体总是以其包名寻址,因此bufio。Reader不会与io。Reader。类似地,创建新实例的函数ring。Ring这是Go中构造函数的定义通常会被调用NewRing,但由于Ring是包导出的唯一类型,并且由于包被称为ring,因此它被称为justNew,包的客户端将其视为ring。New。使用包结构来帮助您选择好名字。
  另一个简短的例子是once。Do;once。Do(setup)读得好,不会因写作而改善once。DoOrWaitUntilDone(setup)。长名称不会自动使内容更具可读性。有用的文档注释通常比超长的名称更有价值。
  Getters
  Go不提供对getter和setter的自动支持。自己提供getter和setter并没有错,而且这样做通常是合适的,但这既不是惯用的,也不是必需Get的。如果您有一个名为owner(小写,未导出)的字段,则应调用getter方法Owner(大写,导出),而不是GetOwner。使用大写名称导出提供了区分字段和方法的钩子。如果需要,可能会调用setter函数SetOwner。这两个名字在实践中都很好读:owner:obj。Owner()ifowner!user{obj。SetOwner(user)}
  接口名称
  按照惯例,一个方法接口由该方法name加上后缀er或类似的修改命名构建的试剂名:Reader,Writer,Formatter,CloseNotifier等。
  有许多这样的名称,尊重它们和它们捕获的函数名称是很有成效的。Read,Write,Close,Flush,String等有规范签名和意义。为避免混淆,除非具有相同的签名和含义,否则不要为您的方法指定其中一个名称。相反,如果您的类型实现了一个与众所周知类型上的方法具有相同含义的方法,则为其赋予相同的名称和签名;调用您的字符串转换器方法StringnotToString。
  混合大写(驼峰命名法)
  最后,Go中的约定是使用MixedCapsormixedCaps而不是下划线来编写多词名称。分号
  与C一样,Go的正式语法使用分号来终止语句,但与C不同的是,这些分号不会出现在源代码中。相反,词法分析器使用一个简单的规则在扫描时自动插入分号,因此输入文本大部分都没有分号。
  规则是这样的。如果换行符之前的最后一个标记是标识符(包括像intand之类的词float64)、基本文字(例如数字或字符串常量)或标记之一breakcontinuefallthroughreturn)}
  词法分析器总是在标记后插入一个分号。这可以概括为,如果换行符出现在可以结束语句的标记之后,则插入分号。
  分号也可以直接在右大括号之前省略,所以像这样的语句gofunc(){for{dstsrc}}()
  不需要分号。惯用的Go程序仅在for循环子句等地方使用分号,以分隔初始化器、条件和延续元素。如果您以这种方式编写代码,它们对于分隔一行中的多个语句也是必要的。
  的分号插入规则的一个后果是,你不能把一个控制结构(中左括号if,for,switch,或select)在下一行。如果这样做,将在大括号之前插入分号,这可能会导致不必要的影响。像这样写它们ifif(){g()}
  不是这样ifif()wrong!{wrong!g()}控制结构
  Go的控制结构与C的控制结构相关,但在重要方面有所不同。没有doorwhile循环,只有一个稍微概括的switch更灵活;if并switch接受一个可选的初始化语句,如break和continue语句采用可选标签来标识要中断或继续的内容;并且有新的控制结构,包括类型开关和多路通信多路复用器,select。语法也略有不同:没有括号,主体必须始终以大括号分隔。
  如果
  在Go中,一个简单的if看起来像这样:ifx0{returny}
  强制大括号鼓励if在多行上编写简单的语句。无论如何,这样做是一种很好的风格,尤其是当主体包含诸如areturn或之类的控制语句时break。
  由于if并switch接受初始化语句,因此通常会看到用于设置局部变量的语句。iferr:file。Chmod(0664);err!nil{log。Print(err)returnerr}
  在Go库中,你会发现当一个if语句没有流入下一个语句时也就是说,主体以break、continue、goto、或return结尾,不必要的else被省略。f,err:os。Open(name)iferr!nil{returnerr}codeUsing(f)
  这是代码必须防范一系列错误条件的常见情况的示例。如果成功的控制流沿着页面向下运行,则代码可读性良好,从而消除出现的错误情况。由于错误案例往往以return语句结尾,因此生成的代码不需要else语句。f,err:os。Open(name)iferr!nil{returnerr}d,err:f。Stat()iferr!nil{f。Close()returnerr}codeUsing(f,d)
  声明和赋值
  上一节中的最后一个示例详细说明了:简短声明表单的工作原理。调用的声明os。Open如下,f,err:os。Open(name)
  该语句声明了两个变量,f和err。几行之后,调用f。Stat读取,d,err:f。Stat()
  看起来好像声明了danderr。但是请注意,这err出现在两个语句中。这种重复是合法的:err由第一条语句声明,但仅在第二条语句中重新赋值。这意味着调用f。Stat使用err上面声明的现有变量,并为其赋予一个新值。
  在:声明中,v即使变量已经声明,也可能出现,条件是:此声明与现有声明的作用域相同v(如果v已在外部作用域中声明,则该声明将创建一个新变量),初始化中的相应值可分配给v,并且至少有一个由声明创建的其他变量。
  这种不寻常的属性是纯粹的实用主义,可以很容易地使用单个err值,例如,在长ifelse链中。你会看到它经常被使用。
  这里值得注意的是,在Go中,函数参数和返回值的范围与函数体相同,即使它们在词法上出现在包围体的大括号之外。
  for
  Gofor循环与C的相似但不相同。它统一for并且while没有dowhile。共有三种形式,其中只有一种带有分号。LikeaCpost{}LikeaCwhileforcondition{}LikeaCfor(;;)for{}
  简短的声明使得在循环中声明索引变量变得容易。sum:0fori:0;i10;i{sumi}
  如果您在数组、切片、字符串或映射上循环,或者从通道读取,range子句可以管理循环。forkey,value:rangeoldMap{newMap〔key〕value}
  如果您只需要范围中的第一项(键或索引),请删除第二项:forkey:rangem{ifkey。expired(){delete(m,key)}}
  如果您只需要范围中的第二项(值),请使用空白标识符(下划线)来丢弃第一项:sum:0for,value:rangearray{sumvalue}
  空白标识符有很多用途,如后面的部分所述。
  对于字符串,range它可以为您做更多的工作,通过解析UTF8来分解单个Unicode代码点。错误的编码消耗一个字节并产生替换符文UFFFD。(名称(带有关联的内置类型)rune是单个Unicode代码点的Go术语。有关详细信息,请参阅语言规范。)循环forpos,char:range日本語{isanillegalUTF8encodingfmt。Printf(characterUstartsatbytepositiond,char,pos)}
  打印characterU65E5日startsatbyteposition0characterU672C本startsatbyteposition3characterUFFFDstartsatbyteposition6characterU8A9E語startsatbyteposition7
  最后,Go没有逗号运算符and是语句而不是表达式。因此,如果您想在a中运行多个变量,for您应该使用并行赋值(尽管这排除了和)。Reverseafori,j:0,len(a)1;i,ji1,j1{a〔i〕,a〔j〕a〔j〕,a〔i〕}
  Switch
  Goswitch比C更通用。表达式不必是常量甚至整数,情况从上到下计算,直到找到匹配项,如果switch没有表达式,则打开true。因此,将ifelseifelse链编写为switch。funcunhex(cbyte)byte{switch{case0cc9:returnc0caseaccf:returnca10caseAccF:returncA10}return0}
  没有自动失败,但案例可以以逗号分隔的列表形式呈现。funcshouldEscape(cbyte)bool{switchc{case,?,,,,,:returntrue}returnfalse}
  尽管它们在Go中不像其他一些类似C的语言那样常见,但break语句可用于提前终止switch。然而,有时,有必要跳出周围的循环,而不是switch,而在Go中,这可以通过在循环上放置一个标签并打破该标签来实现。这个例子展示了这两种用途。Loop:forn:0;nlen(src);nsize{switch{casesrc〔n〕sizeOne:ifvalidateOnly{break}size1update(src〔n〕)casesrc〔n〕sizeTwo:ifn1len(src){errerrShortInputbreakLoop}ifvalidateOnly{break}size2update(src〔n〕src〔n1〕shift)}}
  当然,该continue语句也接受一个可选标签,但它仅适用于循环。
  为了结束本节,这里有一个使用两个switch语句的字节切片比较例程:Comparereturnsanintegercomparingthetwobyteslices,lexicographically。Theresultwillbe0ifab,1ifab,and1ifabfuncCompare(a,b〔〕byte)int{fori:0;ilen(a)ilen(b);i{switch{casea〔i〕b〔i〕:return1casea〔i〕b〔i〕:return1}}switch{caselen(a)len(b):return1caselen(a)len(b):return1}return0}
  类型switch
  开关还可用于发现接口变量的动态类型。这种类型开关使用类型断言的语法,type在括号内带有关键字。如果switch在表达式中声明了一个变量,则该变量将在每个子句中具有相应的类型。在这种情况下重用名称也是惯用的,实际上在每种情况下都声明了一个具有相同名称但类型不同的新变量。vartinterface{}tfunctionOfSomeType()switcht:t。(type){default:fmt。Printf(unexpectedtypeT,t)Tprintswhatevertypethascasebool:fmt。Printf(booleant,t)thastypeboolcaseint:fmt。Printf(integerd,t)thastypeintcasebool:fmt。Printf(pointertobooleant,t)thastypeboolcaseint:fmt。Printf(pointertointegerd,t)thastypeint}函数
  多个返回值
  Go不寻常的特性之一是函数和方法可以返回多个值。这种形式可用于改进C程序中的几个笨拙的习惯用法:带内错误返回,例如1forEOF和修改按地址传递的参数。
  在C中,写入错误由负计数表示,错误代码隐藏在易失性位置。在Go中,Write可以返回一个计数和一个错误:是的,你写了一些字节,但不是全部,因为你填满了设备。Write包中文件的方法签名os是:func(fileFile)Write(b〔〕byte)(nint,errerror)
  正如文档所说,它返回写入的字节数和非零errorwhenn!len(b)。这是一种常见的风格;有关更多示例,请参阅错误处理部分。
  类似的方法不需要传递指向返回值的指针来模拟引用参数。这是一个简单的函数,从字节切片中的某个位置抓取一个数字,返回该数字和下一个位置。funcnextInt(b〔〕byte,iint)(int,int){ilen(b)!isDigit(b〔i〕);i{}x:0ilen(b)isDigit(b〔i〕);i{xx10int(b〔i〕)0}returnx,i}
  您可以使用它来扫描输入切片中的数字,b如下所示:fori:0;ilen(b);{x,inextInt(b,i)fmt。Println(x)}
  命名结果参数
  Go函数的返回或结果参数可以命名并用作常规变量,就像传入参数一样。当命名时,它们在函数开始时被初始化为它们的类型的零值;如果函数执行return不带参数的语句,则结果参数的当前值将用作返回值。
  这些名称不是强制性的,但它们可以使代码更短、更清晰:它们是文档。如果我们命名nextInt它的结果就很明显返回的int是哪个。funcnextInt(b〔〕byte,posint)(value,nextPosint){
  因为命名结果被初始化并绑定到一个简单的返回值,它们可以简化和澄清。这是一个io。ReadFull很好地使用它们的版本:funcReadFull(rReader,buf〔〕byte)(nint,errerror){forlen(buf)0errnil{varnrintnr,errr。Read(buf)nnrbufbuf〔nr:〕}return}
  defer
  Go的defer语句安排一个函数调用(延迟函数)在函数执行defer返回之前立即运行。这是一种不寻常但有效的方法来处理诸如必须释放资源的情况,而不管函数采用哪条路径返回。规范示例是解锁互斥锁或关闭文件。Contentsreturnsthefilescontentsasastring。funcContents(filenamestring)(string,error){f,err:os。Open(filename)iferr!nil{return,err}deferf。Close()f。Closewillrunwhenwerefinished。varresult〔〕bytebuf:make(〔〕byte,100)for{n,err:f。Read(buf〔0:〕)resultappend(result,buf〔0:n〕。。。)appendisdiscussedlater。iferr!nil{iferrio。EOF{break}return,errfwillbeclosedifwereturnhere。}}returnstring(result),nilfwillbeclosedifwereturnhere。}
  推迟对诸如此类的函数的调用Close有两个优点。首先,它保证您永远不会忘记关闭文件,如果您稍后编辑该函数以添加新的返回路径,则很容易犯这个错误。其次,这意味着收盘价位于开盘价附近,这比将其放在函数的末尾要清晰得多。
  延迟函数的参数(如果函数是方法,则包括接收器)在延迟执行时计算,而不是在调用执行时计算。除了避免担心在函数执行时变量会改变值,这意味着单个延迟调用站点可以延迟多个函数执行。这是一个愚蠢的例子。fori:0;i5;i{deferfmt。Printf(d,i)}
  延迟函数以LIFO顺序执行,因此该代码将导致43210在函数返回时打印。一个更合理的例子是通过程序跟踪函数执行的简单方法。我们可以编写几个简单的跟踪例程,如下所示:functrace(sstring){fmt。Println(entering:,s)}funcuntrace(sstring){fmt。Println(leaving:,s)}Usethemlikethis:funca(){trace(a)deferuntrace(a)dosomething。。。。}
  我们可以通过利用延迟函数的参数在defer执行时评估这一事实来做得更好。跟踪例程可以为untracing例程设置参数。这个例子:functrace(sstring)string{fmt。Println(entering:,s)returns}funcun(sstring){fmt。Println(leaving:,s)}funca(){deferun(trace(a))fmt。Println(ina)}funcb(){deferun(trace(b))fmt。Println(inb)a()}funcmain(){b()}
  打印entering:binbentering:ainaleaving:aleaving:b
  对于习惯于其他语言的块级资源管理的程序员来说,这defer可能看起来很奇怪,但它最有趣和最强大的应用程序恰恰来自于它不是基于块而是基于函数的事实。在和的部分中panic,recover我们将看到其可能性的另一个示例。数据
  分配与new
  Go有两个分配原语,内置函数new和make。它们做不同的事情并适用于不同的类型,这可能会令人困惑,但规则很简单。让我们new先谈谈。它是一个分配内存的内置函数,但与其他一些语言中的同名函数不同,它不会初始化内存,只会将其归零。也就是说,new(T)为一个新的type项分配零存储T并返回它的地址,一个type的值T。在Go术语中,它返回一个指向新分配的类型零值的指针T。
  由于由返回的内存new已归零,因此在设计数据结构时安排每种类型的零值无需进一步初始化即可使用是有帮助的。这意味着数据结构的用户可以创建一个new并开始工作。例如,文档bytes。Buffer说明零值Buffer是一个准备使用的空缓冲区。同样,sync。Mutex没有显式构造函数或Init方法。相反,a的零值sync。Mutex被定义为未锁定的互斥锁。
  零值有用的属性可以传递。考虑这个类型声明。typeSyncedBufferstruct{locksync。Mutexbufferbytes。Buffer}
  type的值SyncedBuffer也可以在分配或声明后立即使用。在下一个代码段中,p和v无需进一步安排即可正常工作。p:new(SyncedBuffer)typeSyncedBuffervarvSyncedBuffertypeSyncedBuffer
  构造函数和复合字面量
  有时零值不够好,需要一个初始化构造函数,如从package派生的这个例子os。funcNewFile(fdint,namestring)File{iffd0{returnnil}f:new(File)f。fdfdf。namenamef。dirinfonilf。nepipe0returnf}
  里面有很多锅炉板。我们可以使用复合字面量来简化它,这是一个每次计算时都会创建一个新实例的表达式。funcNewFile(fdint,namestring)File{iffd0{returnnil}f:File{fd,name,nil,0}returnf}
  请注意,与C不同,返回局部变量的地址是完全可以的;与变量关联的存储在函数返回后仍然存在。事实上,每次计算复合文字的地址时都会分配一个新实例,因此我们可以将最后两行组合起来。returnFile{fd,name,nil,0}
  复合文字的字段按顺序排列,并且必须全部存在。但是,通过将元素显式标记为字段:值对,初始值设定项可以按任何顺序出现,缺失的作为各自的零值保留。因此我们可以说returnFile{fd:fd,name:name}
  作为一种限制情况,如果复合文字根本不包含任何字段,它会为该类型创建一个零值。表达式new(File)和File{}是等价的。
  还可以为数组、切片和映射创建复合文字,字段标签是索引或映射键(视情况而定)。在这些例子中,初始化工作无论的值的Enone,Eio和Einval,只要它们是不同的。a:〔。。。〕string{Enone:noerror,Eio:Eio,Einval:invalidargument}s:〔〕string{Enone:noerror,Eio:Eio,Einval:invalidargument}m:map〔int〕string{Enone:noerror,Eio:Eio,Einval:invalidargument}
  分配与make
  回到分配。内置函数make(T,args)的用途不同于new(T)。它仅创建切片、映射和通道,并返回类型为(not)的初始化(未清零)值。区别的原因是这三种类型在幕后表示对必须在使用前初始化的数据结构的引用。例如,切片是一个三项描述符,包含指向数据(在数组内)、长度和容量的指针,并且在这些项被初始化之前,切片是。对于切片、映射和通道,初始化内部数据结构并准备使用值。例如,TTnilmakemake(〔〕int,10,100)
  分配一个包含100个整数的数组,然后创建一个长度为10、容量为100的切片结构,指向数组的前10个元素。(制作切片时,可以省略容量;有关更多信息,请参阅切片部分。)相反,new(〔〕int)返回指向新分配的、归零的切片结构的指针,即指向nil切片值的指针。
  这些示例说明了new和之间的区别make。varp〔〕intnew(〔〕int)rarelyusefulvarv〔〕intmake(〔〕int,100)theslicevnowreferstoanewarrayof100intsUnnecessarilycomplex:varp〔〕intnew(〔〕int)pmake(〔〕int,100,100)Idiomatic:v:make(〔〕int,100)
  请记住,这make仅适用于映射、切片和通道,并且不返回指针。获得显式指针分配new或显式获取变量的地址。
  数组
  数组在规划内存的详细布局时很有用,有时可以帮助避免分配,但主要是它们是切片的构建块,下一节的主题。为了为该主题奠定基础,这里有一些关于数组的词。
  Go和C中数组的工作方式有很大的不同。在Go中,数组是值。将一个数组分配给另一个会复制所有元素。特别是,如果你将一个数组传递给一个函数,它会收到一个数组的副本,而不是一个指向它的指针。数组的大小是其类型的一部分。类型〔10〕int和〔20〕int是不同的。
  value属性可能很有用,但也很昂贵;如果你想要类似C的行为和效率,你可以传递一个指向数组的指针。funcSum(a〔3〕float64)(sumfloat64){for,v:rangea{sumv}return}array:〔。。。〕float64{7。0,8。5,9。1}x:Sum(array)Notetheexplicitaddressofoperator
  但即使是这种风格也不是惯用的Go。改用切片。
  切片
  切片包装数组,为数据序列提供更通用、更强大、更方便的接口。除了具有显式维度的项(例如转换矩阵),Go中的大多数数组编程都是使用切片而不是简单数组完成的。
  切片保存对底层数组的引用,如果将一个切片分配给另一个切片,则两者都引用同一个数组。如果一个函数接受一个切片参数,它对切片元素所做的更改将对调用者可见,类似于传递一个指向底层数组的指针。甲Read因此函数可以接受一个切片参数,而不是一个指针和一个计数;切片内的长度设置了读取数据量的上限。这是package中类型的Read方法的签名:Fileosfunc(fFile)Read(buf〔〕byte)(nint,errerror)
  该方法返回读取的字节数和错误值(如果有)。读入所述第一32个字节的较大的缓冲区的buf,切片(这里用作动词)的缓冲液中。n,err:f。Read(buf〔0:32〕)
  这种切片是常见且高效的。事实上,暂时不考虑效率,以下代码段还将读取缓冲区的前32个字节。varnintvarerrerrorfori:0;i32;i{nbytes,e:f。Read(buf〔i:i1〕)Readonebyte。nnbytesifnbytes0e!nil{errebreak}}
  切片的长度可以改变,只要它仍然适合底层数组的限制;只需将其分配给自身的一部分。可通过内置函数访问的切片的容量cap报告切片可能采用的最大长度。这是一个将数据附加到切片的函数。如果数据超过容量,则重新分配切片。返回结果切片。该函数使用len和cap应用于nil切片时合法的事实,并返回0。funcAppend(slice,data〔〕byte)〔〕byte{l:len(slice)ifllen(data)cap(slice){reallocateAllocatedoublewhatsneeded,forfuturegrowth。newSlice:make(〔〕byte,(llen(data))2)Thecopyfunctionispredeclaredandworksforanyslicetype。copy(newSlice,slice)slicenewSlice}sliceslice〔0:llen(data)〕copy(slice〔l:〕,data)returnslice}
  之后我们必须返回切片,因为虽然Append可以修改的元素slice,但切片本身(保存指针、长度和容量的运行时数据结构)是按值传递的。
  附加到切片的想法非常有用,它被append内置函数捕获。但是,要了解该函数的设计,我们需要更多信息,因此稍后会返回。
  二维切片
  Go的数组和切片是一维的。要创建二维数组或切片的等效项,必须定义一个数组数组或切片数组,如下所示:typeTransform〔3〕〔3〕float64A3x3array,reallyanarrayofarrays。typeLinesOfText〔〕〔〕byteAsliceofbyteslices。
  因为切片是可变长度的,所以可以让每个内部切片的长度不同。这可能是一种常见情况,如我们的LinesOfText示例所示:每条线都有独立的长度。text:LinesOfText{〔〕byte(Nowisthetime),〔〕byte(forallgoodgophers),〔〕byte(tobringsomefuntotheparty。),}
  有时需要分配2D切片,例如,在处理像素扫描线时可能会出现这种情况。有两种方法可以实现这一点。一种是独立分配每个另一种是分配单个数组并将各个切片指向其中。使用哪个取决于您的应用程序。如果切片可能会增长或缩小,则应单独分配以避免覆盖下一行;如果没有,使用单个分配构造对象会更有效。作为参考,这里是两种方法的草图。首先,一次一行:Allocatethetoplevelslice。picture:make(〔〕〔〕uint8,YSize)Onerowperunitofy。Loopovertherows,allocatingthesliceforeachrow。fori:rangepicture{picture〔i〕make(〔〕uint8,XSize)}
  现在作为一个分配,分成几行:Allocatethetoplevelslice,thesameasbefore。picture:make(〔〕〔〕uint8,YSize)Onerowperunitofy。Allocateonelargeslicetoholdallthepixels。pixels:make(〔〕uint8,XSizeYSize)Hastype〔〕uint8eventhoughpictureis〔〕〔〕uint8。Loopovertherows,slicingeachrowfromthefrontoftheremainingpixelsslice。fori:rangepicture{picture〔i〕,pixelspixels〔:XSize〕,pixels〔XSize:〕}
  Map
  映射是一种方便且强大的内置数据结构,它将一种类型(键)的值与另一种类型(元素或值)的值相关联。键可以是定义了相等运算符的任何类型,例如整数、浮点数和复数、字符串、指针、接口(只要动态类型支持相等)、结构和数组。切片不能用作映射键,因为它们没有定义相等性。像切片一样,映射保存对底层数据结构的引用。如果您将地图传递给更改地图内容的函数,则更改将在调用方中可见。
  可以使用带有冒号分隔的键值对的常用复合文字语法构建映射,因此在初始化期间很容易构建它们。vartimeZonemap〔string〕int{UTC:06060,EST:56060,CST:66060,MST:76060,PST:86060,}
  分配和获取映射值在语法上看起来就像对数组和切片执行相同的操作,只是索引不需要是整数。offset:timeZone〔EST〕
  尝试使用映射中不存在的键获取映射值将返回映射中条目类型的零值。例如,如果映射包含整数,则查找不存在的键将返回0。集合可以实现为具有值类型的映射bool。将映射条目设置true为将值放入集合中,然后通过简单的索引对其进行测试。attended:map〔string〕bool{Ann:true,Joe:true,。。。}ifattended〔person〕{willbefalseifpersonisnotinthemapfmt。Println(person,wasatthemeeting)}
  有时您需要将缺失的条目与零值区分开来。是否有条目UTC或0因为它根本不在地图中?你可以用多重赋值的形式来区分。varsecondsintvarokboolseconds,oktimeZone〔tz〕
  出于显而易见的原因,这被称为逗号确定习语。在这个例子中,如果tz存在,seconds将被适当地设置并且ok为真;如果不是,seconds将被设置为零并且ok为假。这是一个将它与一个很好的错误报告放在一起的函数:funcoffset(tzstring)int{ifseconds,ok:timeZone〔tz〕;ok{returnseconds}log。Println(unknowntimezone:,tz)return0}
  要在不担心实际值的情况下测试地图中是否存在,您可以使用空白标识符()代替该值的常用变量。,present:timeZone〔tz〕
  要删除地图条目,请使用delete内置函数,其参数是要删除的地图和键。即使密钥已经不在地图上,这样做也是安全的。delete(timeZone,PDT)NowonStandardTime
  打印
  Go中的格式化打印使用类似于Cprintf家族的风格,但更丰富和更通用。该函数住在fmt包装和有大写的名字:fmt。Printf,fmt。Fprintf,fmt。Sprintf等。字符串函数(Sprintf等)返回一个字符串而不是填充提供的缓冲区。
  您不需要提供格式字符串。对于每一个Printf,Fprintf和Sprintf有另一种双功能,如Print和Println。这些函数不采用格式字符串,而是为每个参数生成默认格式。这些Println版本还在参数之间插入一个空格并在输出中附加一个换行符,而这些Print版本仅在两边的操作数都是字符串时才添加空格。在这个例子中,每一行产生相同的输出。fmt。Printf(Hellod,23)fmt。Fprint(os。Stdout,Hello,23,)fmt。Println(Hello,23)fmt。Println(fmt。Sprint(Hello,23))
  格式化打印函数fmt。Fprint和友元将任何实现io。Writer接口的对象作为第一个参数;变量os。Stdout和os。Stderr是熟悉的实例。
  在这里,事情开始与C不同。首先,数字格式(例如d不带符号或大小的标志);相反,打印例程使用参数的类型来决定这些属性。varxuint641641fmt。Printf(dx,x,x,int64(x),int64(x))
  打印1844674407370955161511
  如果你只想要默认的转换,比如整数的十进制,你可以使用笼统的格式v(对于值);结果是什么Print,并Println会产生。此外,该格式可以打印任何值,甚至是数组、切片、结构和映射。这是上一节中定义的时区地图的打印语句。fmt。Printf(v,timeZone)orjustfmt。Println(timeZone)
  这给出了输出:map〔CST:21600EST:18000MST:25200PST:28800UTC:0〕
  对于地图,Printf朋友按字典顺序对输出进行排序。
  打印结构体时,修改后的格式会v用名称注释结构体的字段,对于任何值,替代格式会v以完整的Go语法打印值。typeTstruct{aintbfloat64cstring}t:T{7,2。35,abcdef}fmt。Printf(v,t)fmt。Printf(v,t)fmt。Printf(v,t)fmt。Printf(v,timeZone)
  打印{72。35abcdef}{a:7b:2。35c:abcdef}main。T{a:7,b:2。35,c:abcdef}map〔string〕int{CST:21600,EST:18000,MST:25200,PST:28800,UTC:0}
  (注意符号。)q当应用于类型为string或的值时,也可以使用带引号的字符串格式〔〕byte。q如果可能,替代格式将使用反引号代替。(该q格式也适用于整数和符文,生成单引号符文常量。)此外,x适用于字符串、字节数组和字节切片以及整数,生成一个长的十六进制字符串,并在格式中使用空格(x)它在字节之间放置空格。
  另一种方便的格式是T,它打印值的类型。fmt。Printf(T,timeZone)
  打印map〔string〕int
  如果要控制自定义类型的默认格式,只需定义一个带有String()string类型签名的方法。对于我们的简单类型T,可能看起来像这样。func(tT)String()string{returnfmt。Sprintf(dgq,t。a,t。b,t。c)}fmt。Printf(v,t)
  按格式打印72。35abcdef
  (如果您需要打印类型的值T以及指向的指针T,则for的接收器String必须是值类型;此示例使用指针,因为这对于结构类型更有效和更惯用。请参阅下面关于指针与值接收器的部分以了解更多信息。)
  我们的String方法能够调用,Sprintf因为打印例程是完全可重入的并且可以用这种方式包装。然而,关于这种方法有一个重要的细节需要理解:不要String通过调用Sprintf的方式来构造一个方法,这种方式会String无限期地重复出现在你的方法中。如果Sprintf调用尝试将接收器直接打印为字符串,则可能会发生这种情况,而后者又会再次调用该方法。如本示例所示,这是一个常见且容易犯的错误。typeMyStringstringfunc(mMyString)String()string{returnfmt。Sprintf(MyStrings,m)Error:willrecurforever。}
  修复也很容易:将参数转换为基本字符串类型,它没有方法。typeMyStringstringfunc(mMyString)String()string{returnfmt。Sprintf(MyStrings,string(m))OK:noteconversion。}
  在初始化部分,我们将看到另一种避免这种递归的技术。
  另一种打印技术是将打印例程的参数直接传递给另一个这样的例程。的签名Printf使用。。。interface{}其最终参数的类型来指定任意数量的参数(任意类型)可以出现在格式之后。funcPrintf(formatstring,v。。。interface{})(nint,errerror){
  在函数内Printf,v就像一个类型的变量,〔〕interface{}但如果它被传递给另一个可变参数函数,它就像一个常规的参数列表。这是log。Println我们上面使用的函数的实现。它将其参数直接传递fmt。Sprintln给实际格式。Printlnprintstothestandardloggerinthemanneroffmt。Println。funcPrintln(v。。。interface{}){std。Output(2,fmt。Sprintln(v。。。))Outputtakesparameters(int,string)}
  我们在嵌套调用中写。。。after告诉编译器将其视为参数列表;否则它只会作为单个切片参数传递。vSprintlnvv
  打印的内容比我们在这里介绍的还要多。有关详细信息,请参阅godoc包的文档fmt。
  顺便说一句,。。。参数可以是特定类型的,例如。。。int对于选择整数列表中最小者的min函数:funcMin(a。。。int)int{min:int(uint(0)1)largestintfor,i:rangea{ifimin{mini}}returnmin}
  追加
  现在我们有了解释append内置函数设计所需的缺失部分。的签名append与我们Append上面的自定义函数不同。示意图是这样的:funcappend(slice〔〕T,elements。。。T)〔〕T
  其中T是任何给定类型的占位符。您实际上无法在Go中编写类型T由调用者确定的函数。这append就是内置的原因:它需要编译器的支持。
  什么append是将元素附加到切片的末尾并返回结果。结果需要返回,因为与我们的手写一样Append,底层数组可能会改变。这个简单的例子x:〔〕int{1,2,3}x追加(x,4,5,6)fmt。Println(x)
  打印〔123456〕。所以append有点像Printf,收集任意数量的参数。
  但是如果我们想做我们Append所做的并将切片附加到切片怎么办?简单:。。。在调用站点使用,就像我们在Output上面调用中所做的那样。这个片段产生与上面相同的输出。x:〔〕int{1,2,3}xappend(x,4,5,6)fmt。Println(x)
  没有那个。。。,它就不会编译,因为类型是错误的;y不是类型int。初始化
  虽然从表面上看它与C或C中的初始化没有太大区别,但Go中的初始化功能更强大。可以在初始化期间构建复杂的结构,并且可以正确处理初始化对象之间,甚至不同包之间的排序问题。
  常数
  Go中的常量就是常量。它们是在编译时创建的,即使在函数中定义为局部变量,并且只能是数字、字符(符文)、字符串或布尔值。由于编译时限制,定义它们的表达式必须是可由编译器计算的常量表达式。例如,13是一个常量表达式,而math。Sin(math。Pi4)不是因为函数调用math。Sin需要在运行时发生。
  在Go中,枚举常量是使用iota枚举器创建的。由于iota可以是表达式的一部分并且表达式可以隐式重复,因此很容易构建复杂的值集。typeByteSizefloat64const(iotaignorefirstvaluebyassigningtoblankidentifierKBByteSize1(10iota)MBGBTBPBEBZBYB)
  将方法附加String到任何用户定义的类型的能力使得任意值可以自动格式化以进行打印。尽管您会看到它最常应用于结构,但此技术对于标量类型也很有用,例如像ByteSize。func(bByteSize)String()string{switch{casebYB:returnfmt。Sprintf(。2fYB,bYB)casebZB:returnfmt。Sprintf(。2fZB,bZB)casebEB:returnfmt。Sprintf(。2fEB,bEB)casebPB:returnfmt。Sprintf(。2fPB,bPB)casebTB:returnfmt。Sprintf(。2fTB,bTB)casebGB:returnfmt。Sprintf(。2fGB,bGB)casebMB:returnfmt。Sprintf(。2fMB,bMB)casebKB:returnfmt。Sprintf(。2fKB,bKB)}returnfmt。Sprintf(。2fB,b)}
  表达式YB打印为1。00YB,而ByteSize(1e13)打印为9。09TB。
  这里使用Sprintf来实现ByteSize的String方法是安全的(避免无限重复)不是因为转换而是因为它调用Sprintfwithf,它不是字符串格式:Sprintf只会在String需要字符串时调用该方法,并且f需要浮动点值。
  变量
  变量可以像常量一样初始化,但初始化器可以是在运行时计算的通用表达式。var(homeos。Getenv(HOME)useros。Getenv(USER)gopathos。Getenv(GOPATH))
  初始化函数
  最后,每个源文件都可以定义自己的niladicinit函数来设置所需的任何状态。(实际上每个文件可以有多个init函数。)finally的意思是finally:init在包中的所有变量声明都评估了它们的初始值设定项之后调用,并且只有在所有导入的包都已初始化之后才评估它们。
  除了不能表示为声明的初始化之外,init函数的一个常见用途是在真正执行开始之前验证或修复程序状态的正确性。funcinit(){ifuser{log。Fatal(USERnotset)}ifhome{homehomeuser}ifgopath{gopathhomego}gopathmaybeoverriddenbygopathflagoncommandline。flag。StringVar(gopath,gopath,gopath,overridedefaultGOPATH)}方法
  指针与值
  正如我们在中看到的ByteSize,可以为任何命名类型定义方法(指针或接口除外);接收者不必是一个结构体。
  在上面对切片的讨论中,我们编写了一个Append函数。我们可以将其定义为切片上的方法。为此,我们首先声明一个可以绑定方法的命名类型,然后使方法的接收器成为该类型的值。typeByteSlice〔〕bytefunc(sliceByteSlice)Append(data〔〕byte)〔〕byte{BodyexactlythesameastheAppendfunctiondefinedabove。}
  这仍然需要返回更新切片的方法。我们可以通过重新定义方法采取消除笨拙指针到ByteSlice它的接收器,因此该方法可以覆盖调用者的切片。func(pByteSlice)Append(data〔〕byte){slice:pBodyasabove,withoutthereturn。pslice}
  事实上,我们还可以做得更好。如果我们修改我们的函数,让它看起来像一个标准的Write方法,像这样,func(pByteSlice)Write(data〔〕byte)(nint,errerror){slice:pAgainasabove。pslicereturnlen(data),nil}
  那么类型ByteSlice满足标准接口io。Writer,就方便了。例如,我们可以打印成一个。varbByteSlicefmt。Fprintf(b,Thishourhasddays,7)
  我们传递a的地址,ByteSlice因为只ByteSlice满足io。Writer。关于接收者的指针与值的规则是值方法可以在指针和值上调用,但指针方法只能在指针上调用。
  出现这个规则是因为指针方法可以修改接收者;在一个值上调用它们将导致该方法接收该值的副本,因此任何修改都将被丢弃。因此,该语言不允许这种错误。但是,有一个方便的例外。当值可寻址时,该语言会通过自动插入地址运算符来处理对值调用指针方法的常见情况。在我们的例子中,变量b是可寻址的,所以我们可以Write只用b。Write。编译器会(b)。Write为我们重写它。
  顺便说一句,在Write字节切片上使用的想法是bytes。Buffer。接口和其他类型
  接口
  Go中的接口提供了一种指定对象行为的方法:如果某些东西可以做到这一点,那么它就可以在这里使用。我们已经看到了几个简单的例子;自定义打印机可以通过String方法实现,同时Fprintf可以通过方法生成任何内容的输出Write。只有一个或两个方法的接口在Go代码中很常见,并且通常被赋予一个从方法派生的名称,例如io。Writer实现Write。
  一个类型可以实现多个接口。例如,一个集合可以通过在包中的例程进行排序sort,如果它实现了sort。Interface,其中包含Len(),Less(i,jint)bool以及Swap(i,jint),它也可以有一个自定义的格式。在这个人为的例子中,Sequence两者都满足。typeSequence〔〕intMethodsrequiredbysort。Interface。func(sSequence)Len()int{returnlen(s)}func(sSequence)Less(i,jint)bool{returns〔i〕s〔j〕}func(sSequence)Swap(i,jint){s〔i〕,s〔j〕s〔j〕,s〔i〕}CopyreturnsacopyoftheSequence。func(sSequence)Copy()Sequence{copy:make(Sequence,0,len(s))returnappend(copy,s。。。)}Methodforprintingsortstheelementsbeforeprinting。func(sSequence)String()string{ss。Copy()Mdontoverwriteargument。sort。Sort(s)str:〔fori,elem:ranges{LoopisO(N);willfixthatinnextexample。ifi0{str}strfmt。Sprint(elem)}returnstr〕}
  转化次数
  的String方法Sequence是重新创建Sprint已经为切片所做的工作。(它也有复杂度O(N),这很差。)如果我们在调用之前将转换Sequence为普通的〔〕int,我们可以分担(并加快速度)Sprint。func(sSequence)String()string{ss。Copy()sort。Sort(s)returnfmt。Sprint(〔〕int(s))}
  此方法是Sprintf从String方法安全调用的转换技术的另一个示例。因为如果我们忽略类型名称,这两种类型(Sequence和〔〕int)是相同的,所以在它们之间进行转换是合法的。转换不会创建新值,它只是暂时充当现有值好像具有新类型一样。(还有其他合法的转换,例如从整数到浮点数,确实会创建一个新值。)
  Go程序中的一个习惯用法是转换表达式的类型以访问不同的方法集。例如,我们可以使用现有类型sort。IntSlice将整个示例简化为:,我们不再Sequence实现多个接口(排序和打印),而是使用将数据项转换为多种类型(Sequence,sort。IntSlice和〔〕int)的能力,每种类型都完成部分工作。这在实践中更不寻常,但可能有效。typeSequence〔〕intMethodforprintingsortstheelementsbeforeprintingfunc(sSequence)String()string{ss。Copy()sort。IntSlice(s)。Sort()returnfmt。Sprint(〔〕int(s))}
  接口转换和类型断言
  类型开关是一种转换形式:它们采用一个接口,对于开关中的每个case,在某种意义上将其转换为那个case的类型。下面是代码如何fmt。Printf使用类型开关将值转换为字符串的简化版本。如果它已经是一个字符串,我们想要接口保存的实际字符串值,而如果它有一个String方法,我们想要调用该方法的结果。typeStringerinterface{String()string}varvalueinterface{}Valueprovidedbycaller。switchstr:value。(type){casestring:returnstrcaseStringer:returnstr。String()}
  第一种情况找到了一个具体的值;第二个将接口转换为另一个接口。以这种方式混合类型非常好。
  如果我们只关心一种类型怎么办?如果我们知道该值包含astring而我们只想提取它?onecase类型switch可以,但类型assertion也可以。类型断言采用接口值并从中提取指定显式类型的值。语法借用了打开类型开关的子句,但使用显式类型而不是type关键字:value。(typeName)
  结果是一个静态类型的新值typeName。该类型必须是接口持有的具体类型,或者值可以转换为的第二个接口类型。要提取我们知道在值中的字符串,我们可以这样写:str:value。(string)
  但如果结果证明该值不包含字符串,则程序将因运行时错误而崩溃。为了防止出现这种情况,请使用逗号,好的习语来安全地测试该值是否为字符串:str,ok:value。(string)ifok{fmt。Printf(stringvalueis:q,str)}else{fmt。Printf(valueisnotastring)}
  如果类型断言失败,str它将仍然存在并且是字符串类型,但它将具有零值,一个空字符串。
  作为功能的说明,这里有一个ifelse语句,它等效于打开此部分的类型开关。ifstr,ok:value。(string);ok{returnstr}elseifstr,ok:value。(Stringer);ok{returnstr。String()}
  概论
  如果一个类型只是为了实现一个接口而存在,并且永远不会在该接口之外导出方法,则不需要导出该类型本身。仅导出接口可以清楚地表明该值除了接口中描述的之外没有其他有趣的行为。它还避免了对通用方法的每个实例重复文档的需要。
  在这种情况下,构造函数应该返回一个接口值而不是实现类型。例如,在哈希库中,crc32。NewIEEE和都adler32。New返回接口类型hash。Hash32。在Go程序中用CRC32算法代替Adler32只需要改变构造函数调用;其余代码不受算法变化的影响。
  类似的方法允许将各种crypto包中的流密码算法与它们链接在一起的分组密码分开。包中的Block接口cryptocipher指定块密码的行为,它提供单个数据块的加密。然后,通过与bufio包的类比,实现该接口的密码包可用于构造流密码,由该Stream接口表示,而无需了解块加密的细节。
  该cryptocipher接口是这样的:typeBlockinterface{BlockSize()intEncrypt(dst,src〔〕byte)Decrypt(dst,src〔〕byte)}typeStreaminterface{XORKeyStream(dst,src〔〕byte)}
  这是计数器模式(CTR)流的定义,它将块密码转换为流密码;请注意,块密码的详细信息被抽象掉了:NewCTRreturnsaStreamthatencryptsdecryptsusingthegivenBlockincountermode。ThelengthofivmustbethesameastheBlocksblocksize。funcNewCTR(blockBlock,iv〔〕byte)Stream
  NewCTR不仅适用于一种特定的加密算法和数据源,还适用于Block接口的任何实现和任何Stream。因为它们返回接口值,所以用其他加密模式替换CTR加密是一种本地化的变化。必须编辑构造函数调用,但由于周围的代码必须仅将结果视为Stream,因此不会注意到差异。
  接口和方法
  由于几乎任何东西都可以附加方法,因此几乎任何东西都可以满足接口。一个说明性的例子是在http包中,它定义了Handler接口。任何实现的对象Handler都可以为HTTP请求提供服务。typeHandlerinterface{ServeHTTP(ResponseWriter,Request)}
  ResponseWriter本身是一个接口,它提供对将响应返回给客户端所需的方法的访问。这些方法包括标准Write方法,因此http。ResponseWriter可以在任何可以使用的地方使用anio。Writer。Request是一个包含来自客户端的请求的解析表示的结构。
  为简洁起见,让我们忽略POST并假设HTTP请求总是GET;这种简化不会影响处理程序的设置方式。这是一个处理程序的简单实现,用于计算页面被访问的次数。Simplecounterserver。typeCounterstruct{nint}func(ctrCounter)ServeHTTP(whttp。ResponseWriter,reqhttp。Request){ctr。nfmt。Fprintf(w,counterd,ctr。n)}
  (与我们的主题保持一致,注意如何Fprintf打印到http。ResponseWriter。)在真实服务器中,访问ctr。n需要防止并发访问。有关建议,请参阅sync和atomic包。
  作为参考,这里是如何将这样的服务器附加到URL树上的节点。importnethttp。。。ctr:new(Counter)http。Handle(counter,ctr)
  但是为什么要创建Counter一个结构体呢?一个整数就是所需要的。(接收者需要是一个指针,以便调用者可以看到增量。)Simplercounterserver。typeCounterintfunc(ctrCounter)ServeHTTP(whttp。ResponseWriter,reqhttp。Request){ctrfmt。Fprintf(w,counterd,ctr)}
  如果您的程序有一些需要通知页面已被访问的内部状态怎么办?将频道绑定到网页。Achannelthatsendsanotificationoneachvisit。(Probablywantthechanneltobebuffered。)typeChanchanhttp。Requestfunc(chChan)ServeHTTP(whttp。ResponseWriter,reqhttp。Request){chreqfmt。Fprint(w,notificationsent)}
  最后,假设我们想显示args调用服务器二进制文件时使用的参数。编写一个函数来打印参数很容易。funcArgServer(){fmt。Println(os。Args)}
  我们如何将其转换为HTTP服务器?我们可以创建ArgServer一个我们忽略其值的某种类型的方法,但有一种更简洁的方法。由于我们可以为除指针和接口之外的任何类型定义方法,因此我们可以为函数编写方法。该http包包含以下代码:TheHandlerFunctypeisanadaptertoallowtheuseofordinaryfunctionsasHTTPhandlers。Iffisafunctionwiththeappropriatesignature,HandlerFunc(f)isaHandlerobjectthatcallsf。typeHandlerFuncfunc(ResponseWriter,Request)ServeHTTPcallsf(w,req)。func(fHandlerFunc)ServeHTTP(wResponseWriter,reqRequest){f(w,req)}
  HandlerFunc是带有方法的类型ServeHTTP,因此该类型的值可以为HTTP请求提供服务。看方法的实现:接收者是一个函数,f,方法调用f。这可能看起来很奇怪,但它与接收者是一个通道和在通道上发送的方法并没有什么不同。
  为了做成ArgServer一个HTTP服务器,我们首先修改它以获得正确的签名。Argumentserver。funcArgServer(whttp。ResponseWriter,reqhttp。Request){fmt。Fprintln(w,os。Args)}
  ArgServer现在有相同的签名HandlerFunc,因此它可以被转换成该类型来访问它的方法,就像我们转换Sequence到IntSlice访问IntSlice。Sort。设置它的代码很简洁:http。Handle(args,http。HandlerFunc(ArgServer))
  当有人访问该页面时args,安装在该页面上的处理程序具有值ArgServer和类型HandlerFunc。HTTP服务器将调用该ServeHTTP类型的方法,ArgServer作为接收方,接收方将依次调用ArgServer(通过f(w,req)内部调用HandlerFunc。ServeHTTP)。然后将显示参数。
  在本节中,我们从一个结构体、一个整数、一个通道和一个函数创建了一个HTTP服务器,所有这一切都是因为接口只是一组方法,可以为(几乎)任何类型定义。空白标识符
  我们已经在forrangeloops和maps上下文中多次提到了空白标识符。可以使用任何类型的任何值分配或声明空白标识符,并无害地丢弃该值。这有点像写入Unixdevnull文件:它代表一个只写值,用作占位符,其中需要变量但实际值无关紧要。它的用途超出了我们已经见过的用途。
  多重赋值中的空白标识符
  在forrange循环中使用空白标识符是一般情况的特例:多重赋值。
  如果赋值需要左侧的多个值,但其中一个值不会被程序使用,则赋值左侧的空白标识符可避免创建虚拟变量的需要,并明确表示该值将被丢弃。例如,当调用一个函数返回一个值和一个错误,但只有错误是重要的时,使用空白标识符来丢弃不相关的值。if,err:os。Stat(path);os。IsNotExist(err){fmt。Printf(sdoesnotexist,path)}
  有时,您会看到为了忽略错误而丢弃错误值的代码;这是可怕的做法。始终检查错误返回;提供它们是有原因的。Bad!Thiscodewillcrashifpathdoesnotexist。fi,:os。Stat(path)iffi。IsDir(){fmt。Printf(sisadirectory,path)}
  未使用的导入和变量
  导入包或声明变量而不使用它是错误的。未使用的导入会使程序膨胀并且编译速度变慢,而初始化但未使用的变量至少是一种浪费的计算,并且可能表明存在更大的错误。然而,当程序处于积极开发状态时,经常会出现未使用的导入和变量,删除它们只是为了让编译继续进行,只是为了以后再次需要它们会很烦人。空白标识符提供了一种解决方法。
  这个写了一半的程序有两个未使用的导入(fmt和io)和一个未使用的变量(fd),所以它不会编译,但最好看看到目前为止的代码是否正确。packagemainimport(fmtiologos)funcmain(){fd,err:os。Open(test。go)iferr!nil{log。Fatal(err)}TODO:usefd。}
  要消除对未使用的导入的抱怨,请使用空白标识符来引用导入包中的符号。类似地,将未使用的变量分配给fd空白标识符将使未使用的变量错误消失。该版本的程序确实可以编译。packagemainimport(fmtiologos)varfmt。PrintfFdeletewhendone。vario。ReaderFdeletewhendone。funcmain(){fd,err:os。Open(test。go)iferr!nil{log。Fatal(err)}TODO:usefd。fd}
  按照惯例,用于消除导入错误的全局声明应该在导入之后立即出现并进行注释,以便于找到它们并提醒以后进行清理。
  导入副作用
  最终应使用或删除类似fmt或io在前面的示例中未使用的导入:空白分配将代码标识为正在进行的工作。但有时导入一个包只是为了它的副作用是有用的,而没有任何明确的使用。例如,在其init功能期间,nethttppprof包注册提供调试信息的HTTP处理程序。它有一个导出的API,但大多数客户端只需要处理程序注册并通过网页访问数据。要仅为其副作用导入包,请将包重命名为空白标识符:importnethttppprof
  这种导入形式清楚地表明该包是为了它的副作用而被导入的,因为该包没有其他可能的用途:在这个文件中,它没有名称。(如果是这样,而且我们没有使用该名称,编译器将拒绝该程序。)
  接口检查
  正如我们在上面对接口的讨论中看到的,一个类型不需要明确声明它实现了一个接口。相反,类型仅通过实现接口的方法来实现接口。实际上,大多数接口转换都是静态的,因此在编译时进行检查。例如,将an传递os。File给期望an的函数io。Reader将不会编译,除非os。File实现该io。Reader接口。
  但是,某些接口检查确实在运行时发生。一个实例在encodingjson包中,它定义了一个Marshaler接口。当JSON编码器接收到实现该接口的值时,编码器调用该值的封送处理方法将其转换为JSON,而不是执行标准转换。编码器在运行时使用如下类型断言检查此属性:m,ok:val。(json。Marshaler)
  如果只需要询问类型是否实现了接口,而不实际使用接口本身,也许作为错误检查的一部分,请使用空白标识符来忽略类型断言的值:if,ok:val。(json。Marshaler);ok{fmt。Printf(valuevoftypeTimplementsjson。Marshaler,val,val)}
  出现这种情况的一个地方是当需要在实现类型的包中保证它实际满足接口时。如果一个类型例如json。RawMessage需要一个自定义的JSON表示,它应该实现json。Marshaler,但没有静态转换会导致编译器自动验证这一点。如果类型无意中未能满足接口,JSON编码器仍然可以工作,但不会使用自定义实现。为了保证实现的正确性,可以在包中使用使用空白标识符的全局声明:varjson。Marshaler(RawMessage)(nil)
  在这个声明中,涉及的转换分配RawMessage到Marshaler需要RawMessage工具Marshaler,并且该属性将在编译时进行检查。如果json。Marshaler接口发生变化,这个包将不再编译,我们会注意到它需要更新。
  此构造中出现的空白标识符表明该声明仅用于类型检查,而不是创建变量。但是,不要对满足接口的每种类型都这样做。按照惯例,只有在代码中不存在静态转换时才使用此类声明,这种情况很少见。嵌入
  Go没有提供典型的、类型驱动的子类化概念,但它确实有能力通过在结构或接口中嵌入类型来借用实现的片段。
  界面嵌入非常简单。我们之前提到过io。Reader和io。Writer接口;这是他们的定义。typeReaderinterface{Read(p〔〕byte)(nint,errerror)}typeWriterinterface{Write(p〔〕byte)(nint,errerror)}
  该io包还导出了几个其他接口,这些接口指定了可以实现多个此类方法的对象。例如,有io。ReadWriter一个包含Read和的接口Write。我们可以io。ReadWriter通过显式列出这两个方法来指定,但是嵌入这两个接口以形成新的接口更容易,更令人回味,如下所示:ReadWriteristheinterfacethatcombinestheReaderandWriterinterfaces。typeReadWriterinterface{ReaderWriter}
  这就是它的样子:AReadWriter可以做aReader所做的和aWriter所做的;它是嵌入式接口的联合。只有接口可以嵌入到接口中。
  同样的基本思想适用于结构,但具有更深远的影响。所述bufio封装具有两个结构类型,bufio。Reader并且bufio。Writer,其中每个过程器具从包的类似接口的io。并且bufio还实现了一个缓冲的读取器写入器,它通过使用嵌入将读取器和写入器组合到一个结构中来实现:它列出了结构中的类型但不给它们字段名称。ReadWriterstorespointerstoaReaderandaWriter。Itimplementsio。ReadWriter。typeReadWriterstruct{Readerbufio。ReaderWriterbufio。Writer}
  嵌入的元素是指向结构的指针,当然在使用之前必须初始化为指向有效的结构。该ReadWriter结构可以写成typeReadWriterstruct{readerReaderwriterWriter}
  但是为了提升字段的方法并满足io接口,我们还需要提供转发方法,如下所示:func(rwReadWriter)Read(p〔〕byte)(nint,errerror){returnrw。reader。Read(p)}
  通过直接嵌入结构,我们避免了这种簿记。嵌入类型的方法是免费出现的,这意味着bufio。ReadWriter不仅有bufio。Readerand的方法,而且bufio。Writer满足所有三个接口:io。Reader,io。Writer,和io。ReadWriter。
  嵌入与子类化有一个重要的区别。当我们嵌入一个类型时,该类型的方法成为外部类型的方法,但是当它们被调用时,方法的接收者是内部类型,而不是外部类型。在我们的例子中,当a的Read方法bufio。ReadWriter被调用时,和上面写的转发方法的效果完全一样;接收者是的reader字段,而ReadWriter不是ReadWriter本身。
  嵌入也可以是一种简单的便利。此示例显示了一个嵌入的字段,旁边是一个常规的命名字段。typeJobstruct{Commandstringlog。Logger}
  该Job类型现在有Print,Printf,Println和其他方法log。Logger。Logger当然,我们可以给a字段名称,但没有必要这样做。现在,一旦初始化,我们就可以登录到Job:job。Println(startingnow。。。)
  该Logger是有规律场Job结构,所以我们可以用通常的方法进行初始化的构造函数中进行Job,这样,funcNewJob(commandstring,loggerlog。Logger)Job{returnJob{command,logger}}
  或使用复合文字,job:Job{command,log。New(os。Stderr,Job:,log。Ldate)}
  如果我们需要直接引用一个嵌入的字段,忽略包限定符的字段的类型名称作为字段名称,就像在Read我们的ReadWriter结构体的方法中一样。在这里,如果我们需要访问log。Logger一个的Job变量job,我们会写job。Logger,如果我们想要改进的方法,这将是有益的Logger。func(jobJob)Printf(formatstring,args。。。interface{}){job。Logger。Printf(q:s,job。Command,fmt。Sprintf(format,args。。。))}
  嵌入类型引入了名称冲突的问题,但解决它们的规则很简单。首先,字段或方法在类型的更深层嵌套部分X隐藏任何其他项目X。如果log。Logger包含名为的字段或方法Command,则的Command字段Job将支配它。
  其次,如果同名出现在同一个嵌套层次,通常是错误的;log。Logger如果Job结构包含另一个名为的字段或方法,则嵌入将是错误的Logger。但是,如果在类型定义之外的程序中从未提到过重名,那也没关系。此限定提供了一些防止对从外部嵌入的类型进行更改的保护;如果添加的字段与另一个子类型中的另一个字段冲突,如果这两个字段都没有使用过,则没有问题。并发
  通过交流分享
  并发编程是一个很大的话题,这里只讨论一些Go特定的亮点。
  由于实现对共享变量的正确访问所需的微妙之处,许多环境中的并发编程变得困难。Go鼓励一种不同的方法,在这种方法中,共享值在通道上传递,实际上,从不由单独的执行线程主动共享。在任何给定时间,只有一个goroutine可以访问该值。按照设计,不会发生数据竞争。为了鼓励这种思维方式,我们将其简化为一个口号:
  不要通过共享内存进行通信;相反,通过通信共享内存。
  这种方法可能太过分了。例如,最好通过在整数变量周围放置互斥锁来完成引用计数。但作为一种高级方法,使用通道来控制访问可以更轻松地编写清晰、正确的程序。
  考虑此模型的一种方法是考虑在一个CPU上运行的典型单线程程序。它不需要同步原语。现在运行另一个这样的实例;它也不需要同步。现在让这两个人交流;如果通信是同步器,则仍然不需要其他同步。例如,Unix管道就非常适合这个模型。尽管Go的并发方法起源于Hoare的CommunicatingSequentialProcesses(CSP),但它也可以被视为Unix管道的类型安全泛化。
  协程
  它们被称为goroutines是因为现有的术语线程、协程、进程等传达了不准确的内涵。goroutine有一个简单的模型:它是一个与同一地址空间中的其他goroutine并发执行的函数。它是轻量级的,成本比分配堆栈空间多一点。并且堆栈开始时很小,因此它们很便宜,并且可以通过根据需要分配(和释放)堆存储来增长。
  Goroutines被多路复用到多个OS线程上,所以如果一个应该阻塞,例如在等待IO时,其他人继续运行。他们的设计隐藏了线程创建和管理的许多复杂性。
  使用go关键字为函数或方法调用添加前缀以在新的goroutine中运行调用。当调用完成时,goroutine静默退出。(效果类似于Unixshell的在后台运行命令的符号。)golist。Sort()同时运行list。S不要等它。
  函数文字在goroutine调用中很方便。funcAnnounce(messagestring,delaytime。Duration){gofunc(){time。Sleep(delay)fmt。Println(message)}()Notetheparenthesesmustcallthefunction。}
  在Go中,函数文字是闭包:实现确保函数引用的变量只要它们处于活动状态就可以存活。
  这些示例不太实用,因为这些函数无法发出完成信号。为此,我们需要渠道。
  Channel
  与映射一样,通道分配有make,结果值充当对底层数据结构的引用。如果提供了一个可选的整数参数,它会设置通道的缓冲区大小。对于无缓冲或同步通道,默认值为零。ci:make(chanint)无缓冲的整数通道cj:make(chanint,0)无缓冲的整数通道cs:make(chanos。File,100)指向文件的指针的缓冲通道
  无缓冲通道将通信(值的交换)与同步相结合,确保两个计算(goroutine)处于已知状态。
  有很多使用频道的好习语。这是让我们开始的一个。在上一节中,我们在后台启动了排序。通道可以允许启动goroutine等待排序完成。c:make(chanint)Allocateachannel。Swhenitcompletes,signalonthechannel。gofunc(){list。Sort()c1Svaluedoesnotmatter。}()doSomethingForAWhile()cWdiscardsentvalue。
  接收器总是阻塞直到有数据要接收。如果通道未缓冲,则发送方会阻塞,直到接收方收到该值。如果通道有缓冲区,则发送方只会阻塞,直到值被复制到缓冲区;如果缓冲区已满,这意味着等待某个接收器检索到一个值。
  缓冲通道可以像信号量一样使用,例如限制吞吐量。在这个例子中,传入的请求被传递到handle,它向通道发送一个值,处理请求,然后从通道接收一个值,为下一个消费者准备信号量。通道缓冲区的容量将同时调用的数量限制为process。varsemmake(chanint,MaxOutstanding)funchandle(rRequest){sem1Waitforactivequeuetodrain。process(r)Maytakealongtime。semDenablenextrequesttorun。}funcServe(queuechanRequest){for{req:queuegohandle(req)Dontwaitforhandletofinish。}}
  一旦MaxOutstanding处理程序正在执行process,任何更多将阻止尝试发送到已填充的通道缓冲区,直到现有处理程序之一完成并从缓冲区接收。
  但是,这种设计有一个问题:Serve为每个传入的请求创建一个新的goroutine,即使只有MaxOutstanding它们中的一个可以在任何时候运行。因此,如果请求来得太快,程序可能会消耗无限资源。我们可以通过改变Servegoroutine的创建来解决这个缺陷。这是一个明显的解决方案,但请注意它有一个错误,我们将在随后修复:funcServe(queuechanRequest){forreq:rangequeue{sem1gofunc(){process(req)Bseeexplanationbelow。sem}()}}
  错误在于,在Gofor循环中,每次迭代都会重用循环变量,因此该req变量在所有goroutine之间共享。那不是我们想要的。我们需要确保req每个goroutine都是独一无二的。这是一种方法,将的值req作为参数传递给goroutine中的闭包:funcServe(queuechanRequest){forreq:rangequeue{sem1gofunc(reqRequest){process(req)sem}(req)}}
  将此版本与前一个版本进行比较,以查看闭包声明和运行方式的不同之处。另一种解决方案是创建一个同名的新变量,如下例所示:funcServe(queuechanRequest){forreq:rangequeue{req:reqCreatenewinstanceofreqforthegoroutine。sem1gofunc(){process(req)sem}()}}
  写起来可能有点奇怪req:req
  但在Go中这样做是合法和惯用的。你会得到一个同名变量的新版本,故意在本地隐藏循环变量,但每个goroutine都是唯一的。
  回到编写服务器的一般问题,另一种很好地管理资源的方法是启动固定数量的handlegoroutines,全部从请求通道读取。goroutine的数量限制了同时调用的数量process。此Serve函数还接受一个通道,在该通道上它将被告知退出;启动goroutines后,它会阻止从该通道接收。funchandle(queuechanRequest){forr:rangequeue{process(r)}}funcServe(clientRequestschanRequest,quitchanbool){Starthandlersfori:0;iMaxOi{gohandle(clientRequests)}quitWaittobetoldtoexit。}
  Channelsofchannels
  Go最重要的特性之一是通道是一流的值,可以像任何其他值一样分。。。
投诉 评论

疑袁立小号发文,称50业内人士认为吴亦凡无罪,网友表示不可信在吴亦凡的事件的审判结果出来之后,很多的网友们都觉得大快人心,称恶人就应该得到法律的制裁,同时还有一些网友们嘲讽吴亦凡,说他死鸭子嘴硬但是也没能逃得过法网。可是,就在十一月二十……0经验,没露营过?别担心,小编教你如何安全舒适地露营曾以为露营是荒野求生,背上帐篷、睡袋、自热火锅,去丛林探险;也曾以为露营是公园野餐,腰酸腿疼盘腿坐着,吃外卖的披萨;后来,小编才明白露营是一次浪漫的出逃。像野生动物一般从……学生月考作文以爱为舟一叶轻舟水上飘,外婆与我荡绿潮,共享清新与美好蓝天佳景一起瞧。每每忆起这古老的童谣,无不想起那慈爱的面庞,那温和的语调,那把我捧在手心上的外婆。外婆原来生活在一处江南水乡……KENDALLJENNER泳装饰品一览,棒球帽配比坚尼成为2KENDALLJENNER这个夏天并不无聊,尽管和交往2年的NBA球星男友DEVINBOOKER分手,但生活一点也不閒著。一直为自家龙舌兰酒品牌818作宣传,举行不少户外派对与……浅谈国内有哪些超值的旅游目的地知乎中有很多有趣的话题,就让我们来探索作为一名自媒体博主,我经常会收到读者的咨询,询问哪些国内旅游目的地性价比较高。经过我的调研和亲身体验,以下是我推荐的一些旅游目的地。……一球未进成球王,一个混子骗懵整个巴西,还差点儿混进世界杯在经历了2020欧锦赛2020东京奥运会等各级别体育赛事的延期、取消或是重新开启之后,2022年卡塔尔世界杯可谓承载了全球人类对体育盛会的激情与欢乐。不过在这里,我们讲的既不是……不容易!广州镜面人孕妈顺利产下元旦宝宝新年伊始,广州医科大学附属第三医院迎来了一位元旦宝宝,宝妈小欣(化名)患有卡塔格内综合征,其心、肝、脾的位置与正常人相反,俗称镜面人。卡塔格内综合征目前没有根治的方法。小欣自2……人类2。0奇点临近文罗峰转载请注明作者和来源经济是有周期的,国家的兴衰也自有内在的逻辑,然而人类技术的进步却稳步地往前进行着。我们身处的时代是一个技术加速进步的时代,我们不禁要……做虎皮青椒,切忌先放油,提前多做1步,香脆起虎皮,一点不油腻做虎皮青椒,切忌先放油,提前多做1步,香脆起虎皮,一点不油腻青椒,大家对此一点都不陌生吧,在餐桌上经常见到。我平时不太能吃辣,但是又想吃点下饭、开胃的东西,是不是好多人都……李盈莹手大胳膊长,直到遇到吴梦洁才知道自己的天赋还是差一些李盈莹是中国女排近年来成长起来的优秀攻手。与其他攻手的低调不同,李盈莹一直对自己非常欣赏。当人们夸赞李盈莹有可能做朱婷的接班人的时候,李盈莹说自己不愿意做他人,只愿意做最好的李……南京原始森林青龙山十里长凹文旅探险记4月2日清晨起来就准备出发,带足一天的口粮和矿泉水,还有近一岁的护卫柴犬,手套、头盔、围巾、护目镜,电动越野三轮电量足足的,马力强大。南京原始森林青龙山十里长凹山脚下……Go语言自学系列高效golang开发视频来源:B站《golang入门到项目实战〔2021最新Go语言教程,没有废话,纯干货!持续更新中。。。〕》一边学习一边整理老师的课程内容及试验笔记,并与大家分享,侵权即……
没什么比找到一辆酷车或废弃经典老车更让人兴奋的了没有什么事情比找到一辆酷车、一家不知名车店或废弃经典老车更让人兴奋的了。某个周末我开车途经靜岡时,发现了一处符合以上全部兴奋点的地方!发现此地实属偶然。导航显示返回东京的……健康才是真谛,改善生活方式,提前预防糖尿病糖尿病是由血糖升高导致的,并且是慢性代谢性疾病的一种。糖尿病的发病率已经跃居三大顽疾之最,死亡率也仅次于心脑血管和癌症。为了引起人们对糖尿病的重视,世界卫生组织把每……珠海2企业上榜500强榜单背后,下一个格力电器在哪?角逐工业总产值万亿俱乐部,珠海何时才能诞生下一个格力电器?近日,中国企业联合会、中国企业家协会发布2022中国企业500强榜单,珠海共有2家企业上榜,总量居全省第五。其中……别跟娃的头发过不去,剃光真的是吃力不讨好每位家长都希望生下的孩子头发乌黑发亮,但现实情况并非如此,有的孩子出生时头发就稀少,毛发细细软软,不少家长们都困惑不已,宝宝头发少该怎么办?这时候一定有许多亲戚朋友纷纷出……天使曾经来过!出生仅2天的婴儿捐献双肾孕周29周,体重仅1。07kg小婴儿欢欢(化名)在父母的期盼中到来可她还没来得及看看这个世界就在出生的第二天因误吸匆匆离世但是,她留下了一份珍贵的生命礼物……1500元,看完这几款手机不踩坑,教你怎么选2021年即将结束,为了迎接新的一年的到来,最近有网友私信我想给家人找1。5K左右的手机,可是自己对手机不是很了解,现在市面上这个价位的手机还是挺多的,希望能给一些建议。今天我……中国高层领导密集接見跨国公司高管显示中国开放合作的诚意!中国最近由国务院发展研究中心举办的中国全球发展高峰论坛,邀请了世界500强中非常著名的企业的高管们!这些世界顶级企业的精英包括苹果的首席执行官库克,雀巢的首席执行官施奈德……Z690吹雪主板颜值搭档XPG龙耀DDR5吹雪联名内存评测对于高性能的12代酷睿平台来说,高频高带宽的DDR5内存是绕不开的选项。而在装机的过程中,追求高颜值,又是白色控二次元的玩家,ROGSTRIXZ690AGAMING吹雪D5主板……Android插件化技术,你值得一看相信大家在工作中会遇到关于Android插件化开发出现的问题,随着对这种技术的了解,我发现在插件开发的一些基础问题上下需要花费更多的时间。就比如在主工程Context和插件Co……中国男足屡次让国人失望,国足无法崛起的三个层面原因男足还有希望吗?为何泱泱大国14亿中国人,却选不出11人足球健将?这一切的背后究竟是人不行还是教练不行?还是道德的沦丧和人性的扭曲?今天就从商业层面来看看作为经济军事都能……甘泉县十大旅游景点下寺湾毛泽东旧居下寺湾毛泽东旧居为陕西省文物保护单位、红色旅游经典景区。下寺湾毛泽东旧居位于延安市甘泉县下寺湾镇下寺湾村。1935年10月19日,中共中央率中央红军经二万五千里长征……外媒台积电现在感受到失去华为的痛苦了在美修改了芯片禁令后,曾有记者采访过台积电的CEO张忠谋,关于失去华为这个大客户会对台积电产生哪些影响。当时的台积电态度还是相当强硬的,张忠谋表示,没有影响。不可否认,在华为的……
友情链接:中准网聚热点快百科快传网快生活快软网快好知文好找