Repository: go-co-op/gocron
Pull request #531 (2023-07-30 09:41:01)

feat: handle pointer to func
Hi,
Merging this pull request can add the ability to pass the pointer of func in `Do()` method.
Example:
```go
fn := func() { fmt.Println("Hi") }
_, err := s.Every(1).Second().Do(&fn)
fmt.Println(err)
// nil

_, err = s.Every(1).Second().Do(fn)
fmt.Println(err)
// nil
```
I have added a test for that, and it works properly and is also backward compatible.
If you have any ideas or concerns regarding the PR, feel free to tell me.


Patch:
diff --git a/.golangci.yaml b/.golangci.yaml
index 7c86335..0354528 100644
--- a/.golangci.yaml
+++ b/.golangci.yaml
@@ -1,5 +1,5 @@
 run:
-  timeout: 2m
+  timeout: 5m
   issues-exit-code: 1
   tests: true
 
@@ -15,8 +15,7 @@ issues:
 linters:
   enable:
     - bodyclose
-    - errcheck
-    - gofmt
+    - exportloopref
     - gofumpt
     - goimports
     - gosec
@@ -28,6 +27,7 @@ linters:
     - staticcheck
     - typecheck
     - unused
+    - whitespace
 
 output:
   # colored-line-number|line-number|json|tab|checkstyle|code-climate, default is "colored-line-number"
diff --git a/gocron.go b/gocron.go
index 8ca2499..cb66702 100644
--- a/gocron.go
+++ b/gocron.go
@@ -127,6 +127,10 @@ func getFunctionName(fn interface{}) string {
 	return runtime.FuncForPC(reflect.ValueOf(fn).Pointer()).Name()
 }
 
