Golang使用embed打包gin应用
历史是一堆灰烬,但灰烬深处有余温。
文件目录
1 | - static |
代码
1 | package main |
1 |
|
历史是一堆灰烬,但灰烬深处有余温。
1 | - static |
1 | package main |
1 | <!DOCTYPE html> |
这里荒芜寸草不生 后来你来这里走了一遭 奇迹般万物生长 这里是我的心
A method is a function or a piece of code that is associated with an object.
Let’s say, you have a struct called “Car” and you create a method of the struct named “Drive”, then you declare an object from the struct and name it as “tesla”. So you can call the “Drive” method of your “tesla” to do whatever you programmed the method to do by using the properties or fields of your “tesla” to go the calculation.
What I just explained to you is only an example to make you get the concept of method. So now I will show you how to create your own methods and how to use them.
Basically, all methods are associated with objects, means that you need to have at least one object in your code to declare any methods.
1 | package main |
After you got your struct, you will be able to create your method
1 | package main |
A method is very similar to a function but the difference is that the method is associated with an object and can you the properties inside the object to calculate, just like the “Drive” method that uses “distance” property of “Car” to calculate new “distance”.
In case you need to expose some fields of your struct to other packages without directly exporting them, using methods as “Getter” is your best choice.
1 | package main |
Setter is a way to set the value of unexported fields and getter can be used to setup default value of any field you want like the example below.
1 | package main |
Now you have already learnt how methods work and how to work with methods, but there is another feature in Golang that can enhance the power of structs and their methods. The feature is “Interface”.
An interface is a set of method signature that an object can implement.
To make it more understandable, let’s say you have another struct named “Plane” and its methods: “Fly” and “GetDistance”. And you want to use it with “Car” struct by using the same logic to get their distance. An interface will help you for this kind of task.
1 | package main |
From the example above, you can use the same function with different parameter types by using interface as argument. And before you use the method from the interface, you need to do “type assertion” which is very important step for using interfaces.
Anyway the code can be improved and shorten by declaring an interface type with a set of method signature. For our case, it is “GetDistance” method. You will see how short it is.
1 | package main |
However, don’t forget to validate your interface before using it because somehow it can be nil.
1 | func GetVehicleDistance(i interface{}) int { |
I hope you would get some point about implementing interface with multiple types, it will allow you to create only one logic for executing their method and make you life easier 😂.
一定要爱着点什么,恰似草木对光阴的钟情。
env()
は コントローラー, モデル, etc.. 内で直接使わない。config/*.php
にenv()
の値を入れてconfig()
から参照する。例
1 | <?php |
1 | <?php |
本番環境でconfig:cache
コマンドを実行した際、.env
ファイルを読み込まないから。
.env
ファイルはIlluminate\Foundation\Bootstrap\LoadEnvironmentVariables
クラスのbootstrap()
で読み込まれるのですが、読み込む前にconfig
のキャッシュの有無を確認し、キャッシュがあった場合は.env
ファイルを読み込まない仕様になっています。
当該部分のLaravelのコードを引用。
1 | <?php |
大体の場合の本番環境では、高速化の為に設定を一纏めにするconfig:cache
コマンドを実行すると思うのですが、前述の通りキャッシュがあると.envが読み込まれないので、env()
を直接叩いてると開発時やテスト時には動くけど本番環境で死ぬ。といった事になります。(実際なった)
適当なコントローラーを用意してddを使って値をダンプするだけ。
1 | <?php |
まずはキャッシュ無しでアクセス。
1 | "Foo" |
テスト環境や開発環境と同じ様にキャッシュを作成していないのでどちらも同じ値が取得できます。
次にキャッシュさせてアクセス。artisan
のconfig:cache
を実行してbootstrap/cache/config.php
を作成します。
1 | php artisan config:cache |
で、先ほどと同じ様にアクセスしてみる。
1 | null |
.env
ファイルが読み出されていないのでenv()
で取得した所で値が入ってる訳もなくnullを返します。
と言う訳で最初に書いた通り、env()は直接使わずにconfig()に.envの値を入れて使いましょう。
看人好处 记人长处 帮人难处
假设我要要计算一个用户的积分,为了能够尽快算出用户所有的积分数,我们开启了协程
main.go
1 | package main |
user/user.go
1 | package user |
结果计算的用户积分每次都不一样,正常情况下应该是 1
user/user.go
1 | package user |
nain.go
1 | package main |
user/user.go
1 | package user |
山脚人多,我们山顶见
假设我要要计算一个用户的积分,为了能够尽快算出用户所有的积分数,我们开启了协程
main.go
1 | package main |
user/user.go
1 | package user |
结果计算的用户积分每次都不一样,正常情况下应该是 1
user/user.go
1 | package user |
man.go
1 | package main |
user/user.go
1 | package user |
人活一生,值得爱的东西很多,不要因为一个不满意,就灰心
1 | for _,v := range Uins{ |
1 | func main(a){ |
1 | package main |
1 | package main |
Home is behind, world ahead.
Laravel comes up with an excellent Eloquent ORM. There are lot of cool things in Eloquent, one of which is chunk method.
Usually when you need to process large data, lets say you want to update users table and assign a coupon code based on 3rd party APIs.
What you can do is :
1 | User::get()->each(function($user){ |
Doing this is good but if you have thousands of users, loading them all up at once to do the coupon saving process. That’s going to take consume a hige memory at a time and maybe the server will be exusted because so much data is stored in memory for processing at a time.
The chunk method helps these kind of implementations. Chunking records means taking a batch of records by a limit, process those, take the next batch processing those and so on… The idea is you are taking a subset of data to process at a time instead of entire data loaded into memory.
The chunk method will retrieve a “chunk” of Eloquent models, feeding them to a given Closure for processing. Using the chunk method will conserve memory when working with large result sets.
Lets do the same but with chunk this time :
1 | $recodsPerBatch = 50; |
Now, as you can probably guess this will take 50 user records at a time to process, once those are completed it will take next 50 untill all records are procesed by chunk closure.
Let’s apply limit to the above example :
1 | $recodsPerBatch = 50; |
If you would think laravel will chunk 50 records in 2 batches are we are limiting the total records to 100, oops not really.
Laravel will do series of queries like below to chunk the records :
1 | select * from `users` limit 50 offset 0 |
The chunk method ignores any previous eloquent method which applies limit. It can be limit and offset OR take and skip…
This becomes a problem for which lot of people had raised an issue on laravel’s github repo. But Laravel’s core dev team mentioned this is the expected behaviour and rightfully so.. Chunk itself is using limit to convert entire collection in batches.
Laravel has a chunk variation called chunkById. Let’s use the first example and implement chunkById now :
1 | $recodsPerBatch = 50; |
The main and only fundamental difference between chunk and chunkById is how it structures the query.
1 | select * from `users` where `id` > 0 order by `id` asc limit 50 |
If you observer in the queries done by chunkById :
This gives you an advantage that you can add your offset in the query using id column as a limit just like chunkById is doing internallu.
Let’s limit the chunk in our example with 100 records to chunk :
1 | $recodsPerBatch = 50; |
What we did was, even if we can not use limit directly, we got an id which falls just above the limit we want. Used that to add a where clause of where('id', '<', $maxId)
.
This will then chunk the 100 results, with 2 batches of 50 records in each batch. Cool, isn’t it!
https://techsemicolon.github.io/blog/2019/02/12/laravel-limiting-chunk-collection/
不要到处宣扬自己的内心,这世上不止你一个人有故事。
1 |
|
因为我的目标文件是在windows下创建然后传到服务器上的,这样在利用while read line
读取文件时,如果文件最后一行之后没有换行符\n,则read读取最后一行时遇到文件结束符EOF时循环即终止。上面代码中,虽然此时$line内存有最后一行的内容,但程序已经没有机会再处理此行内容,因此导致了最后一行无法读取。
修改while循环,增加 [[ -n ${line} ]]
,这样当文件没有到最后一行时不会测试-n $line
,当遇到文件结束(最后一行)时,仍然可以通过测试$line是否有内容来进行继续处理。
1 | #!bin/bash |
通过分析原因可知,本质原因是因为文件格式不是unix导致的,可以直接通过设置文件格式来处理,该方式则脚本代码不需改动。
1 |
|
那些看似不起波澜的日复一日,会突然在某一天让人看到坚持的意义。
对于Go语言(golang)的错误设计,相信很多人已经体验过了,它是通过返回值的方式,来强迫调用者对错误进行处理,要么你忽略,要么你处理(处理也可以是继续返回给调用者),对于golang这种设计方式,我们会在代码中写大量的if判断,以便做出决定。
1 | func main() { |
error其实一个接口,内置的,我们看下它的定义
1 | // The error built-in interface type is the conventional interface for |
它只有一个方法 Error,只要实现了这个方法,就是实现了error。现在我们自己定义一个错误试试。
1 | type fileError struct { |
自定义了一个fileError类型,实现了error接口。现在测试下看看效果。
1 | func main() { |
我们运行模拟的代码,可以看到文件错误的通知。
在实际的使用过程中,我们可能遇到很多错误,他们的区别是错误信息不一样,一种做法是每种错误都类似上面一样定义一个错误类型,但是这样太麻烦了。我们发现Error返回的其实是个字符串,我们可以修改下,让这个字符串可以设置就可以了。
1 | type fileError struct { |
恩,这样改造后,我们就可以在声明fileError的时候,设置好要提示的错误文字,就可以满足我们不同的需要了。
1 | //只是模拟一个错误 |
恩,可以了,已经达到了我们的目的。现在我们可以把它变的更通用一些,比如修改fileError的名字,再创建一个辅助函数,便于我们创建不同的错误类型。
1 | func New(text string) error { |
error 返回的是一般性的错误,但是 panic 函数返回的是让程序崩溃的错误。
也就是当遇到不可恢复的错误状态的时候,如数组访问越界、空指针引用等,这些运行时错误会引起 painc 异常,在一般情况下,我们不应通过调用 panic 函数来报告普通的错误,而应该只把它作为报告致命错误的一种方式。当某些不应该发生的场景发生时,我们就应该调用 panic。
一般而言,当 panic 异常发生时,程序会中断运行。随后,程序崩溃并输出日志信息。日志信息包括 panic value 和函数调用的堆栈跟踪信息。
当然,如果直接调用内置的 panic 函数也会引发 panic 异常;panic 函数接受任何值作为参数。
1 | func TestA() { |
我们在实际的开发过程中并不会直接调用 panic ( ) 函数,但是当我们编程的程序遇到致命错误时,系统会自动调用该函数来终止整个程序的运行,也就是系统内置了 panic 函数。
1 | func TestA() { |
运行时 panic 异常一旦被引发就会导致程序崩溃。这当然不是我们愿意看到的,因为谁也不能保证程序不会发生任何运行时错误。
Go 语言为我们提供了专用于 “拦截” 运行时 panic 的内建函数 ——recover。它可以是当前的程序从运行时 panic 的状态中恢复并重新获得流程控制权。
注意:recover 只有在 defer 调用的函数中有效。
1 | package main |
通过以上程序,我们发现虽然 TestB () 函数会导致整个应用程序崩溃,但是由于在改函数中调用了 recover () 函数,所以整个函数并没有崩溃。虽然程序没有崩溃,但是我们也没有看到任何的提示信息,那么怎样才能够看到相应的提示信息呢?
1 | package main |
https://xuhy.top/posts/develop/go/go-lesson/go-lesson07/
https://learnku.com/docs/qianxi-golang/2020/section-2-panic-function/6444#ed4f2f
https://www.flysnow.org/2019/01/01/golang-error-handle-suggestion.html
无聊是非常有必要的,一个人在空白时间所做的事,决定了这个人和他人根本的不同。
Go编译的程序非常适合部署,如果没有通过CGO引用其它的库的话,我们一般编译出来的可执行二进制文件都是单个的文件,非常适合复制和部署。在实际使用中,除了二进制文件,可能还需要一些配置文件,或者静态文件,比如html模板、静态的图片、CSS、javascript等文件,如何这些文件也能打进到二进制文件中,那就太美妙,我们只需复制、按照单个的可执行文件即可。
比如当前文件下有个hello.txt的文件,文件内容为hello,world!。通过go:embed指令,在编译后下面程序中的s变量的值就变为了hello,world!。
1 | package main |
你还可以把单个文件的内容嵌入为slice of byte,也就是一个字节数组。
1 | package main |
甚至你可以嵌入为一个文件系统,这在嵌入多个文件的时候非常有用。
比如嵌入一个文件:
1 | package main |
嵌入本地的另外一个文件hello2.txt, 支持同一个变量上多个go:embed指令(嵌入为string或者byte slice是不能有多个go:embed指令的):
1 | package main |
当前重复的go:embed指令嵌入为embed.FS是支持的,相当于一个:
1 | package main |
还可以嵌入子文件夹下的文件:
1 | package main |
比如下面的例子,s和s2变量都嵌入hello.txt的文件。
1 | package main |
Go可以将文件可以嵌入为exported的变量,也可以嵌入为unexported的变量。
1 | package main |
前面的例子都是package一级的的变量,即使是函数内的局部变量,也都支持嵌入:
1 | package main |
局部变量s的值在编译时就已经嵌入了,而且虽然s和s2嵌入同一个文件,但是它们的值在编译的时候会使用初始化字段中的不同的值
嵌入的内容是只读的。也就是在编译期嵌入文件的内容是什么,那么在运行时的内容也就是什么。
FS文件系统值提供了打开和读取的方法,并没有write的方法,也就是说FS实例是线程安全的,多个goroutine可以并发使用。
1 | package main |
当然你也可以像前面的例子一样写成多行go:embed:
1 | package main |
文件夹分隔符采用正斜杠/,即使是windows系统也采用这个模式。
1 | package main |
相对路径的根路径是go源文件所在的文件夹。
支持使用双引号”或者反引号的方式应用到嵌入的文件名或者文件夹名或者模式名上,这对名称中带空格或者特殊字符的文件文件夹有用。
1 | package main |
go:embed
指令中可以只写文件夹名,此文件夹中除了.
和_
开头的文件和文件夹都会被嵌入,并且子文件夹也会被递归的嵌入,形成一个此文件夹的文件系统。
如果想嵌入.
和_
开头的文件和文件夹, 比如p
文件夹下的.hello.txt
文件,那么就需要使用*
,比如go:embed p/*
。
不具有递归性,所以子文件夹下的.和_不会被嵌入,除非你在专门使用子文件夹的进行嵌入
1 | package main |
嵌入和嵌入模式不支持绝对路径、不支持路径中包含.和..,如果想嵌入go源文件所在的路径,使用*
:
1 | package main |
embed.FS实现了 io/fs.FS接口,它可以打开一个文件,返回fs.File:
1 | package main |
它还提供了ReadFileh和ReadDir功能,遍历一个文件下的文件和文件夹信息:
1 | package main |
因为它实现了io/fs.FS接口,所以可以返回它的子文件夹作为新的文件系统:
1 | package main |
先前,我们提供一个静态文件的服务时,使用:
1 | http.Handle("/", http.FileServer(http.Dir("/tmp"))) |
现在,io/fs.FS文件系统也可以转换成http.FileServer的参数了:
1 | type FileSystem |
所以,嵌入文件可以使用下面的方式:
1 | http.Handle("/", http.FileServer(http.FS(fsys))) |
同样的,template也可以从嵌入的文件系统中解析模板:
1 | func ParseFS(fsys fs.FS, patterns ...string) (*Template, error) |