我要离开戈朗先生的《狂野旅程》(2020)

2021-01-03 11:12:48

这篇文章的语气将与我去年发布的内容有所不同-这是恰当的说法。我总是很不高兴写这些内容,因为它不可避免地会讨论许多人一直在努力工作的事情。

我已经为该语言投入了数千小时的时间,并用它对我的雇主实施了几个关键的基础架构,我希望自己没有。

如果您已经在Go上投入了大量资金,那么您可能不应该读这本书,它可能只是绞刀而已。如果您使用Go语言,则绝对不应阅读此书。

我一直在相对安静的状态下经历过Go的特质,这是我真正需要摆脱的一些事情。

到现在为止,每个人都知道Go还没有泛型,这使得很多问题无法准确建模(相反,您必须回退到反射,这是非常不安全的,并且API容易出错),错误处理太奇怪了(即使您选择了添加上下文或堆栈跟踪的第三方库),程序包管理也花了一段时间等等。

但是每个人也都知道Go的优势:静态链接使二进制文件易于部署(尽管,即使剥离了DWARF表,Go二进制文件也会变得非常大-堆栈跟踪注释仍然存在,而且成本很高)。

编译时间很短(除非您需要cgo),只有一个交互式的运行时配置程序(pprof)触手可及,它是一个相对跨平台的(甚至有一个很小的嵌入式变体) ),语法突出显示起来很容易,并且现在有一个官方的LSPserver。

或更确切地说,它是一个半真相,可以很方便地掩盖一个事实,当您使简单的事情变复杂时,您会将其转移到其他地方。

计算机,操作系统,网络都是一团糟。即使您知道自己在做什么,也几乎无法管理。十个软件工程师中有九个同意:一切都行得通的奇迹。

实际上,这个示例确实进行了一段时间-但不要让细节分散您的注意力。尽管深入,但它说明了一个更大的观点。

Go的大多数API(非常类似于NodeJS的API)都是为Unix操作系统设计的。这并不奇怪,因为Rob&肯来自9gang计划。

// File表示一个打开的文件描述符。类型File struct {* file //特定于操作系统} func(f * File)Stat()(FileInfo,error){//省略} // FileInfo描述了一个文件,由Stat和Lstat返回。类型FileInfo接口{Name()字符串//文件的基本名称Size()int64 //常规文件的长度(以字节为单位);系统依赖于其他模式Mode()FileMode //文件模式位ModTime()时间。时间//修改时间IsDir()bool // Mode()的缩写。IsDir()Sys()接口{} //基础数据源(可以返回nil)} // FileMode表示文件的模式,并且权限位。 //这些位在所有系统上都具有相同的定义,以便可以将有关文件的信息从一个系统//移植到另一个系统。并非所有位都适用于所有系统。 //唯一需要的位是目录的ModeDir。 type FileMode uint32 //定义的文件模式位是FileMode的最高有效位。 //这9个最低有效位是标准的Unix rwxrwxrwx权限。 //这些位的值应被视为公共API的一部分,并且//可以在有线协议或磁盘表示中使用://尽管可以添加新的位,但不得更改它们。 const(//单个字母是String方法格式使用的缩写。ModeDir FileMode = 1<<(32-1-iota)// // d:是目录ModeAppend // a:仅附加ModeExclusive // l:专用ModeTemporary // T:临时文件;仅计划9 ModeSymlink // L:符号链接ModeDevice // D:设备文件ModeNamedPipe // p:命名管道(FIFO)ModeSocket // S: Unix域套接字ModeSetuid // // u:setuid ModeSetgid // g:setgid ModeCharDevice // c:Unix字符设备,设置了ModeDevice后ModeSticky // t:sticky ModeIrregular //?:非常规文件;对此一无所知file //类型位的掩码。对于常规文件,将不会设置任何值。ModeType= ModeDir | ModeSymlink | ModeNamedPipe | ModeSocket | ModeDevice | ModeCharDevice | ModeIrregular ModePerm FileMode = 0777 // Unix权限位)