+func getFunctionNameOfPointer(fn interface{}) string {
+	return runtime.FuncForPC(reflect.ValueOf(fn).Elem().Pointer()).Name()
+}
+
 func parseTime(t string) (hour, min, sec int, err error) {
 	var timeLayout string
 	switch {
diff --git a/job.go b/job.go
index 4744bac..d72a0f1 100644
--- a/job.go
+++ b/job.go
@@ -312,6 +312,12 @@ func (j *Job) setDuration(t time.Duration) {
 	j.duration = t
 }
 
+func (j *Job) setInterval(i int) {
+	j.mu.Lock()
+	defer j.mu.Unlock()
+	j.interval = i
+}
+
 // hasTags returns true if all tags are matched on this Job
 func (j *Job) hasTags(tags ...string) bool {
 	// Build map of all Job tags for easy comparison
diff --git a/scheduler.go b/scheduler.go
index 9d7f614..7b763d2 100644
--- a/scheduler.go
+++ b/scheduler.go
@@ -289,7 +289,6 @@ func (s *Scheduler) calculateMonths(job *Job, lastRun time.Time) nextRun {
 	}
 
 	if len(job.daysOfTheMonth) != 0 { // calculate days to job.daysOfTheMonth
-
 		nextRunDateMap := make(map[int]nextRun)
 		for _, day := range job.daysOfTheMonth {
 			nextRunDateMap[day] = calculateNextRunForMonth(s, job, lastRun, day)
@@ -548,7 +547,7 @@ func (s *Scheduler) Every(interval interface{}) *Scheduler {
 			job.error = wrapOrError(job.error, ErrInvalidInterval)
 		}
 	case time.Duration:
-		job.interval = 0
+		job.setInterval(0)
 		job.setDuration(interval)
 		job.setUnit(duration)
 	case string:
@@ -932,22 +931,35 @@ func (s *Scheduler) doCommon(jobFun interface{}, params ...interface{}) (*Job, e
 		return nil, job.error
 	}
 
-	typ := reflect.TypeOf(jobFun)
-	if typ.Kind() != reflect.Func {
+	val := reflect.ValueOf(jobFun)
+	for val.Kind() == reflect.Ptr {
+		val = val.Elem()
+	}
+
+	if val.Kind() != reflect.Func {
 		// delete the job for the same reason as above
 		s.RemoveByReference(job)
 		return nil, ErrNotAFunction
 	}
 
-	fname := getFunctionName(jobFun)
+	var fname string
+	if val == reflect.ValueOf(jobFun) {
+		fname = getFunctionName(jobFun)
+	} else {
+		fname = getFunctionNameOfPointer(jobFun)
+	}
+
 	if job.funcName != fname {
 		job.function = jobFun
+		if val != reflect.ValueOf(jobFun) {
+			job.function = val.Interface()
+		}
+
 		job.parameters = params
 		job.funcName = fname
 	}
 
-	f := reflect.ValueOf(jobFun)
-	expectedParamLength := f.Type().NumIn()
+	expectedParamLength := val.Type().NumIn()
 	if job.runWithDetails {
 		expectedParamLength--
 	}
@@ -958,7 +970,7 @@ func (s *Scheduler) doCommon(jobFun interface{}, params ...interface{}) (*Job, e
 		return nil, job.error
 	}
 
-	if job.runWithDetails && f.Type().In(len(params)).Kind() != reflect.ValueOf(*job).Kind() {
+	if job.runWithDetails && val.Type().In(len(params)).Kind() != reflect.ValueOf(*job).Kind() {
 		s.RemoveByReference(job)
 		job.error = wrapOrError(job.error, ErrDoWithJobDetails)
 		return nil, job.error
@@ -1153,10 +1165,8 @@ func (s *Scheduler) Months(daysOfTheMonth ...int) *Scheduler {
 			job.error = wrapOrError(job.error, ErrInvalidDayOfMonthEntry)
 		}
 	} else {
-
 		repeatMap := make(map[int]int)
 		for _, dayOfMonth := range daysOfTheMonth {
-
 			if dayOfMonth < 1 || dayOfMonth > 28 {
 				job.error = wrapOrError(job.error, ErrInvalidDayOfMonthEntry)
 				break
@@ -1298,7 +1308,11 @@ func (s *Scheduler) Update() (*Job, error) {
 	job.setStartsImmediately(false)
 
 	if job.runWithDetails {
-		return s.DoWithJobDetails(job.function, job.parameters...)
+		params := job.parameters
+		if len(params) > 0 {
+			params = job.parameters[:len(job.parameters)-1]
+		}
+		return s.DoWithJobDetails(job.function, params...)
 	}
 
 	if job.runConfig.mode == singletonMode {


Test patch:
diff --git a/scheduler_test.go b/scheduler_test.go
index 6a67796..3147222 100644
--- a/scheduler_test.go
+++ b/scheduler_test.go
@@ -57,6 +57,24 @@ func TestImmediateExecution(t *testing.T) {
 	}
 }
 
+func TestExecutionWithPointerToFunc(t *testing.T) {
+	s := NewScheduler(time.UTC)
+	semaphore := make(chan bool)
+	fn := func() { semaphore <- true }
+
+	_, err := s.Every(1).Second().Do(&fn)
+	require.NoError(t, err)
+	s.StartAsync()
+	select {
+	case <-time.After(time.Second):
+		s.stop()
+		t.Fatal("job did not run immediately")
+	case <-semaphore:
+		// test passed
+		s.stop()
+	}
+}
+
 func TestScheduler_Every_InvalidInterval(t *testing.T) {
 	testCases := []struct {
 		description   string
@@ -1931,6 +1949,19 @@ func TestScheduler_Update(t *testing.T) {
 		require.NoError(t, err)
 		s.Stop()
 	})
+
+	// Verifies https://github.com/go-co-op/gocron/issues/528
+	t.Run("update with DoWithJobDetails", func(t *testing.T) {
+		s := NewScheduler(time.UTC)
+		j, err := s.Every(time.Second).DoWithJobDetails(func(job Job) {})
+		require.NoError(t, err)
+		s.StartAsync()
+		time.Sleep(time.Millisecond * 500)
+
+		_, err = s.Job(j).Every(2 * time.Second).Update()
+		require.NoError(t, err)
+		s.Stop()
+	})
 }
 
 func TestScheduler_RunByTag(t *testing.T) {