唉,曾经。JetBrains 的体验真的比 VS Code 好太多,哪怕在 VS Code 最擅长的前端领域,我也敢说 WebStorm 吊打它。奈何,现在是 AI 的时代,IDE 已经被 Cursor 为首的 AI IDE 重塑,而 JetBrains 真的在 AI 的潮流上落后了。打败你的,可能并不是你的竞争对手。
通常在本地化时往往会涉及到时区转换的问题,而通常在真正关注到时区之前我们所「默认」使用的时区为 UTC 或 “本地”。
本文以 Go 为例,分析下 Go 中的时区使用。
读取时区
在 Go 中,读取时区使用的是 LoadLocation 函数。
// LoadLocation returns the Location with the given name.
//
// If the name is "" or "UTC", LoadLocation returns UTC.
// If the name is "Local", LoadLocation returns Local.
//
// Otherwise, the name is taken to be a location name corresponding to a file
// in the IANA Time Zone database, such as "America/New_York".
//
// LoadLocation looks for the IANA Time Zone database in the following
// locations in order:
//
// - the directory or uncompressed zip file named by the ZONEINFO environment variable
// - on a Unix system, the system standard installation location
// - $GOROOT/lib/time/zoneinfo.zip
// - the time/tzdata package, if it was imported
func LoadLocation(name string) (*Location, error)
阅读注释可知,如果 name 为空 / UTC 则使用 UTC、为 Local 则使用本地时区(在后面进行讲解),否则,从特定位置进行读取。
type Time struct {
wall uint64
ext int64
loc *Location
}
// Now returns the current local time.
func Now() Time {
sec, nsec, mono := now()
mono -= startNano
sec += unixToInternal - minWall
if uint64(sec)>>33 != 0 {
// Seconds field overflowed the 33 bits available when
// storing a monotonic time. This will be true after
// March 16, 2157.
return Time{uint64(nsec), sec + minWall, Local}
}
return Time{hasMonotonic | uint64(sec)<<nsecShift | uint64(nsec), mono, Local}
}
可以看到,Time 结构体的最后一个字段 loc *Location 就是时区,而 time.Now 中使用的时区为 Local。
// Local represents the system's local time zone.
// On Unix systems, Local consults the TZ environment
// variable to find the time zone to use. No TZ means
// use the system default /etc/localtime.
// TZ="" means use UTC.
// TZ="foo" means use file foo in the system timezone directory.
var Local *Location = &localLoc
阅读 Go 中关于 Local 的说明可知,Go 会优先尊重 TZ 环境变量所指定的时区,如果没有特殊指定,则使用 /etc/localtime 文件读取当前时区。
那么,Local 又是怎么初始化的呢?
// localLoc is separate so that initLocal can initialize
// it even if a client has changed Local.
var localLoc Location
var localOnce sync.Once
func (l *Location) get() *Location {
if l == nil {
return &utcLoc
}
if l == &localLoc {
localOnce.Do(initLocal)
}
return l
}
从这段代码的逻辑中不难猜出,Local 并没有真的在程序启动时读取上述信息,而是在首次使用时才真正的通过执行 initLocal 函数来进行初始化。同时,这段代码也隐性的为使用 Location 提出了一个要求:必须调用 get 方法来获取「真正的 Location」。
// 为了减少文章长度突出重点,注释部分有所删改
// A Time represents an instant in time with nanosecond precision.
//
// The zero value of type Time is January 1, year 1, 00:00:00.000000000 UTC.
//
// In addition to the required “wall clock” reading, a Time may contain an optional
// reading of the current process's monotonic clock, to provide additional precision
// for comparison or subtraction.
type Time struct {
// wall and ext encode the wall time seconds, wall time nanoseconds,
// and optional monotonic clock reading in nanoseconds.
//
// From high to low bit position, wall encodes a 1-bit flag (hasMonotonic),
// a 33-bit seconds field, and a 30-bit wall time nanoseconds field.
// The nanoseconds field is in the range [0, 999999999].
// If the hasMonotonic bit is 0, then the 33-bit field must be zero
// and the full signed 64-bit wall seconds since Jan 1 year 1 is stored in ext.
// If the hasMonotonic bit is 1, then the 33-bit field holds a 33-bit
// unsigned wall seconds since Jan 1 year 1885, and ext holds a
// signed 64-bit monotonic clock reading, nanoseconds since process start.
wall uint64
ext int64
// 本文重点不在时区,或另写一篇文章讨论相关话题
loc *Location
}
实际上,根据 Golang 内部实现,最大界限受限于系统返回的 monotonic,对 Linux 而言是整个系统 uptime 最大达 292.47 年
64 位有符号自 1.1.1 起的秒数最大值达 2924.7 亿年,我们有生之年是见不到溢出了
获取时间
time.Now()
// Now returns the current local time.
func Now() Time {
sec, nsec, mono := now()
mono -= startNano
sec += unixToInternal - minWall
if uint64(sec)>>33 != 0 {
// Seconds field overflowed the 33 bits available when
// storing a monotonic time. This will be true after
// March 16, 2157.
return Time{uint64(nsec), sec + minWall, Local}
}
return Time{hasMonotonic | uint64(sec)<<nsecShift | uint64(nsec), mono, Local}
}
// Provided by package runtime.
func now() (sec int64, nsec int32, mono int64)
前面说了,存储在 Time 中的单调钟的值并非系统返回的,而是自进程启动起的纳秒数,因此这里进行了一次减法操作(与系统启动时的系统单调钟的值做减法)
// Monotonic times are reported as offsets from startNano.
// We initialize startNano to runtimeNano() - 1 so that on systems where
// monotonic time resolution is fairly low (e.g. Windows 2008
// which appears to have a default resolution of 15ms),
// we avoid ever reporting a monotonic time of 0.
// (Callers may want to use 0 as "time not set".)
var startNano int64 = runtimeNano() - 1
// Unix returns the local Time corresponding to the given Unix time,
// sec seconds and nsec nanoseconds since January 1, 1970 UTC.
// It is valid to pass nsec outside the range [0, 999999999].
// Not all sec values have a corresponding time value. One such
// value is 1<<63-1 (the largest int64 value).
func Unix(sec int64, nsec int64) Time {
if nsec < 0 || nsec >= 1e9 {
n := nsec / 1e9
sec += n
nsec -= n * 1e9
if nsec < 0 {
nsec += 1e9
sec--
}
}
return unixTime(sec, int32(nsec))
}
func unixTime(sec int64, nsec int32) Time {
return Time{uint64(nsec), sec + unixToInternal, Local}
}
const (
unixToInternal int64 = (1969*365 + 1969/4 - 1969/100 + 1969/400) * secondsPerDay
)
// UnixMilli returns the local Time corresponding to the given Unix time,
// msec milliseconds since January 1, 1970 UTC.
func UnixMilli(msec int64) Time {
return Unix(msec/1e3, (msec%1e3)*1e6)
}
// UnixMicro returns the local Time corresponding to the given Unix time,
// usec microseconds since January 1, 1970 UTC.
func UnixMicro(usec int64) Time {
return Unix(usec/1e6, (usec%1e6)*1e3)
}
// 为了减少文章长度突出重点,代码和注释部分有所删改
// Date returns the Time corresponding to
//
// yyyy-mm-dd hh:mm:ss + nsec nanoseconds
//
// The month, day, hour, min, sec, and nsec values may be outside
// their usual ranges and will be normalized during the conversion.
// For example, October 32 converts to November 1.
func Date(year int, month Month, day, hour, min, sec, nsec int, loc *Location) Time {
...
// Normalize overflow
...
// Compute days since the absolute epoch.
d := daysSinceEpoch(year)
// Add in days before this month.
d += uint64(daysBefore[month-1])
if isLeap(year) && month >= March {
d++ // February 29
}
// Add in days before today.
d += uint64(day - 1)
// Add in time elapsed today.
abs := d * secondsPerDay
abs += uint64(hour*secondsPerHour + min*secondsPerMinute + sec)
unix := int64(abs) + (absoluteToInternal + internalToUnix)
// Look for zone offset for expected time, so we can adjust to UTC.
...
t := unixTime(unix, int32(nsec))
t.setLoc(loc)
return t
}
其实一切难点在我们了解了 time.Time 的结构体之后都解决了,设计好结构体后,让你自己去写 Sub 你也会这么写。
话不多说,让我们直接来看 Sub 的代码
// Sub returns the duration t-u. If the result exceeds the maximum (or minimum)
// value that can be stored in a Duration, the maximum (or minimum) duration
// will be returned.
// To compute t-d for a duration d, use t.Add(-d).
func (t Time) Sub(u Time) Duration {
if t.wall&u.wall&hasMonotonic != 0 {
te := t.ext
ue := u.ext
d := Duration(te - ue)
if d < 0 && te > ue {
return maxDuration // t - u is positive out of range
}
if d > 0 && te < ue {
return minDuration // t - u is negative out of range
}
return d
}
d := Duration(t.sec()-u.sec())*Second + Duration(t.nsec()-u.nsec())
// Check for overflow or underflow.
switch {
case u.Add(d).Equal(t):
return d // d is correct
case t.Before(u):
return minDuration // t - u is negative out of range
default:
return maxDuration // t - u is positive out of range
}
}
Time 的初始化没有魔法,和 Go 中其他结构体的初始化 0 值相同 —— 其所有字段都被赋予了 0。那么,根据规则,其 hasMonotonic=0,因此使用 ext 存储秒、wall 的第三部分存储纳秒,这俩也是 0,所以,0 值的时间就是 January 1, year 1, 00:00:00.000000000 UTC
本文重点不在时区,时区不会影响 Time 中的 wall 和 ext(其都是用 UTC 值存储的),只会影响其中的 loc 字段
// Equal reports whether t and u represent the same time instant.
// Two times can be equal even if they are in different locations.
// For example, 6:00 +0200 and 4:00 UTC are Equal.
// See the documentation on the Time type for the pitfalls of using == with
// Time values; most code should use Equal instead.
func (t Time) Equal(u Time) bool {
if t.wall&u.wall&hasMonotonic != 0 {
return t.ext == u.ext
}
return t.sec() == u.sec() && t.nsec() == u.nsec()
}
在 Linux Amd64 上,整个 time_now 都是用汇编实现的,下面的代码是我加了注释的版本,你可从此链接阅读原始版本
// 为了减少文章长度突出重点,代码和注释部分有所删改
// func time.now() (sec int64, nsec int32, mono int64)
// $16-24 表示函数需要 16 字节的栈空间和 24 字节的返回值空间
TEXT time·now<ABIInternal>(SB),NOSPLIT,$16-24
// 准备 vDSO
...
// (如果不是 g0)切换到 g0
...
noswitch: // 获取时间的逻辑(优先利用 vDSO)
SUBQ $32, SP // Space for two time results
ANDQ $~15, SP // Align for C code
MOVL $0, DI // CLOCK_REALTIME(获取日历时钟)
LEAQ 16(SP), SI
MOVQ runtime·vdsoClockgettimeSym(SB), AX
CMPQ AX, $0
JEQ fallback // 失败跳转
CALL AX
MOVL $1, DI // CLOCK_MONOTONIC(获取单调钟)
LEAQ 0(SP), SI
MOVQ runtime·vdsoClockgettimeSym(SB), AX
CALL AX
ret: // 获取成功,结果存在了栈中
MOVQ 16(SP), AX // realtime sec
MOVQ 24(SP), DI // realtime nsec (moved to BX below)
MOVQ 0(SP), CX // monotonic sec
IMULQ $1000000000, CX
MOVQ 8(SP), DX // monotonic nsec
// 恢复现场
...
// 返回结果
// set result registers; AX is already correct
MOVQ DI, BX
ADDQ DX, CX // 计算出 monotonic nanoseconds
RET
fallback: // CLOCK_REALTIME 获取失败时,证明 vdso 失败,利用系统调用获取
MOVQ $SYS_clock_gettime, AX
SYSCALL
MOVL $1, DI // CLOCK_MONOTONIC
LEAQ 0(SP), SI
MOVQ $SYS_clock_gettime, AX
SYSCALL
JMP ret
简单来说,在 Linux Amd64 下,Go 会尽可能利用 vDSO 获取时间信息(包括日历时钟和单调钟的时间),如果出现错误才会 fallback 到系统调用。vDSO 的主要目的是为了降低系统调用的时间,具体你可阅读 Linux 手册获得更多信息。在 Go 中,实际上只有和时间有关的系统调用才用到了 vDSO:vdso_linux_amd64.go。
而非 Linux Amd64 下,使用了 walltime() 和 nanotime() 来分别获取日历时钟和单调钟。
// Round returns the result of rounding t to the nearest multiple of d (since the zero time).
// The rounding behavior for halfway values is to round up.
// If d <= 0, Round returns t stripped of any monotonic clock reading but otherwise unchanged.
//
// Round operates on the time as an absolute duration since the
// zero time; it does not operate on the presentation form of the
// time. Thus, Round(Hour) may return a time with a non-zero
// minute, depending on the time's Location.
func (t Time) Round(d Duration) Time {
t.stripMono()
if d <= 0 {
return t
}
_, r := div(t, d)
if lessThanHalf(r, d) {
return t.Add(-r)
}
return t.Add(d - r)
}
// stripMono strips the monotonic clock reading in t.
func (t *Time) stripMono() {
if t.wall&hasMonotonic != 0 {
t.ext = t.sec()
t.wall &= nsecMask
}
}
你可以利用 Duration 来提供精度(例如 t.Round(time.Second))。
另外,它还有一个「副作用」,就是会将我们前面说过的 Time 中的单调钟的信息删除(第一行的 stripMono)。因此,除了四舍五入,如果你想让他删除单调钟的内容,可以使用 t.Round(0)。
Initialized empty Git repository in /opt/homebrew/Library/Taps/imsingee/homebrew-pnpm/.git/
[main (root-commit) 1b89b92] Create imsingee/pnpm tap
3 files changed, 90 insertions(+)
create mode 100644 .github/workflows/publish.yml
create mode 100644 .github/workflows/tests.yml
create mode 100644 README.md
==> Created imsingee/pnpm
/opt/homebrew/Library/Taps/imsingee/homebrew-pnpm
When a pull request making changes to a formula (or formulae) becomes green
(all checks passed), then you can publish the built bottles.
To do so, label your PR as `pr-pull` and the workflow will be triggered.
class PnpmAT7 < Formula
require "language/node"
desc "📦🚀 Fast, disk space efficient package manager"
homepage "https://pnpm.io/"
url "https://registry.npmjs.org/pnpm/-/pnpm-7.32.2.tgz"
sha256 "f4b40caa0c6368da2f50b8ef891f225c24f14e7d60e42a703c84d3a9db8efede"
license "MIT"
livecheck do
url "https://registry.npmjs.org/pnpm"
regex(/["']latest-7["']:\s*?["']([^"']+)["']/i)
end
depends_on "node" => :test
conflicts_with "corepack", because: "both installs `pnpm` and `pnpx` binaries"
def install
libexec.install buildpath.glob("*")
bin.install_symlink "#{libexec}/bin/pnpm.cjs" => "pnpm"
bin.install_symlink "#{libexec}/bin/pnpx.cjs" => "pnpx"
end
def caveats
<<~EOS
pnpm requires a Node installation to function. You can install one with:
brew install node
EOS
end
test do
system "#{bin}/pnpm", "init"
assert_predicate testpath/"package.json", :exist?, "package.json must exist"
end
end
pnpm@7 is keg-only, which means it was not symlinked into /opt/homebrew,
because this is an alternate version of another formula.
If you need to have pnpm@7 first in your PATH, run:
echo 'export PATH="/opt/homebrew/opt/pnpm@7/bin:$PATH"' >> ~/.zshrc
与一般的 formula buts,这种会被检测到是另一个程序的不同版本,因此不会安装到系统的 PATH 下,而是独立的放在额外的路径。
// newTypeEncoder constructs an encoderFunc for a type.
// The returned encoder only checks CanAddr when allowAddr is true.
func newTypeEncoder(t reflect.Type, allowAddr bool) encoderFunc {
// If we have a non-pointer value whose type implements
// Marshaler with a value receiver, then we're better off taking
// the address of the value - otherwise we end up with an
// allocation as we cast the value to an interface.
if t.Kind() != reflect.Pointer && allowAddr && reflect.PointerTo(t).Implements(marshalerType) {
return newCondAddrEncoder(addrMarshalerEncoder, newTypeEncoder(t, false))
}
if t.Implements(marshalerType) {
return marshalerEncoder
}
if t.Kind() != reflect.Pointer && allowAddr && reflect.PointerTo(t).Implements(textMarshalerType) {
return newCondAddrEncoder(addrTextMarshalerEncoder, newTypeEncoder(t, false))
}
if t.Implements(textMarshalerType) {
return textMarshalerEncoder
}
switch t.Kind() {
case reflect.Bool:
return boolEncoder
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
return intEncoder
case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Uintptr:
return uintEncoder
case reflect.Float32:
return float32Encoder
case reflect.Float64:
return float64Encoder
case reflect.String:
return stringEncoder
case reflect.Interface:
return interfaceEncoder
case reflect.Struct:
return newStructEncoder(t)
case reflect.Map:
return newMapEncoder(t)
case reflect.Slice:
return newSliceEncoder(t)
case reflect.Array:
return newArrayEncoder(t)
case reflect.Pointer:
return newPtrEncoder(t)
default:
return unsupportedTypeEncoder
}
}