每个文件都有一个模式,甚至还有一个命令可以让您将其转储为十六进制:

包主要导入(" fmt"" os")func main(){arg:= os。 Args [1] fi,_:= os。 Stat(arg)fmt。 Printf("(%s)mode =%o \ n&#34 ;, arg,fi。Mode()&os。ModePerm)}

$运行main.go / etc / hosts(/ etc / hosts)模式= 644 $运行main.go / usr / bin / man(/ etc / hosts)模式= 755

在Windows上,文件没有模式。它没有stat,lstat,fstatsyscalls-它具有FindFirstFile系列函数(或者,要打开的CreateFile,然后是GetFileAttributes,或者是GetFileInformationByHandle),该函数采用指向WIN32_FIND_DATA结构的指针,该结构包含文件属性。

// src / os / types_windows.go func(fs * fileStat)Mode()(m FileMode){如果fs ==& devNullStat {返回ModeDevice | ModeCharDevice | 0666}如果fs。文件属性和系统调用。 FILE_ATTRIBUTE_READONLY!= 0 {m | = 0444}否则{m | = 0666}如果fs。 isSymlink(){返回m | ModeSymlink}如果是fs。文件属性和系统调用。 FILE_ATTRIBUTE_DIRECTORY!= 0 {m | = ModeDir | 0111}开关fs。文件类型{case syscall。 FILE_TYPE_PIPE:m | = ModeNamedPipe案例syscall。 FILE_TYPE_CHAR:m | = ModeDevice | ModeCharDevice}返回m}

使用Unix所具有的一切因为最低的公分母在开源代码库中非常普遍,所以这并不奇怪。

让我们再进一步一点。在Unix系统上,您可以更改文件的模式,使其成为只读文件,或翻转可执行位。

包主要导入(" fmt"" os")func main(){arg:= os。 Args [1] fi,err:= os。 stat(arg)must(err)fmt。 Printf("(%s)旧模式=%o \ n&#34 ;, arg,fi.Mode()& os.ModePerm)must(os.Chmod(arg,0755))fi,err = os 。 stat(arg)must(err)fmt。 Printf("(%s)new mode =%o \ n&#34 ;, arg,fi。Mode()& os。ModePerm)} func must(err error){if err!= nil {panic(呃) }}

$ touch test.txt $去运行main.go test.txt(test.txt)旧模式= 644(test.txt)新模式= 755

因此,没有错误。 Chmod只是默默地做...什么。这是合理的-它不等同于可执行位。 Windows上的文件。

// src / syscall / syscall_windows.go func Chmod(路径字符串,模式uint32)(err错误){p,e:= UTF16PtrFromString(path)如果e!= nil {return e} attrs,e:= GetFileAttributes(p)如果e!= nil {return e}如果mode& S_IWRITE!= 0 {attrs& ^ = FILE_ATTRIBUTE_READONLY} else {attrs | = FILE_ATTRIBUTE_READONLY} return SetFileAttributes(p,attrs)}

我们有一个uint32的论点,用42.149.497.295的可能值编码……一点信息。

那是一个很纯真的谎言。从一开始就将文件具有模式的假设引入了API设计,现在,每个人都必须使用它,就像在Node.JS中,可能还有其他许多语言一样。

具有更多类型系统和更好设计的库的语言可以避免这种陷阱。

好吧,我竭尽全力使Rust摆脱了所有这些。除其他外,因为人们会认为这篇文章来自“非典型的甲壳类动物”。

但是,对于本文中提出的所有问题,Rust都是正确的。如果我有另一个很好的例子,我会使用它。但是我不知道,所以去吧。

这个函数签名已经告诉了我们很多。它返回一个结果,这意味着,不仅我们知道这会失败,而且我们必须处理它。通过使用.unwrap()或.expect()来对错误进行恐慌,或者将其与Result :: Ok / Result :: Err进行匹配,或者通过将它冒泡?操作员。

关键是,此函数签名使我们无法访问无效/未初始化/空的元数据。使用Go函数,如果忽略了所产生的错误,您仍然会得到结果-最有可能是空指针。

另外,该参数也不是字符串-它是路径。更确切地说,它可以变成一条路径。

fn main(){让元数据= std :: fs ::元数据(" Cargo.toml")。解开(); println! ("是dir?{:?}&#34 ;, metadata.is_dir()); println! ("是文件吗?{:?}&#34 ;, metadata.is_file());}

但是路径不一定是字符串。在Unix(!)上,路径可以是任何字节序列,但空字节除外。

$ cd rustfun / $ touch" $(printf" \ xbd \ xb2 \ x3d \ xbc \ x20 \ xe2 \ x8c \ x98")" $ llsls:无法比较文件名'Cargo .lock'和'\ 275 \ 262 = \ 274⌘':无效或不完整的多字节或宽字符src目标Cargo.lock Cargo.toml'' $' \ 275 \ 262' ' =' \ 274'' ⌘'

我们刚刚制作了一个文件名很顽皮的文件-但这是一个完全有效的文件,即使ls很难解决。

$ stat" $(printf" \ xbd \ xb2 \ x3d \ xbc \ x20 \ xe2 \ x8c \ x98")"文件:=⌘大小:0块:0 IO块:65536常规空文件设备:8c70d496h / 2356204694d索引节点:72620543991375285链接:1访问权限:(0644 / -rw-r--r--)Uid:(197611 / amos)Gid: (197611 / amos)访问:2020-02-28 13:12:12.783734000 +0100修改:2020-02-28 13:12:12.783734000 +0100更改:2020-02-28 13:12:12.783329400 +0100出生日期:2020-02 -28 13:12:12.783329400 +0100

那不是我们可以用Rust中的String表示的东西,因为RustStrings是有效的utf-8,而事实并非如此。

因此,如果我们使用std :: fs :: read_dir,则列出它并获取其元数据没有问题。

使用std :: fs; fn main(){让条目= fs :: read_dir("。")。解开();用于输入条目{let path = entry。展开()。路径();让meta = fs ::元数据(& path)。解开();如果meta。 is_dir(){println! ("(dir){:?}&#34 ;, path); } else {println! (" {:?}&#34 ;,路径); }}}

包主要导入(" fmt"" os")func main(){arg:= os。 Args [1] f,err:= os。打开(arg)必须(err)条目,err:= f。 _,e:=范围条目{如果e。的Readdir(-1)必须(err)。 IsDir(){fmt。 Printf("(dir)%s \ n&#34 ;,例如Name())}否则{fmt。 Printf("%s \ n&#34 ;,例如Name())}}} func must(err error){if err!= nil {panic(err)}}

看,没有路径。输入Go。只是"字符串"。 Go字符串是justbyte切片,无法保证其中包含什么。

因此它会打印垃圾,而在Rust中,Path不实现Display,因此我们无法做到这一点:

而且,如果我们希望获得更友好的输出,我们可以处理两种情况:路径恰好是有效的utf-8字符串,以及何时路径不正确:

使用std :: fs; fn main(){让条目= fs :: read_dir("。")。解开();用于输入条目{let path = entry。展开()。路径();让meta = fs ::元数据(& path)。解开();让前缀=如果meta。 is_dir(){"(dir)" }其他{" " };匹配路径。 to_str(){某些=> println! (" {} {}&#34 ;,前缀s),无=> println! (" {} {:?}(无效utf-8)&#34 ;,前缀,路径),}}}

$ cargo run --quiet(dir)./src ./Cargo.toml ./.gitignore" ./ \ xBD \ xB2 = \ xBC⌘" (无效的utf-8)(dir)./.git ./Cargo.lock(dir)./target

除非他们不是。路径不是。因此,在Go中,所有路径操作例程都对字符串进行操作,让我们看一下path / filepath包。

软件包filepath实现了实用程序例程,用于以与目标操作系统定义的文件路径兼容的方式来处理文件名路径。

文件路径包使用正斜杠或反斜杠,具体取决于操作系统。若要处理诸如URL之类的路径,无论使用什么操作系统,该路径始终使用正斜杠,请参阅路径包。

func Abs(路径字符串)(字符串,错误)func Base(路径字符串)字符串func Clean(路径字符串)字符串func Dir(路径字符串)字符串func EvalSymlinks(路径字符串)(字符串,错误)func Ext(路径字符串)字符串func FromSlash(路径字符串)字符串func Glob(模式字符串)(匹配[]字符串,错误错误)func HasPrefix(p,前缀字符串)bool func IsAbs(路径字符串)bool func Join(elem ... string)字符串func Match (模式,名称字符串)(匹配的布尔值,错误错误)func Rel(基本路径,targpath字符串)(string,错误)func Split(路径字符串)(dir,文件字符串)func SplitList(路径字符串)[]字符串func ToSlash(路径字符串)字符串func VolumeName(路径字符串)字符串func Walk(根字符串,walkFn WalkFunc)错误

// Ext返回路径使用的文件扩展名。扩展名是后缀// //从path的最后一个元素的最后一个点开始; //如果没有点,则为空。 func Ext(路径字符串)字符串

包主要导入(" fmt"" path / filepath")func main(){输入:= []字符串{" /&#34 ;," / 。&#34 ;、" /。foo&#34 ;、" / foo&#34 ;、" /foo.txt" ;、" /foo.txt/bar&# 34;," C:\\"," C:\\。"," C:\\ foo.txt"," C :\\ foo.txt \\ bar&#34 ;,},用于_,i:=范围输入{fmt。 Printf("%24q =>%q \ n",i,文件路径。Ext(i))}} func must(err error){if err!= nil {panic(err)}}

$ go运行main.go" /" => "" " /.&# 34; => "。" " /。foo" => " .foo" " / foo" => "" " /foo.txt" => " .txt" " /foo.txt/bar" => "" " C:\\" => "" " C:\\。" => "。" " C:\\ foo.txt" => " .txt" " C:\\ foo.txt \\ bar" => " .txt \\ bar"

马上,我正在辩论情绪-.foo的扩展名真的是.foo吗?巴特继续前进。

为什么?因为Go标准库假设平台具有单个路径分隔符-在Unix和BSD之类的平台上,在Windowsit的\\上。

//在`fun.c`中void main(){处理hFile = CreateFile(" C:/Users/amos/test.txt",GENERIC_WRITE,0,NULL,CREATE_NEW,FILE_ATTRIBUTE_NORMAL,NULL); char * data =" Win32 API中的Hello DWORD dwToWrite =(DWORD)strlen(数据); DWORD dwWritten = 0; WriteFile(hFile,data,dwToWrite,& dwWritten,NULL); CloseHandle(hFile);}

> cl fun.c适用于x64的Microsoft(R)C / C ++优化编译器版本19.23.28107版权所有(C)Microsoft Corporation。保留所有权利。fun.c Microsoft(R)增量链接程序版本14.23.28107.0版权所有(C)Microsoft Corporation。保留所有权利。/out:fun.exefun.obj> 。\ fun.exe>从Win32 API输入C:\ Users \ amos \ test.txtHello

// src / os / path_windows.go const(PathSeparator =' \\' //操作系统特定的路径分隔符PathListSeparator =&#39 ;;' //操作系统特定的路径列表分隔符)

$ go运行main.go" /" => "" " /.&# 34; => "。" " /。foo" => " .foo" " / foo" => "" " /foo.txt" => " .txt" " /foo.txt/bar" => "" " C:\\" => "" " C:\\。" => "。" " C:\\ foo.txt" => " .txt" " C:\\ foo.txt \\ bar" => ""

// src / path / filepath / path.go func Ext(路径字符串)字符串{for i:= len(path)-1;我> = 0&& ! os。 IsPathSeparator(path [i]); i-{如果path [i] =='。' {return path [i:]}} return""}

// src / os / path_windows.go // IsPathSeparator报告c是否为目录分隔符。 func IsPathSeparator(c uint8)bool {//注意:Windows接受/作为路径分隔符。返回c ==' \\' || c ==' /'}

(我能否指出“扩展名”被认为足以缩写为“#Ext ##”,但“" IsPathSeparator" t不是那么有趣?”)

///确定字符是否为当前平台允许的//路径分隔符之一。 pub fn is_separator(c:char)->布尔

///当前平台的路径组件的主要分隔符。 /// ///例如,在Unix上为/,在Windows上为\。 pub const MAIN_SEPARATOR:char

命名方式使我们更加清楚可能存在辅助路径分隔符,并且丰富的Path操作API使得查找此类代码的可能性大大降低,例如:

如果是。 PathSeparator ==' /' {projname =字符串。如果是os,请替换(name," \\&#34 ;," /&#34 ;, -1)}否则。 PathSeparator ==' \\' {projname =字符串。 Replace(name," /&#34 ;," \\&#34 ;, -1)}

事实证明,Rust还具有“获取路径”扩展名。功能,但在其做出的承诺中要保守得多:

//提取self.file_name的扩展名(如果可能)。 // //扩展名是:// // * *无,如果没有文件名; // *无,如果没有嵌入式。 // *如果文件名以开头,则为None。并且没有其他.s; // *否则,文件名中final之后的部分。 pub fn扩展名(& self)->选项< & OsStr>

fn main(){让输入= [r" /&#34 ;、 r" /.&# 34 ;、 r" /。foo&#34 ;、 r" / foo" ,r" /foo.txt",r" /foo.txt/bar",r" C:\",r" C:\。&#34 ;,r" C:\ foo.txt&#34 ;, r" C:\ foo.txt \ bar&#34 ;,];用于& inputs中的输入{使用std :: path :: Path; println! (" {:> 20} => {:?}&#34 ;,输入,路径:: new(input).extension()); }}

$货物运行-安静/ =>没有 /。 =>无/.foo =>无/ foo。 => Some("")/ foo =>无/foo.txt =>某些(" txt")/foo.txt/bar =>无C:\ =>无C:\。 =>某些("")C:\ foo.txt =>某些(" txt")C:\ foo.txt \ bar =>一些(" txt \\ bar")

$货物运行-安静/ =>没有 /。 =>无/.foo =>无/ foo。 => Some("")/ foo =>无/foo.txt =>某些(" txt")/foo.txt/bar =>无C:\ =>无C:\。 =>无C:\ foo.txt =>某些(" txt")C:\ foo.txt \ bar =>没有

我们来剖析一下:首先它调用file_name()。这是如何运作的?它是从路径末尾向后搜索路径分隔符的地方吗?

pub fn file_name(& self)->选项< & OsStr> {自我。组件 ( ) 。 next_back()。 and_then(| p |匹配p {组件:: Normal(p)=>一些(p。as_ref()),_ =>无,})}

没有!它调用组件,该组件返回实现DoubleEndedIterator的类型-您可以从正面或背面导航的迭代器。然后,它从背面抓取第一个项目(如果有的话)并将其返回。

迭代器确实以可重复使用的方式懒惰地查找路径分隔符。没有代码重复,例如在Go库中:

// src / os / path_windows.go func dirname(path string)string {vol:= volumeName(path)i:= len(path)-1 for i> = len(vol)&& ! IsPathSeparator(path [i]){i-} dir:= path [len(vol):i +1] last:= len(dir)-如果last> 0&& IsPathSeparator(dir [last]){dir = dir [:last]}如果dir =="" {dir ="。" } return vol + dir}

因此,现在我们只有文件名。如果我们有/foo/bar/baz.txt,现在只处理baz.txt-作为OsStr,而不是utf-8字符串。我们仍然可以拥有随机字节。

pub fn扩展名(& self)->选项< & OsStr> {如果让Some(file_name)= self。 file_name(){let(before,after)= split_fi

......